Scraping per linguaggio 6 min di lettura

Web scraping in Python con lxml: velocità e XPath

Perché lxml è uno dei parser HTML/XML più veloci per Python: query XPath, confronto con BeautifulSoup e tecniche per i documenti di grandi dimensioni.

TW
Team Web-Scraping.it
Raccolta dati per le esigenze del business
Pubblicato il 21 gennaio 2025

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

  1. Installazione e i due moduli di lxml
  2. Parsing dell’HTML
  3. XPath: la base della potenza di lxml
  4. Selettori CSS con cssselect
  5. Estrazione dei dati: testo, attributi e link
  6. Accenti e codifiche in lxml
  7. Documenti di grandi dimensioni: iterparse
  8. lxml vs BeautifulSoup
  9. Pro e contro

1. Installazione e i due moduli di lxml

bash
pip install lxml

lxml 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

python
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 solo

fromstring restituisce l’elemento radice dell’albero. Da lì si applicano XPath o selettori CSS. Si può fare il parsing anche da un file:

python
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é.

python
# 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

python
# 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:

python
# 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):

bash
pip install cssselect
python
# 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

python
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:

python
# <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:

python
tree.make_links_absolute("https://example.com")
links = tree.xpath('//a/@href')   # ora tutti i link sono assoluti

make_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:

python
# 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):

python
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:

python
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à elaborati

Questa 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.
  • iterparse per 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.