lxml, recensito: il motore XPath che continua a superare ogni parser Python

Ultimo aggiornamento il August 12, 2026
lxml, recensito: il motore XPath che continua a superare ogni parser Python
Riassunto AI

Questa recensione di lxml presenta la libreria come il binding Python di lunga durata attorno a libxml2 e libxslt, con un vantaggio principale che i parser più recenti ancora raramente eguagliano: un vero motore XPath. L’articolo testa la copertura XPath, le modalità di rigidità del parser, il comportamento della memoria nello streaming, la differenza di espressività tra CSS e XPath e i limiti di profondità di libxml2. Mostra lxml come veloce, efficiente nella memoria e straordinariamente capace per carichi XML e HTML che richiedono axis, predicati, funzioni, streaming o modalità di recovery robuste. La recensione spiega anche i default di profondità orientati alla sicurezza e quando huge_tree modifica quel confine.

Ogni pochi mesi salta fuori un parser HTML più veloce, i benchmark fanno il giro del web e qualcuno proclama che la vecchia guardia è finita. Poi però devi selezionare ogni paragrafo che contiene una certa parola, oppure risalire al genitore di un nodo trovato, e ti ricordi perché lxml è ancora aperto nell’altra scheda.

lxml è un binding libxml2 vecchio di 20 anni. Non è scintillante. Non è nuovo. E per un compito molto specifico — tutto ciò che richiede un vero XPath — in Python mainstream non ha davvero rivali. Questa è una recensione pratica di ciò che fa, di dove vince in modo silenzioso e dei pochi casi in cui i suoi default possono morderti se non sai che esistono.

lxml in un paragrafo: cos’è davvero

lxml è un binding Python per le librerie C libxml2 e libxslt. È un parser e un serializzatore, non uno scraper e non un browser: trasforma il markup in un albero interrogabile e modificabile, e poi riconverte quell’albero in byte. Offre un’API compatibile con ElementTree, un motore completo XPath 1.0, XSLT 1.0 e validazione tramite schema, ed è mantenuto da Stefan Behnel con lo slogan "the most feature-rich and easy-to-use library for processing XML and HTML in the Python language".

Ecco dove si colloca, secondo uno snapshot GitHub e PyPI raccolto il 2026-07-14:

CampoValore
Repolxml/lxml
Star3.043
Fork620
Issue aperte16
LicenzaBSD-3-Clause
Creato2011-02-11
Ultimo push2026-07-02
Versione stabile su PyPI6.1.1 (2026-05-18)
Motore inclusolibxml2 2.14.6 + libxslt 1.1.43

Una cosa da chiarire subito, prima che qualcuno mi accusi di esagerare: in questa recensione non ci sono segreti. lxml è abbastanza maturo che ogni comportamento citato è documentato da qualche parte nella documentazione di lxml, in un changelog di libxml2 o in un thread di Launchpad. Non ho scoperto trucchi esclusivi o non documentati, e non me li inventerò. Il valore di ciò che segue sta nel fatto che è sistematizzato, quantificato e organizzato attorno a lxml come oggetto di analisi — non nel fatto che sia una novità.

Setup del test (e perché i numeri di timing sono presi in prestito)

Questa recensione usa due categorie di dati, provenienti da due fonti diverse, quindi chiarisco fin da subito cosa è cosa.

