Ogni articolo sul "parser HTML Python più veloce" finisce prima o poi per citare selectolax, e quasi tutti si fermano a "molto più veloce di BeautifulSoup". Questa parte è vera. Quello che quasi nessuno completa è cosa succede quando metti selectolax a confronto con lxml — perché lì il concetto di "più veloce" va preso con un asterisco.
Così l'ho benchmarkato sul serio: selectolax (entrambi i backend), lxml, BeautifulSoup con html.parser e con lxml, e parsel, su cinque dimensioni di pagina da 1 KB a 10 MB, prendendo per ogni misura la mediana di tre esecuzioni separate in processi distinti. selectolax ha battuto di netto BeautifulSoup e ha praticamente pareggiato lxml nel confronto completo — ma poi ha perso la prova del puro parsing contro lxml. Tutti i numeri qui sotto sono provvisori e ottenuti su una sola macchina (macOS arm64, Python 3.14.2); gli script sono nel repository, quindi eseguili sul tuo ambiente prima di citarmi.
Cos'è davvero selectolax (e cosa non è)
selectolax è un binding Python verso due motori C — Modest e Lexbor — che analizzano HTML5 e lo interrogano con selettori CSS. Non è un crawler, non è un browser, non è uno "scraper" nel senso di premere un bottone e via. È quello che usi dopo aver già scaricato l'HTML. La descrizione dell'autore è: "A fast HTML5 parser with CSS selectors, written in Cython, using Modest and Lexbor engines."
Ci sono due backend, e la differenza conta molto più di quanto facciano pensare i documenti:
LexborHTMLParser(motore Lexbor) — quello che il README consiglia di usare dal 2024 in poi.HTMLParser(motore Modest) — l'originale, il cui C sottostante "non è più mantenuto", sempre secondo il README.
Prima di parlare di velocità, vale la pena fissare alcuni dati. Al snapshot del repository del 2026-07-10, selectolax ha 1.653 star e l'ultima release è la v0.4.10 (maggio 2026); PyPI lo dichiara compatibile con Python >=3.9,<3.15. L'installazione è la parte meno rognosa di tutta la recensione: pip install selectolax ha scaricato un wheel precompilato cp314 da 2,3 MB e ha funzionato subito su Python 3.14 — niente download del browser, nessun passaggio doctor, nessuna compilazione. È il vantaggio silenzioso di un parser puro rispetto a uno basato su browser: lo importi e parte.
C'è però una nota importante sul piano della licenza: il binding Python è MIT, ma il wheel include i motori compilati, che hanno licenze proprie — Modest è LGPL-2.1, Lexbor è Apache-2.0. Quindi dire "selectolax è MIT" è vero per il codice Python, ma incompleto per il binario che distribuisci davvero. Se il tuo team legale segue i componenti redistribuiti, questo è il dettaglio da segnalare.
La domanda sulla velocità, con numeri veri
Il compito che ho misurato era questo: analizzare la stringa HTML, estrarre tutto il testo di <h3 class="title">, estrarre ogni href di <a>. Latenza mediana in millisecondi, calcolata come mediana tra tre esecuzioni separate in processi diversi; la variabilità tra i run è rimasta sotto circa il 5% per i parser basati su C nella maggior parte delle dimensioni. Prima di cronometrare qualunque cella, l'output di ogni parser è stato ridotto a un content hash, così un parser che facesse silenziosamente meno lavoro sarebbe stato individuato ed escluso — su queste pagine tutti e sei hanno coinciso in ogni dimensione, quindi il confronto è davvero a parità di condizioni. I dati completi sono nel file bench_parse.json nel repository.

