lxml è la libreria più veloce per il parsing di HTML e XML nell’ecosistema Python. È costruita sulle librerie C libxml2 e libxslt, quindi supera di gran lunga i parser scritti in puro Python e in più offre XPath completo, più potente dei selettori CSS. Se hai toccato il limite di prestazioni di BeautifulSoup sui grandi volumi o ti servono selezioni complesse, lxml è la scelta giusta.
Questo articolo approfondisce un tema della guida generale «Web scraping con Python», dove lxml è citato accanto ad altri parser. Qui lo analizziamo in dettaglio.
Indice
- Installazione e i due moduli di lxml
- Parsing dell’HTML
- XPath: la base della potenza di lxml
- Selettori CSS con cssselect
- Estrazione dei dati: testo, attributi e link
- Accenti e codifiche in lxml
- Documenti di grandi dimensioni: iterparse
- lxml vs BeautifulSoup
- Pro e contro
1. Installazione e i due moduli di lxml
pip install lxmllxml ha due moduli chiave:
lxml.html— per il parsing dell’HTML (perdona il markup «sporco», come un browser).lxml.etree— per l’XML rigoroso (vedi l’articolo dedicato «Parsing di XML in Python»).
Per lo scraping web, nel 95% dei casi ti serve lxml.html.
2. Parsing dell’HTML
import requests
from lxml import html
resp = requests.get("https://example.com", timeout=10)
tree = html.fromstring(resp.content) # passiamo i byte: lxml riconosce la codifica da solofromstring restituisce l’elemento radice dell’albero. Da lì si applicano XPath o selettori CSS. Si può fare il parsing anche da un file:
tree = html.parse("page.html").getroot()Importante: passa resp.content (byte) e non resp.text (stringa): così lxml leggerà correttamente la dichiarazione di codifica dall’HTML stesso. Questo elimina la maggior parte dei problemi con le lettere accentate e i caratteri speciali.
3. XPath: la base della potenza di lxml
XPath è un linguaggio per indirizzare i nodi nell’albero del documento, ed è qui che lxml dà il meglio di sé.
# testo di tutti i titoli h2 con classe title
titles = tree.xpath('//h2[@class="title"]/text()')
# attributo href di tutti i link
links = tree.xpath('//a/@href')
# testo dentro un blocco specifico
price = tree.xpath('//div[@class="price"]/text()')[0]Costrutti XPath utili
# per corrispondenza parziale della classe (quando class="title big featured")
tree.xpath('//h2[contains(@class, "title")]/text()')
# per il testo dell'elemento
tree.xpath('//a[text()="Leggi di più"]/@href')
# l'n-esimo elemento (la numerazione parte da 1!)
tree.xpath('(//div[@class="item"])[3]')
# percorso relativo dal nodo trovato
for card in tree.xpath('//div[@class="card"]'):
name = card.xpath('.//h3/text()') # il punto significa «dal nodo corrente»
link = card.xpath('.//a/@href')Il punto iniziale (.//) in un XPath relativo è critico: senza, la ricerca parte dalla radice del documento e non dalla card corrente. È l’errore più comune tra chi inizia.
Gli assi di XPath
XPath sa muoversi nell’albero in qualsiasi direzione, cosa che il CSS non permette:
# il padre dell'elemento
tree.xpath('//span[@class="price"]/parent::div')
# l'elemento successivo dello stesso livello
tree.xpath('//h2/following-sibling::p[1]/text()')
# l'antenato con una classe specifica
tree.xpath('//a[@id="buy"]/ancestor::div[@class="product"]')
4. Selettori CSS con cssselect
Se XPath ti sembra macchinoso, lxml supporta i selettori CSS (serve il pacchetto cssselect):
pip install cssselect# il metodo .cssselect() restituisce una lista di elementi
cards = tree.cssselect("div.product-card")
title = tree.cssselect("h1.title")[0].text_content()
links = [a.get("href") for a in tree.cssselect("a.product-link")]Il CSS risulta più familiare a chi arriva dal front-end. Sotto il cofano cssselect traduce il CSS in XPath, quindi la velocità resta la stessa. Per le selezioni complesse (per testo, per antenati) servirà comunque XPath.
5. Estrazione dei dati: testo, attributi e link
el = tree.cssselect(".product")[0]
el.text_content() # tutto il testo interno, tag annidati inclusi
el.get("href") # valore di un attributo
el.attrib # dizionario di tutti gli attributi
el.tag # nome del tag
el.text # solo il testo diretto del nodo (senza gli annidati)La differenza tra .text e .text_content() è importante:
# <p>Ciao, <b>mondo</b>!</p>
p.text # "Ciao, " (solo fino al tag annidato)
p.text_content() # "Ciao, mondo!" (tutto il testo, in modo ricorsivo)Per trasformare i link relativi in assoluti:
tree.make_links_absolute("https://example.com")
links = tree.xpath('//a/@href') # ora tutti i link sono assolutimake_links_absolute ti risparmia la concatenazione manuale degli URL: comodo quando esplori un sito.
6. Accenti e codifiche in lxml
lxml gestisce senza problemi le lettere accentate e qualsiasi carattere non ASCII, purché riceva byte e non una stringa già decodificata:
# CORRETTO: byte; lxml legge la codifica dal <meta charset>
tree = html.fromstring(resp.content)
# RISCHIOSO: stringa già decodificata da requests (forse in modo sbagliato)
tree = html.fromstring(resp.text)Se devi fissare la codifica a mano (ad esempio quando il server mente nelle intestazioni):
from lxml import html
parser = html.HTMLParser(encoding="windows-1252")
tree = html.fromstring(resp.content, parser=parser)La teoria completa del problema delle codifiche è nella guida generale, sezione «Codifiche».
7. Documenti di grandi dimensioni: iterparse
Quando il documento è enorme (un feed XML da centinaia di megabyte), caricarlo per intero in memoria è uno spreco. iterparse legge in streaming, elaborando gli elementi man mano che compaiono e liberando memoria:
from lxml import etree
for event, element in etree.iterparse("huge.xml", tag="item"):
title = element.findtext("title")
process(title)
element.clear() # liberiamo la memoria
while element.getprevious() is not None:
del element.getparent()[0] # eliminiamo i nodi già elaboratiQuesta tecnica (lettura + clear()) permette di analizzare file che non entrano per intero nella RAM. Altri dettagli sul parsing di XML in streaming in «Parsing di XML in Python».
8. lxml vs BeautifulSoup
| Criterio | lxml | BeautifulSoup |
|---|---|---|
| Velocità | molto alta (C) | inferiore (senza il motore lxml) |
| API | più rigorosa, richiede XPath | amichevole, si legge come testo |
| XPath | completo | no (solo ricerca CSS) |
| HTML «sporco» | bene | molto bene, perdona quasi tutto |
| Memoria | più efficiente, con iterparse | consumo maggiore |
| Soglia d’ingresso | più alta | più bassa |
In pratica spesso si combinano: BeautifulSoup può usare lxml come motore (BeautifulSoup(html, "lxml")): ottieni la comodità dell’API di bs4 e la velocità di lxml. Se invece ti servono XPath o il parsing in streaming di file da gigabyte, usa lxml direttamente. Il confronto tra i vari parser è nella guida generale, e l’estrazione delle tabelle in un articolo a parte.
9. Pro e contro di lxml
Pro:
- Uno dei parser più veloci dell’ecosistema Python (nucleo in C).
- XPath completo con gli assi (parent, sibling, ancestor) — irraggiungibile per il CSS.
- Supporto CSS tramite cssselect: il meglio dei due mondi.
iterparseper l’elaborazione in streaming di documenti giganteschi.- Consumo di memoria contenuto.
Contro:
- Curva di apprendimento più ripida, soprattutto per XPath.
- API più rigorosa e meno «indulgente» di quella di BeautifulSoup.
- Messaggi di errore a volte criptici.
- L’installazione richiede a volte librerie di sistema (su alcuni sistemi operativi).
Conclusione: lxml è la scelta quando contano le prestazioni e le selezioni complesse. Per compiti una tantum e la massima leggibilità è più semplice BeautifulSoup; per volumi giganteschi con selezioni CSS vale la pena dare un’occhiata anche a selectolax. È proprio per la velocità che lxml si abbina spesso allo scraping asincrono: quando si scaricano centinaia di pagine contemporaneamente, un parser lento diventa il collo di bottiglia, e il motore in C di lxml lo elimina.