I test di capacità — comportamento XPath, le due API del parser, namespace, encoding, ciclo di vita dei nodi — li ho eseguiti da zero su una macchina: macOS arm64, Python 3.14.2, lxml 6.1.1, libxml2 2.14.6. Ogni numero nei file artifacts/raw/*.json è calcolato da uno script eseguito, non inserito a mano. I test di capacità sono booleani ed enum deterministici, quindi un singolo run è stabile: il carico della macchina non cambia il fatto che //a/@href restituisca o meno una stringa attributo.

I numeri di timing e di footprint in memoria non provengono da questo pacchetto. Sono riutilizzati pari pari dal precedente benchmark pack di selectolax — stessa macchina, stesso ambiente virtuale, stessa build di lxml e libxml2, benchmark aggiornati al 2026-07-13 — e qui non li ho rieseguiti. È una scelta deliberata. Rieseguire benchmark temporali insieme a una batteria di script di capacità introdurrebbe contesa sulla CPU e inquinerebbe i valori riutilizzati; in più sarebbe lavoro duplicato: in quel pack lxml era già una libreria di controllo completamente misurata. Riutilizzare gli stessi bench mantiene il confronto omogeneo invece di introdurre una seconda misura, leggermente diversa. Quindi quando sotto vedi un valore in millisecondi, leggilo come "stesso banco di prova, aggiornato al 2026-07-13", non come "l’ho ritimato oggi".

I risultati portano un tag di confidenza: single-observation per i test deterministici di capacità, triple-run per le distribuzioni di timing riutilizzate, hypothesis quando propongo un meccanismo che non ho isolato.

XPath: la cosa che selectolax e BeautifulSoup semplicemente non hanno

Questa è la notizia principale, quindi parto da qui.

lxml XPath coverage moat with axes predicates and functions

Ho sottoposto xpath() di lxml a una matrice pre-registrata di 37 casi — il risultato atteso per ogni scenario era già scritto nel sorgente prima dell’esecuzione, così non potevo assegnare punteggi “a sentimento”. Dieci axis, nove stili di predicato, dieci funzioni built-in, tre tipi di ritorno scalare e cinque casi-trappola costruiti apposta con sintassi XPath 2.0 che il motore 1.0 di lxml dovrebbe rifiutare.

CategoriaCoperturaRisultato
Axischild / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding10/10 passati
Predicati[1] / last() / position()<n / uguaglianza attributo / esistenza attributo / and / or / [.//a] annidato / not()9/9 passati
Funzionitext() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string()10/10 passate
Tipi di ritornoboolean / numeri scalari3/3 passati
Casi-trappolamatches() / sequence / if-then-else / except / errore di sintassi5/5 correttamente rifiutati

Il punteggio è 37/37, e la colonna dei casi-trappola è quella che conta davvero. matches(), le espressioni con sequence, if/then/else ed except sono tutte sintassi XPath 2.0, e il motore 1.0 di libxml2 non le “supporta a metà”: solleva XPathEvalError e basta, invece di restituire in silenzio un set di nodi errato. Quindi qui abbiamo un punteggio perfetto dopo aver provato a romperlo, non un perfetto ottenuto con domande facili. Ogni comportamento è esattamente ciò che la documentazione XPath di lxml descrive, ed è proprio questo il punto.

Ammetto una cosa: l’harness ha sbagliato una volta, ma in un modo che rende il 37/37 davvero affidabile. Il mio primo set atteso per //div[.//a[@href]] prevedeva due risultati; l’esecuzione ne ha restituito uno. Per circa trenta secondi ho pensato che lxml avesse torto, poi ho controllato il fixture e ho scoperto che il secondo elemento era un <footer>, non un <div>: l’aspettativa era sbagliata, non il motore. Ho corretto il set atteso e ho lasciato l’errore in un commento nel sorgente. È il giusto ordine delle colpe: sospetta prima del tuo test, poi della libreria C vecchia di 20 anni.

XPath vs CSS: ciò che in CSS non puoi proprio esprimere

L’affermazione astratta “XPath è più potente” merita un numero concreto, quindi ho quantificato il divario. lxml offre sia .xpath() sia .cssselect() (quest’ultimo traduce CSS in XPath sotto il cofano). Ho preso dieci obiettivi di selezione e verificato quali fossero realmente esprimibili via CSS.

XPath expresses seven of ten tasks CSS cannot express

ObiettivoXPathCSS (cssselect)
Filtrare per contenuto testuale (contains(text(),"bargain"))No, nessun predicato testuale
Selezionare il genitore partendo dal figlio (//b/parent::p)No, nessun selettore parent
Restituire il valore di un attributo (//a/@href)Solo elementi
Restituire un nodo di testo (//p/text())Nessun nodo di testo
Axis ancestor (//td/ancestor::div)Nessuna navigazione verso l’alto
Filtrare il genitore in base al numero di figli (//ul[count(li)=4])Nessun predicato di conteggio
Filtrare per lunghezza del testo (string-length(text())>5)Nessun predicato di lunghezza
nth-child / last-child / sibling adiacenteSì (3 base)

Sette obiettivi su dieci non hanno alcun equivalente CSS. Filtraggio sul contenuto testuale, navigazione verso i genitori e gli antenati, estrazione di un attributo o di un nodo di testo come risultato, predicati basati sul conteggio: CSS non sa esprimere nulla di tutto questo. Solo tre casi (nth-child, last-child, sibling adiacente) funzionano in entrambi. Questa è la risposta quantificata alla domanda “cosa guadagno davvero usando lxml?”. selectolax è solo CSS e non ha proprio il metodo xpath(), quindi quei sette tipi di query lì diventano loop Python in più passaggi oppure non si fanno. Se la tua logica di scraping si appoggia a uno di questi casi, la scelta è già fatta.

(E sì, anche qui l’harness mi ha fregato una seconda volta: avevo previsto un set vuoto per string-length(text())>5, ma due stringhe di sei caratteri hanno effettivamente matchato. Corretto il risultato atteso, non lo strumento.)

Tre livelli di rigidità: etree vs recover vs lxml.html

XPath è il motivo per scegliere lxml. Il controllo di rigidità a tre velocità è il motivo per cui vale la pena continuare a usarlo.

lxml strictness gears: etree, recover, and lxml.html

La maggior parte dei parser offre un solo comportamento per l’input rotto. lxml ne offre tre, e sono abbastanza prevedibili da permettermi di passare sei classi di markup malformato attraverso ciascuna modalità, con esito atteso già pre-registrato per ogni percorso.

Input malformatolxml.etree (rigido)etree + recover=Truelxml.html (tollerante)
Tag non chiuso <root><a>x</root>sollevarecuperaaccetta
Nidificazione errata <b><i></b></i>sollevarecuperaaccetta
Entità non definita &nbsp;sollevarecuperaaccetta
Ampersand nudo & (Tom & Jerry)sollevarecuperaaccetta
Più radici <a>1</a><b>2</b>sollevarecuperaaccetta
XML ben formatoaccettaaccetta (0 errori)accetta
Attributo booleano <input disabled>sollevarecuperaaccetta

Sette su sette hanno corrisposto all’aspettativa pre-registrata. lxml.etree solleva XMLSyntaxError su tutte e sei le classi di errore. Aggiungi recover=True allo stesso parser e lui ingoia gli errori e ricostruisce un albero utilizzabile — e questa è la parte sottovalutata — parser.error_log elenca poi ogni errore che ha assorbito. lxml.html accetta tutto senza lamentarsi.

Il classificatore che decide “solleva vs recupera vs accetta” è guidato dalla lunghezza reale di error_log, non da un valore hard-coded; per questo un documento ben formato, eseguito con recover=True, viene correttamente etichettato come “accetta” (log vuoto) e non come “recupera”. La mia prima versione del classificatore marchiava qualsiasi risultato con recover=True come “recupera” e sbagliava l’input pulito; leggere il vero error_log ha risolto il problema.

Cosa ti dà in pratica: validazione rigorosa quando un feed rotto deve fallire in modo rumoroso, usa lxml.etree. HTML sporco reale che devi solo riuscire a processare, usa lxml.html. E il caso intermedio che la maggior parte degli strumenti non gestisce — “sii tollerante, ma dimmi esattamente cosa era rotto così posso loggarlo” — usa recover=True e leggi l’error log. selectolax ha solo la modalità tollerante e nient’altro: niente modalità rigida e niente error log.

iterparse: il motore di streaming che selectolax non ha proprio

Qui non è una questione di velocità, ma di capacità. selectolax ingerisce solo una stringa intera: non ha alcuna interfaccia incrementale. L’iterparse di lxml emette gli elementi quando si chiudono e, se abbinato al classico pattern fast_iter (chiama elem.clear() ed elimina i sibling precedenti man mano), mantiene la memoria piatta indipendentemente dalla dimensione del documento.

lxml iterparse streams 300K records with about 1-2 MB RSS

Ho misurato direttamente il comportamento in memoria — RSS di picco via ru_maxrss, ogni soggetto in un processo nuovo, su 300.000 elementi <record> per un totale di circa 26,7 MB (26.744.801 byte).

ModalitàDelta RSS di piccoNote
iterparse + clear (fast_iter)~1-2 MBrilascia man mano; piatto a prescindere dal numero di nodi
iterparse senza clear~386 MBmantiene riferimenti; pesante quanto un caricamento completo
etree.parse (caricamento completo, baseline)~386 MBnoto per essere pesante; dimostra che il misuratore legge l’ordine di grandezza

La modalità vincolata mantiene il delta RSS di picco intorno a 1-2 MB contro circa 386 MB del caricamento completo — una differenza di grandezza dello 0,3-0,4% — e il primo evento record scatta prima che il file abbia finito di essere letto, quindi è davvero incrementale, non streaming finto. La riga più istruttiva è quella centrale. Esegui lo stesso loop iterparse ma salta clear(), e la memoria risale a circa 386 MB, perché stai tenendo riferimenti a tutto. Il vantaggio vive in clear(), non in iterparse da solo. Il baseline del caricamento completo, molto più alto della modalità vincolata, conferma anche che il misuratore RSS vede davvero il divario di magnitudine e non sta andando alla cieca. (Questo test di memoria l’ho eseguito in questo pacchetto — è una misurazione di footprint, distinta dai numeri di timing presi in prestito.)

La versione reale del problema è semplice: un export XML multi-gigabyte che non entra in RAM non ha alcun percorso con selectolax. Serve il parser streaming di lxml oppure un altro linguaggio.

Namespace: RSS, SVG e la trappola del namespace predefinito

Dodici casi di namespace, che coprono RSS attraverso tre namespace, SVG con un namespace predefinito più xlink, e XML con namespace predefinito. Tutti e dodici sono passati.

lxml estrae //dc:creator/text() da un feed RSS come esattamente ["Alice", "Bob"], risolve //atom:link/@href e //content:encoded attraverso tre namespace separati nello stesso documento, gestisce //s:rect e //s:use/@xlink:href nel secondo namespace di SVG, divide i nomi in notazione Clark {uri}local con QName e permette l’ispezione tramite nsmap. Questo è il comportamento documentato e mantenuto, ed è un’intera dimensione che selectolax non tocca, perché selectolax è solo HTML5 e non processa namespace XML arbitrari.

C’è però una trappola documentata che vale la pena memorizzare. XPath non ha il concetto di namespace predefinito. Se punti //book a un documento che dichiara xmlns="urn:...", ottieni zero hit: il prefisso vuoto non esiste per XPath, come spiegano chiaramente le lxml docs. Devi associare un prefisso artificiale (//c:book con namespaces={"c": "urn:..."}, che ha trovato tutti e tre) oppure ricorrere a //*[local-name()='book'] (anche qui tre risultati). Non è un bug: è lo standard XPath, implementato fedelmente. Semplicemente sorprende tutti una volta sola.

Pagine sporche reali: fedeltà su 11 scrape veri

I test sintetici sono puliti; il web no. Ho riutilizzato undici pagine reali catturate dal set di fixture del pacchetto selectolax (aggiornate al 2026-07-10, sola lettura) e le ho passate a lxml.html prendendo lxml come soggetto.

FixtureDimensioneLinkerrori recuperati da libxml2XML rigido
news_bbc.html398 KB2554ok
docs_mdn_array.html243 KB5080solleva
wiki_scraping.html227 KB4600solleva
gov_whitehouse.html289 KB1540solleva
oldstyle_craigslist.html561 KB3510solleva
forum_reddit.html129 KB3180solleva
docs_python.html80 KB3412solleva
ecommerce_books.html51 KB940solleva
news_hackernews.html35 KB2290solleva
ecommerce_webscraper_allinone.html16 KB350solleva
spa_quotes_js.html6 KB50solleva

Tutti e undici sono stati parsati con lxml.html, e i conteggi di link, heading e immagini hanno coinciso con i conteggi lxml riutilizzati dal pacchetto selectolax in tutti e undici i casi — cross-check true. È questa corrispondenza che mi dice che il riuso è davvero omogeneo e non due misure diverse mascherate con la stessa etichetta.

Il dato laterale: il parser XML rigido ha sollevato su dieci degli undici page. Le pagine web reali non sono quasi mai XML ben formato, ed è esattamente per questo che esiste la modalità di recovery di libxml2 per HTML. L’unica eccezione era BBC News, resa con Next.js e abbastanza ben formata da sopravvivere al parsing XML rigido. Non tutto ciò che è etichettato “HTML” richiede la via di recovery.

Una nota sui conteggi su cui è facile inciampare. Su docs_python.html, //a[@href] (esistenza dell’attributo) contava 343, mentre il pacchetto selectolax con if n.get("href") (valore truthy) contava 341. I due extra sono link con href="" vuoto. È una differenza di convenzione di conteggio — attributo esistente contro attributo non vuoto — non una differenza di comportamento di lxml, e i numeri tornano appena allinei il predicato. Utile da sapere quando fai scraping: se gli href vuoti contano o no lo decide il tuo filtro, non il parser.

Il limite di profondità che sembra un bug (ma non lo è)

Il pacchetto selectolax aveva già registrato che lxml perdeva il contenuto più profondo in markup <div> annidati a 1.000 e 5.000 livelli, descrivendolo come “lxml perde in silenzio il contenuto più profondo”. Volevo capirne il meccanismo, quindi ho eseguito il parser di default contro huge_tree=True.

lxml default depth guard around 253 levels and huge_tree to 2045

Profondità richiestaProfondità raggiunta dal parser di defaultProfondità raggiunta con huge_tree=True
300253 (taglia il resto)299 (recuperata)
1000253 (taglia il resto)999 (recuperata)
5000253 (taglia il resto)2045 (taglia ancora)

Il parser di default tronca intorno a 253 livelli e scarta in silenzio tutto ciò che va oltre. Non è un bug: è una difesa DoS di libxml2, un limite di nidificazione di circa 256 livelli che impedisce a un documento ostile di far saltare lo stack, ed è documentato nel thread Launchpad su XML_PARSE_HUGE. Impostando huge_tree=True, le profondità 300 e 1.000 vengono recuperate completamente. A 5.000 livelli, però, anche con huge_tree il recupero si ferma a 2.045: esiste un secondo limite di ricorsione più duro in libxml2, sopra quello configurabile, e huge_tree non lo elimina.

Quindi l’azione concreta è questa: quando analizzi markup molto profondo proveniente da una fonte fidata, usa lxml.html.HTMLParser(huge_tree=True). Ciò che questo pacchetto aggiunge rispetto all’osservazione riutilizzata è il meccanismo (un limite di sicurezza, non corruzione dei dati), la correzione (huge_tree) e il fatto che esiste un secondo tetto che la correzione non raggiunge.

DOM in lettura/scrittura, serializzazione, encoding

lxml è un albero completo in lettura e scrittura, non un estrattore read-only, e ho verificato l’area di editing caso per caso. Tutte e otto le operazioni DOM sono passate: SubElement, insert, remove, replace, strip_tags (rimuove i tag ma mantiene il testo), strip_elements (rimuove tag e testo), drop_tree (esclusiva di lxml.html) e il modello a due slot text/tail che confonde chi inizia — in <p>head<b>bold</b>tail</p>, p.text è "head", b.text è "bold" e b.tail è "tail".

La serializzazione ha passato cinque su cinque: tostring in modalità XML e HTML (HTML lascia correttamente gli elementi void non auto-chiusi), pretty_print, canonicalizzazione C14N (method="c14n", un’altra esclusiva di lxml) e un round-trip pulito.

L’encoding è il punto in cui lxml si distingue in silenzio. Se gli passi byte non UTF-8 — ad esempio "<p>café éè</p>".encode("latin-1") attraverso lxml.html.fromstring — recupera intatti café éè, senza caratteri di sostituzione U+FFFD e senza byte persi. Questo riproduce direttamente il suo ruolo di “reference pulita” nel pacchetto selectolax, dove lo stesso input si corrompeva in modo silenzioso negli altri due motori (Lexbor produceva caratteri di replacement, Modest perdeva byte del tutto). Il rilevamento charset basato su libxml2 è semplicemente più stabile qui.

Il rovescio della medaglia è la rigidità su come dichiari un encoding. encoding="latin-1" nella dichiarazione XML solleva XMLSyntaxError: Unsupported encoding: latin-1, mentre il nome canonico IANA encoding="ISO-8859-1" si parse senza problemi e restituisce café. libxml2 accetta solo nomi di encoding canonici, non alias — dettaglio documentato già in launchpad #613302. Fastidioso se non lo sai, banale una volta che lo sai.

Infine, il ciclo di vita dei nodi. Ho eseguito tre scenari di handle obsoleti in processi separati (un crash duro si sarebbe visto come uscita non zero): mantenere un nodo dopo che il suo tree è stato garbage-collected, leggere un handle dopo drop_tree(), e usare un nodo dopo remove(). Nessun segfault in nessuno dei casi — lxml mantiene attivo il riferimento del nodo al suo tree per prevenire use-after-free. Stesso esito pulito ottenuto da selectolax in questo test.

Velocità e memoria (prese in prestito, ma dichiarate con onestà)

Tutto ciò che segue è riutilizzato dal pacchetto selectolax, aggiornato al 2026-07-13. Questo pacchetto non ha prodotto alcun numero di timing proprio, e preferisco dirlo due volte piuttosto che lasciarti pensare che abbia ritimato qualcosa.

DimensioneValore lxmlLettura
Pure parse p50 (10 MB)77,9 mscirca 33-34% più veloce di selectolax-Lexbor
Full parse + extract p50 (1 MB / 10 MB)14,18 ms / 172,9 mscirca alla pari con Lexbor sulle piccole dimensioni
Throughput CSS su 100k nodi3.002.646 nodi/sfascia più veloce dei tre engine C
Delta RSS su 10 MB128,9 MBil più leggero dei sei parser, ~1,7x più parsimonioso di BeautifulSoup
Cold start import14,1 ms~2,3x più veloce degli import in stile parsel

I numeri di pure-parse e throughput sono solidi, e lxml è il parser con l’uso di memoria più contenuto tra i sei misurati. Il quadro del threading, però, richiede una precisazione. I dati riutilizzati mostrano un speedup wall-clock a 4 thread di appena 1,21x, segnato come inconcludente — ma quello è il percorso con parser condiviso di default. La FAQ di lxml è esplicita: il GIL viene rilasciato durante il parsing solo quando ogni thread usa il proprio parser (o una copia del default); un parser condiviso serializza l’accesso. Ho verificato strutturalmente l’API necessaria per fare le cose nel modo giusto (XMLParser.copy() esiste, esistono get/set_default_parser, XPathEvaluator porta un lock interno), ma non ho misurato il guadagno con parser per-thread — sarebbe una nuova misura temporale, e questo pacchetto non produce quel tipo di dato. Quindi leggi "1,21x" come "nel percorso ingenuo con parser condiviso", non come limite di threading di lxml.

E una nota importante su tutto questo: si tratta di numeri di una sola piattaforma, macOS arm64. L’affermazione che il pure parse di lxml batta Lexbor va contro il consenso abituale secondo cui il parser basato su Lexbor è il più veloce, quindi richiede davvero una verifica su Linux x86_64 prima che qualcuno lo consideri definitivo.

Licenza: la vittoria noiosa

lxml è distribuito sotto BSD-3-Clause, e le librerie C che incorpora — libxml2 e libxslt — sono entrambe MIT. È una catena completamente permissiva, senza copyleft in nessun punto, cosa che conta appena lo redistribuisci. Per confronto, la wheel di selectolax incorpora Modest con licenza LGPL-2.1 e Lexbor con licenza Apache-2.0, quindi lxml racconta una storia più semplice se devi distribuirlo in un prodotto chiuso.

C’è anche un vantaggio pratico nell’installazione: lxml pubblica wheel precompilate che linkano staticamente libxml2 e libxslt, quindi pip install lxml di solito non richiede una libxml2 di sistema né un compilatore sulla macchina — un’esperienza diversa rispetto alla compilazione da sorgente.

Dove si colloca lxml — e dove subentra uno strato di estrazione AI

È il momento di essere chiari sul confine, perché qui è facile fare confusione di categoria. lxml è una libreria di parsing. Ti restituisce un albero e un motore di query eccellente, ma tutto il resto resta a te: recuperare la pagina, renderizzare JavaScript, superare difese anti-bot, scrivere e mantenere gli XPath e strutturare il risultato. Questo è un livello diverso rispetto a un servizio di estrazione gestito, e i due non sono tanto rivali quanto vicini di casa.

Per uno sviluppatore che preferisce non gestire da solo lo stack fetch-render-select-maintain, quel livello superiore è il posto in cui vive qualcosa come Thunderbit — e, per questo pubblico, parliamo di API, server MCP e CLI, non dell’estensione browser. La Thunderbit Open API espone POST /distill per trasformare una pagina in Markdown pulito e POST /extract per estrarre dati strutturati in base a uno schema JSON, con un selettore renderMode e job batch per grandi volumi. Lo stesso motore è disponibile come server MCP (thunderbit_suggest_fields, thunderbit_distill, thunderbit_extract) per agenti e coding assistant, e come CLI da eseguire direttamente dal terminale con npx @thunderbit/thunderbit-cli. Gestisce rendering JS, anti-bot e CAPTCHA out of the box e restituisce JSON allineato allo schema — quindi è uno strato sopra il parsing, non un suo sostituto.

Prova Thunderbit per l’estrazione di dati web

Il quadro è semplice. Usa lxml quando controlli tu la pipeline e vuoi un controllo XPath chirurgico su un albero che conosci. Usa una API di estrazione AI quando preferisci non mantenere selettori e rendering. Molti sistemi reali usano entrambi — lxml per i feed strutturati che controllano, un servizio di estrazione per le pagine sporche e long-tail che non controllano.

Cosa questa recensione non ha testato

Questa è una recensione provvisoria, non una scheda finale, quindi ecco ciò che non copre.

Tutti i numeri di timing e memoria sono riutilizzati, su una sola piattaforma (macOS arm64, Python 3.14), e si portano dietro le avvertenze di quel pacchetto: il risultato "lxml è più veloce nel pure parsing" va contro il consenso comune e richiede una verifica su Linux x86_64. Lo speedup del threading con parser per-thread non è stato testato (richiederebbe nuovi timing). Ho misurato il memory footprint di iterparse su 300k record ma non XML reali da gigabyte, non iterparse su HTML rispetto a XML e non un test di tenuta di più ore. L’XSLT 1.0 di lxml, la validazione RelaxNG / XMLSchema / DTD e le estensioni EXSLT non sono state testate qui — una superficie di capacità molto ampia, ma al di fuori del nucleo parsing+selection. Ho osservato il secondo limite di profondità a 2.045, ma non ho determinato la costante esatta di ricorsione di libxml2. Ho testato solo la release stabile 6.1.1, non l’alpha 7.0.0. Windows, build da sorgente e la build free-threaded 3.14t non sono stati testati. E, all’interno di XPath, ho coperto le funzioni built-in ma non le variabili XPath, le funzioni Python personalizzate o il riuso di oggetti etree.XPath precompilati.

Il verdetto

lxml non è la novità veloce del momento, e proprio questo è il suo punto forte. È un binding libxml2 vecchio di due decenni con un motore XPath 1.0 completo che nessuna alternativa mainstream in Python eguaglia, tre modalità prevedibili di rigidità nel parsing con un error log in mezzo, un vero parser streaming per documenti che non entrano in memoria, una gestione corretta di namespace multipli ed encoding, e una licenza completamente permissiva. Le poche asperità — il limite di profondità intorno a 253 livelli e il numero del threading con parser condiviso — sono documentate, configurabili e ora spiegate.

Se controlli tu la pipeline di scraping e fai largo uso di XPath, lxml resta il parser a cui andare. Se invece preferisci non occuparti di selettori e rendering, è qui che entra uno strato di estrazione AI come la Thunderbit API, MCP e CLI — una chiara divisione dei compiti, non una competizione. In ogni caso, considera questi numeri provvisori e ricontrolla il timing sulla tua piattaforma prima di citarli in un documento di design.

Prova Thunderbit per l’estrazione di dati web Get Started Free

FAQ

lxml è uno web scraper? No. lxml è un parser e serializzatore — un binding Python per libxml2/libxslt che trasforma il markup in un albero modificabile e interrogabile. Non recupera pagine, non renderizza JavaScript e non gestisce difese anti-bot; la parte di richiesta la fornisci tu (con requests, httpx, un browser headless o un servizio di scraping) e poi passi i byte a lxml.

Quando dovrei usare lxml invece di BeautifulSoup o selectolax? Scegli lxml quando ti serve XPath. BeautifulSoup può usare lxml come parser backend, ma non espone XPath nativo; selectolax è solo CSS e, nel suo ambito ristretto, è più veloce. Se la tua logica di selezione deve filtrare per contenuto testuale, navigare verso genitori o antenati, estrarre attributi o nodi di testo, oppure usare predicati di conteggio, il motore XPath di lxml è l’unica opzione mainstream in Python che li esprime direttamente.

Perché lxml scarta in silenzio contenuti molto annidati? Il parser di default limita la nidificazione a circa 253 livelli — una difesa DoS di libxml2 contro documenti ostili, non un bug. Imposta huge_tree=True (per esempio lxml.html.HTMLParser(huge_tree=True)) e recupera completamente profondità di 300 e 1.000. Tieni presente però un secondo limite di ricorsione, più rigido, intorno a 2.045 livelli che huge_tree non rimuove.

lxml rilascia il GIL nel parsing multithread? Solo alle condizioni giuste. La FAQ di lxml dice che il GIL viene rilasciato durante il parsing quando ogni thread usa il proprio parser o una copia del parser predefinito; un parser condiviso invece serializza l’accesso. Lo speedup riutilizzato di 1,21x su 4 thread riflette il percorso ingenuo con parser condiviso, non il limite con parser per-thread, che qui non è stato misurato.

lxml è ancora mantenuto nel 2026? Sì. La release stabile 6.1.1 è stata pubblicata il 2026-05-18, il repository ha ricevuto l’ultimo push il 2026-07-02 ed è in sviluppo un’alpha 7.0.0. Con circa 3.000 star GitHub e una base libxml2 attivamente mantenuta, resta una libreria attuale e ben supportata, non un pezzo di archeologia.

Ke
Ke
CTO di Thunderbit | Senior Data Scientist ed esperto di ML Con quasi un decennio di esperienza nel machine learning e nella data science, Ke Shen è un ex studente della Columbia University ed ex Senior Data Scientist presso Walmart Labs. Grazie a una profonda competenza, riconosciuta dai suoi pari, in Python, R, Java e statistica, condivide insight collaudati sul passaggio di algoritmi AI complessi dalla teoria a un'architettura pronta per la produzione.
Topics
Strumenti di Web ScrapingAI Web Scraper
Indice
Thunderbit · Agente AI per i dati web

Estrai dati da qualsiasi pagina in 1 clic

Scelto da oltre 250.000 utenti
piano gratuito disponibile
Dalla pagina web al foglio di calcolo
Descrivi ciò che ti serve — l’AI Agent di Thunderbit lo estrae ed esporta in Excel, Google Sheets, Airtable o Notion. Puoi iniziare gratis.
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week