| Pagina | selectolax (Lexbor) | selectolax (Modest) | lxml | parsel | BS (lxml) | BS (html.parser) |
|---|---|---|---|---|---|---|
| 1 KB | 0.027 | 0.029 | 0.036 | 0.044 | 0.281 | 0.323 |
| 10 KB | 0.160 | 0.171 | 0.166 | 0.228 | 1.705 | 2.049 |
| 100 KB | 1.464 | 1.565 | 1.423 | 2.020 | 16.5 | 20.6 |
| 1 MB | 14.901 | 16.9 | 14.177 | 20.9 | 181.9 | 232.6 |
| 10 MB | 159.9 | 247.9 | 172.9 | 231.9 | 2261.6 | 2788.7 |
Contro BeautifulSoup: circa 12-17x, e la narrativa comune la sottostima
Tradotti in rapporti, i numeri dicono che selectolax-Lexbor è circa 12 volte più veloce di BeautifulSoup(html.parser) su una pagina da 1 KB, fino a circa 17 volte su una da 10 MB, e circa 10-14 volte più veloce di BeautifulSoup(lxml) nello stesso intervallo. Il dato che gira online — "selectolax è circa 4-5 volte più veloce di BeautifulSoup" — è troppo basso se confrontato con html.parser e diventa più o meno giusto solo se parli di BeautifulSoup basato su lxml. Il moltiplicatore reale dipende da quale BeautifulSoup intendi e da quanta estrazione fai per pagina.
Questo è coerente anche con il benchmark del README, che lascia intendere un vantaggio di 25,5x rispetto a BeautifulSoup(html.parser). Nessuno dei due dati è sbagliato. Il task del README (titoli, link, script e meta su homepage piccole) fa meno estrazione sulle pagine piccole, e quindi pesa di più l'overhead di parsing di BeautifulSoup. Restringendo il campo, la conclusione è questa: selectolax è circa 10-15 volte più veloce di BeautifulSoup in un flusso realistico di parsing ed estrazione, con un vantaggio più alto sulle pagine minuscole e con estrazioni leggere.
Se oggi il tuo collo di bottiglia è una montagna di codice BeautifulSoup che macina pagine, questa è una migrazione che si ripaga da sola. Questo caso non fa discutere. Il prossimo, invece, sì.
Contro lxml: un pareggio — e lxml vince la parte che tutti dimenticano di isolare
Guarda le righe da 100 KB e 1 MB. Lexbor e lxml stanno entro circa il 5% l'uno dall'altro, i loro intervalli per singola esecuzione si sovrappongono, e secondo la mia metodologia questo è un pareggio: nessun vincitore, nessun "più veloce". L'unico punto in cui selectolax prende davvero il vantaggio è sulla pagina da 10 MB (159,9 ms contro 172,9 ms, un gap dell'8,1% con intervalli non sovrapposti). Quindi sul task completo selectolax eguaglia lxml e lo batte solo sui documenti più grandi.

Poi ho separato la costruzione dell'albero dalla query CSS, e il risultato si ribalta in un modo che molti articoli non colgono. Per il puro parsing, senza alcuna query, lxml è risultato costantemente circa il 33-34% più veloce di selectolax-Lexbor su questa macchina — 77,9 ms contro 116,6 ms sulla pagina da 10 MB. Sul task completo, però, i due si avvicinano comunque, e la mia ipotesi di lavoro (non dimostrata con un esperimento di attribuzione) è che su queste pagine la query CSS sia una parte piccola del tempo totale, quindi il vantaggio di lxml nella fase di parsing si diluisce fino a far coincidere i totali.
Questa è l'affermazione più attaccabile dell'intera recensione, e voglio essere trasparente sul perché. Va contro la saggezza comune, e l'unico benchmark pubblicato che ho trovato e che isola il solo parsing — aows.jpt.sh — riporta l'opposto, con selectolax circa 4x più veloce. Per questo ho messo dei paletti: il risultato è su una sola piattaforma (macOS arm64, Python 3.14, wheel cp314 precompilati — Linux x86_64 o build da sorgente non testati), è stato verificato su quattro dimensioni di pagina ed è rimasto stabile in tutte, ed è stato ricontrollato con due API diverse di lxml per escludere un artefatto dell'API. Entrambe le API di lxml hanno battuto selectolax-Lexbor in ogni dimensione. Non sto presentando "lxml fa parsing più velocemente" come verità definitiva — lo sto presentando come il risultato del mio bench, con lo script allegato, in contrasto con gran parte dei numeri pubblicati. Provalo sul tuo hardware.
Un'ultima sfaccettatura: interrogando 100.000 <a> su una pagina piatta, lxml e selectolax-Modest sono praticamente pari (33,30 ms contro 34,19 ms, intervalli sovrapposti), mentre selectolax-Lexbor è dietro di circa il 15%. Ciò che i tre motori C hanno in comune è che sono da 5 a 7 volte più veloci di parsel o BeautifulSoup nelle selezioni massive, dove il vero freno è il modello Python-object-per-node. Quindi nemmeno "selectolax è il più veloce nella selezione CSS massiva" regge: Modest pareggia appena lxml, e Lexbor perde.
La conclusione che mi sento di difendere davvero è questa: il vantaggio di selectolax su lxml non è una superiorità ampia sul task completo. Vince solo sulla pagina più grande. Il suo valore sta altrove — ergonomia dell'API, comportamento con input sporchi e CSS moderno — ed è lì che passa il resto della recensione.
Memoria e cold start: guarda l'RSS, non il profiler
La memoria è il punto in cui devo correggere i miei numeri precedenti, e la correzione è il punto centrale. Misurata come variazione di RSS sulla pagina da 10 MB con tracemalloc disattivato, BeautifulSoup consuma circa 1,5-1,8 volte la memoria di selectolax o lxml — l'intervallo va da 1,51x (BS-lxml a 218,4 MB contro Lexbor a 144,6 MB) fino a 1,75x nel valore più alto. selectolax e lxml stanno nella fascia leggera; lxml è il più parsimonioso in termini di RSS.

In una passata precedente avevo scritto "~3x", e quel numero era sbagliato per un motivo istruttivo: era stato misurato con tracemalloc attivo, e il bookkeeping per allocazione di tracemalloc raddoppia circa l'RSS apparente del parser che alloca di più. Quindi un consiglio per chi misura la memoria dei parser: classificali per RSS con il profiler spento. Ordinare i parser in base al picco di tracemalloc altera soprattutto quelli basati su C — faceva sembrare selectolax-Lexbor più pesante di Modest quando, in termini di RSS reale, sono vicini. BeautifulSoup è davvero il più pesante qui; semplicemente non lo è nella misura di 3x che mostrava uno strumento contaminato.
Il cold start è un dettaglio minore ma reale: selectolax si importa in circa 14 ms, all'incirca alla pari con lxml e circa 2,3x più velocemente di bs4 o parsel. Se stai distribuendo un tool CLI o una funzione serverless in cui il tempo di importazione pesa a ogni invocazione, questo divario merita attenzione.
Copertura dei selettori CSS: solida, con alcune vere lacune
La copertura CSS è stata testata con una matrice di 41 casi, ciascun selettore verificato contro un fixture con un insieme di risposte corrette noto, più una sessione di fault-finding costruita apposta per mandare in crisi il motore Lexbor. Ogni caso è stato eseguito in un processo separato, cosa che si è rivelata necessaria — uno di essi fa crashare l'interprete intero. I risultati:

| Motore | PASS | ERRATO | NON SUPPORTATO | PROCESS_ABORT |
|---|---|---|---|---|
| soupsieve | 41 | 0 | 0 | 0 |
| selectolax Lexbor | 39 | 0 | 2 | 0 |
| lxml (cssselect) | 37 | 1 | 3 | 0 |
| parsel (cssselect) | 37 | 1 | 3 | 0 |
| selectolax Modest | 35 | 3 | 2 | 1 |
Quando entrano in gioco i selettori ostili, Lexbor non è il vincitore assoluto — lo è soupsieve, con un pulito 41/41 contro il 39/41 di Lexbor. I due casi mancanti di Lexbor sono :lang(en) e :dir(rtl), che rifiuta con un errore di parsing. È perfetto sul resto, inclusi :has(), :is(), :where() e gli attributi case-insensitive.
Dove Lexbor brilla davvero è nel confronto con lo stack cssselect. Il selettore di punta del README — div > :nth-child(2n+1):not(:has(a)) — restituisce l'insieme corretto su entrambi i backend di selectolax e su soupsieve, ma l'insieme sbagliato su lxml e parsel, senza alcun errore. Uno scraper che copia quel selettore in Scrapy o parsel ottiene risultati sbagliati senza alcun avviso. Per essere precisi nel framing: cssselect supporta :has() dalla versione 1.2.0 (2022), e io ho testato la 1.4.0, quindi qui non si parla di "non supportato", ma di "supportato ma valutato male nel composto". Il comportamento silenziosamente errato su questo composto non compare nel tracker di cssselect, che registra i limiti di :has() come errori sollevati. Lexbor gestisce anche il flag case-insensitive degli attributi [data-role="LEAD" i], che cssselect rifiuta in modo netto.
Ci sono però due lacune che contano davvero per chi migra. selectolax non supporta alcun XPath — nessun backend espone xpath() — e non supporta neppure i pseudo-elementi ::text / ::attr(), perché si tratta di un'estensione di parsel/Scrapy e non di vero CSS. Se i tuoi scraper attuali dipendono da XPath, questo è il muro più grande che incontrerai; dovrai riscrivere i selettori, non sostituire solo una libreria. Dall'altro lato, Lexbor offre una pseudo-classe :lexbor-contains("text" i) per il matching testuale case-insensitive, che né lxml, né parsel, né il CSS standard forniscono, e funziona come documentato.
Robustezza su HTML brutto, ed è qui che selectolax guadagna punti
Lo scraping reale significa dare in pasto al parser input sporco e sperare che non crolli. Ho testato 18 input avversari, e questa è la categoria in cui il confronto di selectolax con lxml è più favorevole.
Se passi a lxml.html.fromstring una stringa vuota o contenente solo spazi, solleva ParserError("Document is empty"). Entrambi i backend di selectolax restituiscono invece un albero valido ma vuoto. Per uno scraper che scorre una lista di URL e riceve a volte risposte vuote, è un try/except in meno da mettere attorno a tutto. selectolax ha anche gestito senza problemi 100.000 elementi senza andare in stack overflow.
L'annidamento profondo è stato il caso più netto. A 1.000 e 5.000 livelli di <div> annidati, lxml scarta in silenzio il contenuto più profondo mentre selectolax lo conserva. libxml2 limita la profondità di parsing intorno a 256 livelli e tronca l'albero senza errori, quindi il testo più profondo diventa semplicemente irraggiungibile. Entrambi i backend di selectolax restituiscono l'albero completo. È il contrario della trappola <template> che vedrai tra poco: lì Lexbor perde contenuto che gli altri mantengono; qui è lxml a perdere contenuto che selectolax conserva.
Non tutte le celle sono state un successo. Il backend Modest manda in crash l'intero interprete Python con un SIGABRT quando incontra :dir() — non una eccezione intercettabile, ma una chiusura brutale del processo. È una vera criticità di robustezza per chi è ancora sul backend legacy, ed è esattamente il tipo di problema che resta invisibile finché non fa cadere un job di produzione alle 3 del mattino.
Due trappole di perdita dati silenziosa da conoscere prima del rilascio
Nessuna delle due è una scoperta nuova — entrambe sono documentate upstream — ma entrambe fanno perdere dati reali, senza rumore, e nessuna delle due è evidenziata con forza nel README.
Lexbor scarta i link <a> dentro <template>
Sulla pagina MDN live che ho testato, selectolax-Lexbor ha trovato 497 link, mentre lxml, entrambi i backend di BeautifulSoup e perfino il backend Modest di selectolax ne hanno trovati 508. Gli undici mancanti erano un selettore di lingua e un link alle discussioni dentro elementi <template> (la pagina usa web component Lit).

La causa è legittima: secondo la specifica HTML5, il contenuto di <template> viene analizzato in un frammento separato e inerente, non nel DOM normale, e Lexbor segue questa regola in modo rigoroso — tree.css("a") non entra nel contenuto dei template. lxml, entrambi i backend di BeautifulSoup e Modest appiattiscono il contenuto del template nell'albero principale, e quindi trovano quei link. Si tratta di un problema aperto documentato (selectolax#146, con la causa a livello motore in lexbor#170), e entrambe le interpretazioni hanno una loro difendibilità — Lexbor è probabilmente più corretto rispetto alla specifica. Ma per uno sviluppatore sul backend raccomandato, quei dati spariscono in silenzio, senza alcun errore. Vale anche il contrario: gli altri parser espongono contenuto template inerte che un browser non renderizzerà mai, quindi possono restituirti dati fantasma che l'utente non vede. La via di uscita più affidabile, per quella pagina specifica, è il backend Modest o un'altra libreria.
Byte non UTF-8 corrompono .text() senza avvisare
Se passi a selectolax byte non validi in UTF-8, il parsing va a buon fine — la corruzione emerge più tardi, e il problema è peggiore di un crash pulito. Su "<p>café éè</p>".encode("latin-1"), .text() di Lexbor restituisce caratteri di sostituzione, .text() di Modest scarta in silenzio i byte problematici, e entrambi i motori sollevano UnicodeDecodeError solo quando accedi a .html. Il binding decodifica in UTF-8 rigoroso solo al momento della lettura, non durante il parsing. Questo è collegato a un problema noto di selectolax sulla rigidità di encode/decode.
La correzione è una sola riga, e dovrebbe entrare nella memoria muscolare: decodifica tu i byte prima — LexborHTMLParser(resp.content.decode("latin-1")) — e entrambi i motori restituiranno correttamente 'café éè'. In pratica, passa sempre a selectolax una str, mai bytes grezzi non UTF-8. Il README non lo dice chiaramente.
Aspetti di produzione (una sola osservazione, quindi prendili come indicazioni)
I risultati che seguono li ho misurati una sola volta, non su tre run, quindi li segnalo come segnali e non come numeri definitivi.
Lo scaling con i thread è il dato più interessante. Analizzando una pagina da 1 MB 48 volte su quattro thread, selectolax ha mostrato un'accelerazione wall-clock di circa 3,5-3,9x — la firma empirica di una libreria che rilascia il GIL durante il parsing C — mentre BeautifulSoup(lxml) è diventato più lento di diverse volte in threading, la firma di un lavoro che si serializza sul GIL. lxml si è collocato nel mezzo, senza una conclusione netta. Per l'era del free-threading verso cui Python si sta muovendo, il fatto che selectolax parallelizzi il parsing tra thread mentre BeautifulSoup no è un vantaggio reale, anche se provvisorio. Si tratta però di un solo numero di thread su una sola dimensione di pagina, e il meccanismo è un'ipotesi, non qualcosa che ho confermato strumentando il C.
Per quanto riguarda le perdite di memoria: dopo 2.000 iterazioni parse-extract-drop su pagine da 1 MB, nessuno dei tre parser mostrava la crescita lineare di RSS tipica di una leak — ciascuno si è stabilizzato in una banda di lavoro limitata. Mi fido di quel risultato perché ho fatto passare sullo stesso strumento un soggetto noto per perdere memoria, che è salito fino a +198 MB come previsto: quindi lo strumento era in grado di vedere una leak, semplicemente nei parser non ne ha trovata una. Inoltre, un handle di nodo mantenuto vivo dopo che il suo albero proprietario era uscito di scope è rimasto utilizzabile, senza segfault. Anche qui: una sola osservazione, nessun test di soak di molte ore.
Dove si colloca selectolax — e dove passa il testimone
Tutto quello che hai letto riguarda un'unica cosa: trasformare HTML già disponibile in dati strutturati, in fretta. selectolax è molto bravo in questo. Quello che non fa, di proposito, è scaricare la pagina, renderizzare JavaScript, ruotare proxy, risolvere CAPTCHA o capire quali elementi ti servono. Tutto questo resta codice tuo. selectolax è il livello di parsing, e non pretende di essere altro.
È qui che un servizio di estrazione gestito si colloca sopra un parser invece di sostituirlo. Se preferisci non costruire e mantenere da solo lo stack fetch-render-anti-bot-extract, Thunderbit lo espone come API, server MCP e CLI — POST /distill trasforma una pagina in Markdown pulito e POST /extract restituisce JSON strutturato conforme allo schema, con rendering JS e anti-bot gestiti per te. È un livello diverso del problema: userai selectolax quando hai già l'HTML e vuoi velocità di parsing grezza sotto il tuo controllo, e qualcosa come API, server MCP o CLI di Thunderbit quando vuoi che siano fetch ed estrazione a essere gestiti e ti serva solo il dato strutturato in uscita. Non uno swap: un'altitudine diversa nello stesso stack.
Prova Thunderbit per l'estrazione di dati web
Pro, contro e chi dovrebbe usarlo davvero
Dove selectolax vince:
- Da 12 a 17 volte più veloce di BeautifulSoup su un flusso realistico di parsing ed estrazione, stabile su tre ordini di grandezza di dimensione pagina.
- Memoria contenuta (fascia lxml, circa 1,5-1,8 volte più leggero di BeautifulSoup) e import di circa 14 ms.
- Si comporta bene sugli input che mandano in crisi lxml — vuoti, fatti solo di spazi, o con nesting patologicamente profondo.
- CSS moderno, incluso
:has(),:is(),:where(), attributi case-insensitive e il:lexbor-contains()esclusivo di Lexbor. - DOM leggibile e modificabile in sicurezza rispetto a
None: gli elementi mancanti restituisconoNoneo[]invece di sollevare eccezioni, e puoi anche modificare e serializzare di nuovo l'albero. - Manutenzione attiva (v0.4.10, metà 2026) e installazione banale.
Dove non vince:
- Non è in generale più veloce di lxml — pareggio sul task completo e perdita nella sola fase di parsing sul mio benchmark.
- Niente XPath e niente
::text/::attr()— un ostacolo duro per gli scraper basati su XPath. - Due trappole di perdita dati silenziosa: contenuto
<template>su Lexbor e byte non UTF-8 via.text(). - Il backend Modest è legacy e manda in SIGABRT su
:dir(). - Tutti i numeri qui sono su una sola piattaforma (macOS arm64, Python 3.14) e provvisori.
Dovresti usare selectolax? Sì, se vuoi velocità da classe lxml con un'API più comoda e sicura rispetto a None, e un comportamento nettamente migliore su input vuoti o malformati — e sei disposto a restare nel territorio solo CSS. Se il tuo codebase si basa su XPath, il costo di riscrittura è reale e va valutato con onestà. E se stai inseguendo "il parser singolo più veloce", la risposta corretta da questo bench è che selectolax e lxml sono abbastanza vicini che il criterio decisivo diventa ergonomia e robustezza, non la pura velocità. In realtà è un motivo migliore per scegliere uno strumento.
Prova Thunderbit per l'estrazione di dati web Get Started Free
FAQ
selectolax è più veloce di BeautifulSoup?
Sì, chiaramente — circa 12-17 volte più veloce di BeautifulSoup(html.parser) e 10-14 volte più veloce di BeautifulSoup(lxml) in un task realistico di parsing ed estrazione, mantenendo il vantaggio su pagine da 1 KB fino a 10 MB (macOS arm64, Python 3.14). Il dato comunemente citato di "4-5x" sottostima il divario rispetto a html.parser.
selectolax è più veloce di lxml? Non in generale. Sul task completo di parsing ed estrazione i due sono pari a 100 KB e 1 MB, e selectolax vince solo sulla pagina da 10 MB. Nel puro parsing senza query, sul mio computer lxml è risultato circa il 33-34% più veloce — un risultato contro il consenso che ho delimitato come specifico di questa piattaforma, quindi da verificare sul tuo hardware.
Meglio il backend Lexbor o Modest?
Lexbor, quasi sempre — è il motore mantenuto e completo che il README consiglia, con una copertura CSS migliore. L'unica eccezione è una pagina che nasconde contenuti dentro <template>, dove il comportamento corretto secondo specifica fa sparire quei contenuti a Lexbor, mentre Modest per caso li mantiene. Modest ha anche spigoli duri, incluso un crash totale dell'interprete su :dir().
selectolax supporta XPath?
No. Nessuno dei due backend espone un metodo xpath() — selectolax è solo CSS. Se i tuoi scraper dipendono da XPath, la migrazione significa riscrivere i selettori, ed è il costo più grande del passaggio da uno stack basato su lxml o parsel.
Perché l'output di selectolax è sballato o mancano elementi?
Di solito ci sono due colpevoli. Se il testo torna con caratteri sostitutivi o accenti mancanti, probabilmente hai passato byte grezzi non UTF-8 — decodificali in str prima (resp.content.decode("latin-1")) del parsing. Se link o elementi mancano su un sito moderno, potrebbero essere dentro tag <template> che il backend Lexbor non attraversa; per quella pagina usa Modest o un altro parser.


