BeautifulSoup nel 2026: il parser HTML più amichevole è anche il più lento (di 12–17x)

Ultimo aggiornamento il July 17, 2026
BeautifulSoup nel 2026: il parser HTML più amichevole è anche il più lento (di 12–17x)
Riepilogo AI
Questa recensione misura BeautifulSoup come il parser HTML per Python più accessibile e attribuisce un costo preciso a questa comodità. Confronta bs4 con parser accelerati in C su velocità, tolleranza all’HTML malformato, copertura dei selettori CSS, retention degli oggetti e recupero delle codifiche. L’articolo mostra che BeautifulSoup è molto più lento, spesso di 12–17 volte, ma spiega anche perché molti sviluppatori continuano a sceglierlo: API leggibili, parsing tollerante, ottimo supporto ai selettori soupsieve ed ergonomia eccellente per lavori di scraping sporadici e disordinati. È una guida pratica per capire quando il costo in velocità è accettabile e quando conviene invece un parser più rapido.

BeautifulSoup è la libreria a cui quasi tutti ricorrono la prima volta che devono estrarre dati da una pagina web in Python, ed è davvero il parser HTML serio più lento tra quelli più affidabili. Entrambe le cose sono vere, e nessuna delle due è una critica. La parte interessante è che “più lento” si traduce in un numero preciso, concreto e valutabile, non in una semplice impressione.

Ho messo bs4 (cioè beautifulsoup4, versione 4.15.0, pubblicata a giugno 2026, con licenza MIT) alla prova con una combinazione di test funzionali recenti e dati di timing riutilizzati dallo stesso banco di benchmark, e il quadro è coerente: in cambio dell’interfaccia più intuitiva e della migliore tolleranza agli errori del settore, paghi un rallentamento di circa un ordine di grandezza. Se questo compromesso abbia senso dipende interamente dal tuo carico di lavoro, quindi questa recensione mette sul tavolo entrambe le facce della medaglia.

Che cos’è davvero BeautifulSoup, e che cosa non è

Molti tutorial saltano la parte più importante: BeautifulSoup non analizza direttamente l’HTML. È un wrapper. In pratica passa il documento a uno dei tre parser reali — il html.parser incluso in Python, lxml oppure html5lib — e poi avvolge l’albero risultante in un’API unica, semplice e molto gradevole per navigazione e ricerca. Il lavoro di bs4 non è parsare. È rendere piacevole esplorare il risultato.

Lo stesso autore la definisce una “screen-scraping library”, e il messaggio è sempre stato lo stesso: puntala verso HTML così malformato da far rabbrividire un browser, e lei riuscirà comunque a tirare fuori i dati che ti servono. Questa reputazione è meritata, con un asterisco di cui parleremo tra poco.

Prima di andare oltre, conviene fissare alcuni fatti:

VoceValore
Pacchettobeautifulsoup4 (importato come bs4)
Versione testata4.15.0 (caricata il 2026-06-07)
Requisito Python>=3.7.0
LicenzaMIT
Sito ufficialecrummy.com/software/BeautifulSoup
Sorgente + bug trackerLaunchpadnon GitHub
ManutenzioneAttiva (4.15.0 a giugno 2026, sei release nell’ultimo anno)

Quel “non GitHub” conta più di quanto sembri. bs4 è una libreria con 20 anni di storia, ospitata su crummy.com e Launchpad, quindi il solito controllo “quanti stelle ha su GitHub?” qui non vale. Il modo corretto di valutarne la salute è guardare la frequenza delle release, e sotto questo aspetto è viva e in buona forma.

Un dettaglio importante sulla licenza, per chi deve rispondere a un team legale o di compliance: il wrapper è MIT, ma ciò che “usi davvero” con bs4 dipende dal backend che installi. html.parser fa parte della libreria standard di Python (licenza PSF, nessuna dipendenza aggiuntiva). lxml è BSD, ma si appoggia a libxml2/libxslt — una dipendenza C esterna che devi compilare oppure ottenere tramite wheel precompilata. html5lib è puro Python e MIT. Se vuoi l’impronta di dipendenze più pulita possibile, il parser integrato html.parser è la scelta più leggera — ed è anche, come vedremo, quello con l’inghippo più grosso. Tra poco.

Il costo in velocità, quantificato

Mettiamo subito il numero nero su bianco, perché è il titolo della storia e nasconderlo sarebbe disonesto. Su un compito realistico di parsing più estrazione — analizzare la stringa, prendere ogni <h3 class="title"> e ogni <a href> — BeautifulSoup è il parser più lento del confronto, e non di poco.

BeautifulSoup speed tax: 232 ms versus 15 ms C parsers

Questi tempi provengono dal banco di benchmark di selectolax riutilizzato (stessa macchina, stessa metodologia a 3 esecuzioni, aggiornato al 2026-07-13); questa recensione non esegue nuovi benchmark di timing, per evitare contesa sulla CPU e lavoro duplicato. Latenza mediana p50, in millisecondi:

Dimensione paginabs4 (html.parser)bs4 (lxml)selectolax-Lexborlxmlbs4-hp più lentobs4-lxml più lento
1 KB0.3230.2810.0270.03612.0x10.5x
10 KB2.0491.7050.1600.16612.8x10.7x
100 KB20.59916.5401.4641.42314.1x11.3x
1 MB232.558181.85514.90114.17715.6x12.2x
10 MB2788.7462261.557159.935172.93317.4x14.1x

Quindi bs4(html.parser) risulta circa 12–17x più lento di un parser C come selectolax-Lexbor, e passando al backend lxml recuperi solo fino a 10.5–14x: resta comunque indietro di un intero ordine di grandezza. Il motivo è strutturale, non un bug: qualunque backend faccia il parsing, bs4 crea un oggetto Python completo (Tag o NavigableString) per ogni singolo nodo. Quel livello di materializzazione degli oggetti è un costo che i parser C semplicemente non pagano.

Nota come il moltiplicatore cresce con l’aumentare della pagina — 12.0x a 1 KB, 17.4x a 10 MB. Questo ti dice che non si tratta di un costo fisso di avvio che puoi ammortizzare. È una tassa per nodo, che scala linearmente con il numero di nodi creati.

Ora cambiamo prospettiva, perché “10x più lento” suona più drammatico di quanto spesso non sia. Su una pagina da 1 MB, si parla di 232 ms contro 15 ms. Se il tuo lavoro è “estrarre qualche centinaio o qualche migliaio di pagine, da qualche centinaio di KB ciascuna”, quella differenza assoluta è praticamente invisibile — non la percepirai, e ottimizzarla non ti porta alcun beneficio reale. Se invece devi gestire una pipeline da un milione di pagine, lo stesso rapporto diventa la differenza tra un job che finisce e uno che non ce la fa. Stesso numero, giudizio opposto. Va valutato sul volume reale, non sul benchmark.

No, cambiare backend non risolve il problema

C’è un mito molto diffuso: basta dare a bs4 il backend lxml per ottenere la velocità di lxml. Non è così, ed è utile capire perché. Su una query CSS batch da 100.000 nodi (seleziona ogni <a> e leggi il suo href, con l’albero già costruito), la differenza di throughput è netta:

ParserQuery p50Nodi/sec
lxml33.30 ms3,002,646
selectolax-Modest34.19 ms2,924,550
selectolax-Lexbor39.46 ms2,534,027
bs4 (lxml)250.56 ms399,111

bs4(lxml) arriva a circa 399.000 nodi al secondo — all’incirca 6.3–7.5x più lento dei tre motori C, anche se il suo backend è proprio lxml. Il backend accelera la fase di costruzione dell’albero. La query e la navigazione, invece, passano comunque attraverso soupsieve e gli oggetti Tag di bs4, e ogni nodo corrispondente viene ancora incapsulato in Python. Quindi il modello mentale “dai a bs4 lxml e diventa veloce come lxml” è sbagliato: il backend velocizza una fase, ma non quella che resta più costosa.

Anche memoria e avvio a freddo fanno parte del conto. Su un documento da 10 MB, bs4 usa circa 1.5–1.75x la memoria residente di selectolax o lxml (218–226 MB contro 129–145 MB) — stessa causa di fondo, un oggetto Python per ogni nodo. E importare bs4 richiede circa 33.4 ms contro 14.1 ms per lxml.html, quindi è 2.36x più lento all’import. Quest’ultimo dato è quasi trascurabile in un processo di lunga durata, ma per una CLI o una funzione serverless che parte continuamente da zero è un costo reale, anche se piccolo, da tenere presente.

Perché i thread non ti salveranno

Se il tuo istinto davanti a un task lento e CPU-bound è “buttiamoci dei thread sopra”, bs4 punirà proprio quell’istinto. Su una pagina da 1 MB parsata 48 volte, confronto tra esecuzione single-thread e 4 thread:

Parser1 thread4 threadAccelerazione
selectolax-Lexbor0.563 s0.159 s3.54x
lxml0.459 s0.378 s1.21x
bs4 (lxml)6.945 s26.842 s0.26x

Leggi due volte l’ultima riga. Quattro thread hanno reso bs4 circa 3.9x più lento, non più veloce. Il segnale empirico è chiaro: bs4 probabilmente tiene stretto il GIL. La costruzione dell’albero avviene in puro Python, quindi resta serializzata sotto il Global Interpreter Lock, e aggiungere thread significa solo introdurre overhead di scheduling in un lavoro che di fatto non può andare in parallelo. selectolax ottiene il suo guadagno di circa 3.5x perché il core C rilascia il lock; bs4 non ha questo margine.

Per l’era del free-threading, il messaggio pratico è questo: se devi parallelizzare BeautifulSoup, usa il multiprocessing (ProcessPoolExecutor), non i thread. selectolax e lxml possono scalare con i thread; bs4 no. Una precisazione metodologica: si tratta di una singola osservazione con un solo numero di thread (4) e una sola dimensione pagina (1 MB), e il meccanismo “tiene il GIL” è un’ipotesi dedotta dal comportamento nel tempo, non qualcosa che ho verificato strumentando il codice per vedere quale percorso trattiene il lock. La direzione è chiara; il meccanismo esatto resta provvisorio.

Il backend di default è la trappola. Leggi prima questo.

Se devi ricordarti una sola cosa di questa recensione, ricorda questa. Un semplice BeautifulSoup(html) senza secondo argomento usa html.parser, e html.parser non implementa le regole HTML5 sui tag di chiusura opzionali. Sembra un dettaglio accademico, finché non ti corrompe i dati in silenzio.

BeautifulSoup backend tolerance matrix: html.parser 12/15, lxml and html5lib 15/15

Ho eseguito 15 campioni HTML deliberatamente malformati su tutti e tre i backend, con una verifica strutturale indipendente dal backend preregistrata per ciascun caso prima dell’esecuzione (così nessuno può scegliere il vincitore dopo aver visto il risultato). I punteggi:

BackendRispetto atteso / 15
lxml15
html5lib15
html.parser12

Le tre differenze condividono tutte la stessa causa. Prendi una tabella senza tag di chiusura: <table><tr><td>a<td>b<tr><td>c<td>d</table>. Con html.parser, il testo estratto delle celle diventa ['abcd','bcd','cd','d'] — ogni <td> si mangia tutto ciò che segue, perché il parser annida le celle invece di chiuderle. lxml e html5lib restituiscono correttamente ['a','b','c','d']. Anche gli elementi di lista non chiusi si comportano allo stesso modo: <li>a<li>b<li>c produce ['abc','bc','c'] con html.parser, e il pulito ['a','b','c'] con gli altri due. Anche gli attributi duplicati si comportano in modo diverso — <div id="first" id="second"> conserva "second" con html.parser ma "first" con lxml/html5lib, e lo standard HTML5 dice di tenere il primo.

Perché questo è pericoloso, non solo fastidioso? Perché succede senza generare errori. Uno scraper che fa tranquillamente BeautifulSoup(html) e si imbatte in una tabella o in un elenco non chiusi — cosa purtroppo comunissima su siti vecchi, HTML scritto a mano e template con tag di chiusura dimenticati — finirà per mescolare il testo delle celle adiacenti in un unico campo, consegnarti dati sporchi e non lamentarsi mai. La soluzione è un solo argomento: BeautifulSoup(html, "lxml") oppure BeautifulSoup(html, "html5lib").

Per correttezza verso html.parser, i restanti 12 campioni malformati su 15 hanno prodotto lo stesso risultato su tutti e tre i backend: tag annidati male come <b><i></b></i>, assenza dell’ossatura html/body, attributi non quotati, tag di chiusura orfani, commenti non chiusi, form annidati, maiuscole/minuscole miste e altro ancora. La tolleranza di bs4 è davvero forte in generale; la divergenza si concentra quasi tutta sulla famiglia dei tag di chiusura opzionali. E nulla di tutto questo è una scoperta nuova — la stessa documentazione di bs4 sulle “Differenze tra parser” dice già che html.parser è “meno permissivo” in modo esplicito. La matrice sui casi malformati aggiunge però i casi specifici e riproducibili in cui “meno permissivo” si traduce in output sbagliato.

Cosa non perdi: API e CSS sono il vero punto forte

Quindi bs4 è lento, monocore e ha la trappola del backend di default. Eppure la gente continua a usarlo, perché la metà “amichevole” del compromesso è del tutto reale — e i test lo confermano.

BeautifulSoup soupsieve CSS coverage wins with 41/41 plus 20/20

Ho eseguito 29 verifiche API su ricerca, CSS, navigazione dell’albero, estrazione del testo e modifica del DOM. Tutte e 29 sono passate, con ogni risultato confrontato contro un valore atteso invece che giudicato a occhio. Due di queste capacità sono vantaggi ergonomici che i parser C semplicemente non offrono:

  • Predicati funzione in find / find_all. Puoi scrivere soup.find(lambda t: t.name == "a" and "btn" in t.get("class", [])) ed esprimere una condizione complessa in una sola riga di Python — senza il classico passaggio “seleziona tutto, poi filtra”.
  • Navigazione dell’albero con nomi chiari e bidirezionale. .parent, .next_sibling, .find_parent, .stripped_strings, .descendants — le traversate si leggono quasi come linguaggio naturale e funzionano in entrambe le direzioni. selectolax per alcune di queste richiede più passaggi, o non le offre affatto.

Ecco la metà che “ti fa risparmiare tempo da sviluppatore”, resa concreta. Non è marketing: sono 29 spunte verdi.

Due trappole da annotare, perché una recensione onesta deve mostrare entrambi i lati. Primo, gli attributi booleani: <input disabled> restituisce in bs4 una stringa vuota "" per disabled (selectolax restituisce None). Entrambi sono falsy, quindi if node.get("disabled") può saltare in silenzio un attributo booleano che in realtà è presente in entrambi i casi; il controllo sicuro è "disabled" in tag.attrs. Secondo, get_text(strip=True) concatena il testo dei nodi senza separatore dopo lo strip, quindi "...with " + "link1" diventa "withlink1". Quando ti servono i confini tra parole, passa separator=" ". Nessuna delle due trappole è specifica di bs4; sono entrambe insidie comuni tra librerie.

E ora la parte che sorprende molti: scegliere bs4 non ti fa perdere copertura CSS. Il suo motore CSS, soupsieve, è l’implementazione più completa di tutto questo confronto. Nella matrice base da 41 casi (riutilizzata dal banco di selectolax), soupsieve ha ottenuto 41/41 — l’unico punteggio perfetto del gruppo, davanti ai 39/41 di selectolax-Lexbor e ai 37/41 di cssselect (lxml/parsel). Poi ho eseguito altri 20 casi estesi documentati da soupsieve, e il risultato è stato 20/20, inclusi selettori che Lexbor rifiuta del tutto: :lang(en), il solo-di-soupsieve :-soup-contains('featured'), :is(), :where() e :has(> a). Le uniche vere lacune sono XPath (soupsieve è solo CSS) e i pseudo-elementi ::text / ::attr() di parsel, che sono estensioni di Scrapy. Se vivi in XPath, il passaggio farà male.

Il verdetto di questa sezione è chiaro: ciò che sacrifichi scegliendo BeautifulSoup è la velocità. Non l’ergonomia dell’API, e sicuramente non la copertura CSS.

Due aspetti di produzione che vale la pena mettere in budget

Oltre al backend di default, ci sono due comportamenti che ti colpiranno soprattutto in workload lunghi o non UTF-8.

Cicli di riferimento: chiama decompose() nei loop lunghi

Ogni Tag di bs4 mantiene un riferimento al proprio genitore e ai propri figli, formando così un ciclo di riferimenti. Il reference counting di CPython da solo non può liberare un ciclo: quel compito spetta al garbage collector generazionale. Per vedere quanto conta davvero, ho creato e distrutto un albero 300 volte con il GC disattivato, poi ho contato gli oggetti Tag ancora presenti in memoria:

BeautifulSoup reference cycles retain 120,900 objects with GC off and 0 with GC on

ScenarioTag trattenuti dopo del
GC disattivato120,900 (300 cicli, nulla recuperato)
GC attivato26,598 (il GC generazionale è intervenuto nel ciclo)
Dopo gc.collect() forzato0 (tutto recuperato)
Controllo senza cicli (lista di stringhe, GC disattivato)delta 0

Con il GC disattivato, del soup non ha liberato nulla — tutti i 120.900 oggetti sono rimasti residenti, perché il ciclo di riferimenti vanifica il reference counting. Un solo gc.collect() ha ripulito tutto. Il gruppo di controllo senza cicli (una semplice lista di stringhe, nota per non formare cicli) ha mostrato un delta pari a zero, prova che l’accumulo derivava dal ciclo di bs4 e non da rumore di misura. La documentazione di bs4 stessa dice che gli oggetti sono “densamente interconnessi ... esattamente il tipo di struttura che metterebbe in difficoltà un garbage collector”, quindi si tratta di un comportamento documentato; il test aggiunge il numero di oggetti trattenuti e la prova che collect() azzera tutto.

La regola pratica: in una pipeline che analizza molte pagine grandi in un loop serrato, se il tuo codice (o qualche impostazione ad alta produttività) disattiva il GC o non lo fa scattare abbastanza spesso, gli alberi bs4 resteranno in memoria e il consumo crescerà. Chiama soup.decompose() dopo ogni pagina — bs4 lo offre proprio per rompere il ciclo e liberare la memoria prima. Gli alberi C di selectolax e lxml non hanno affatto questo problema.

Encoding: UnicodeDammit è il vantaggio silenzioso di bs4

bs4 include un componente che i parser veloci non hanno: UnicodeDammit, che prova a riconoscere la codifica di un documento e a convertirlo automaticamente in Unicode. Gli ho sottoposto una matrice di 8 casi “codifica dichiarata vs. codifica reale”:

BeautifulSoup UnicodeDammit recovers 5 of 8 encoding cases

CasoCodifica realeUnicodeDammit ha ipotizzatoRecuperato?
utf8_no_declutf-8utf-8
utf16_bomutf-16utf-16le
gbk_chinesegbkgb18030Sì (sottinsieme/sovrainsieme compatibile)
shiftjisshift_jiscp932Sì (sovrainsieme compatibile)
latin1_declared_utf8latin-1 (dichiarato utf-8)iso-8859-1Sì (ha ignorato la bugia)
latin1_no_decllatin-1cp720No
cp1252_no_declcp1252cp862No
utf8_declared_latin1utf-8 (dichiarato latin-1)iso-8859-1No (ha seguito la bugia)

Cinque su otto recuperati. UTF-8, UTF-16 con BOM, GBK, Shift-JIS e persino un latin-1 etichettato male sono stati riconosciuti correttamente, e le ipotesi “sovrainsieme” (GBK→gb18030, Shift-JIS→cp932) decodificano comunque senza problemi. Le due modalità di fallimento sono utili da conoscere: brevi campioni in latin-1/cp1252 vengono a volte scambiati per codici DOS, perché il rilevamento statistico non è affidabile su input corti e i caratteri grafici DOS si sovrappongono ai punti di codice latin-1; inoltre, quando un <meta charset> è semplicemente sbagliato, UnicodeDammit si fida della dichiarazione. La documentazione di bs4 segnala entrambe le cose — un campione può essere “così corto che Unicode, Dammit non riesce a fissarlo bene”, e più dati significano una stima migliore.

Rispetto a selectolax, che corrompe in silenzio byte non UTF-8 e si aspetta che tu faccia la decodifica manualmente, questo è un vantaggio reale: bs4 almeno prova a riconoscere il formato e spesso ci riesce. Ma non è una garanzia. Se conosci la codifica, evita il guessing e sii esplicito: BeautifulSoup(bytes, from_encoding="...").

I backend divergono mai davvero su pagine reali?

La matrice sui casi malformati mostra i backend che divergono su input volutamente rotti. La domanda successiva è ovvia: succede qualcosa del genere nel mondo reale? Per rispondere, ho eseguito tutti e tre i backend su 11 pagine reali scaricate — BBC, Wikipedia, Craigslist, MDN, old.reddit, la documentazione di Python, Hacker News, Books to Scrape, webscraper.io, whitehouse.gov e una pagina di citazioni renderizzata via JS — confrontando conteggi di link, titoli e immagini.

Su tutte e 11 le pagine i tre backend hanno concordato. Nessuna divergenza. Questo significa che la differenza vista nella sezione della trappola emerge solo sull’HTML volutamente malformato; quando un sito di produzione moderno è strutturato bene abbastanza — anche se “disordinato” — la scelta del backend non cambia ciò che estrai. Il messaggio pratico è semplice: per i siti mainstream e ben formati, html.parser va benissimo e ti fa risparmiare una dipendenza. Solo quando stai raschiando HTML palesemente non standard, scritto a mano o molto vecchio, la scelta del backend inizia a cambiare i risultati, ed è lì che conviene passare a lxml o html5lib.

Una nota emersa da quel test, perché è un caso limite reale. La pagina di MDN contiene un elemento <template>, e tutti i backend di bs4 hanno restituito 508 link — segno che bs4 appiattisce il contenuto di <template> nell’albero principale. Questo lo allinea a lxml e lo mette in contrasto con selectolax-Lexbor, che segue rigorosamente lo standard HTML5 (un <template> è un DocumentFragment inerte) e restituisce 497, scartando silenziosamente gli 11 link dentro il template. Quindi bs4 cattura i dati dentro un <template> — utile, ma anche un modo per raccogliere contenuti “fantasma” che un browser non renderebbe mai. Nessuno dei due comportamenti è sbagliato; sono interpretazioni diverse dello standard, e devi sapere quale stai ottenendo.

Dove si colloca BeautifulSoup — e dove no

Invece di schiacciare tutto in un singolo punteggio da 0 a 100, che nasconderebbe proprio i compromessi che contano, ecco una scheda per dimensione, con una nota di cautela per ogni riga:

DimensioneCosa hanno mostrato i testNota per il lettore
Installazione / primo avvioWrapper puro, nessun browser/setup; html.parser senza dipendenze; tutte wheel precompilateIl backend lxml richiede una dipendenza C
Velocità vs parser C12–17x più lento (html.parser) / 10.5–14x (backend lxml), su tutte le dimensioniUn solo banco; dati selectolax riutilizzati
Throughput delle query CSS~6–7.5x più lento su 100k nodi; il backend lxml non lo salvaDati riutilizzati; paga la tassa del Python Tag
Memoria1.5–1.75x selectolax/lxml; il più pesanteDati riutilizzati; misurati via RSS
Avvio a freddo dell’import2.36x più lento (33.4 vs 14.1 ms)Dato riutilizzato; voce minore
Scalabilità con threadbs4-lxml ~3.9x più lento con 4 thread (tiene il GIL)Singola osservazione; usare multiprocessing
Ergonomia API29/29 verifiche; find con predicati funzione + navigazione bidirezionaleTrappole: attributi booleani stringa vuota e strip senza separatore
Copertura CSSsoupsieve il migliore: 41/41 base + 20/20 estesi; supporta :langNiente XPath, niente ::text
Tolleranza sui 3 backendlxml/html5lib 15/15; html.parser 12/15Divergenza solo su HTML malformato
Coerenza su pagine reali3 backend d’accordo 11/11; tutti appiattiscono <template> (508)Siti ben formati: il backend non conta
Garbage collection per cicli di riferimentoL’albero è un ciclo; 300 loop hanno trattenuto 120,900 oggetti, collect() ha azzeratoNei loop lunghi serve decompose()
EncodingUnicodeDammit recupera 5/8; sbaglia sui campioni corti, segue dichiarazioni sbagliateSingola osservazione
ManutenzioneAttiva (4.15.0, giugno 2026); MITSito su crummy/Launchpad, non GitHub

Allora, per chi è BeautifulSoup? Per chi dà più valore a un’API leggibile e a un parsing tollerante rispetto al throughput puro, lavorando su volumi moderati — prototipi, scraping una tantum, strumenti interni, team in cui il tempo di sviluppo costa più del tempo di esecuzione. Chi dovrebbe guardare altrove? Pipeline da milioni di pagine in cui il costo della lentezza diventa denaro vero, workload che richiedono parallelismo a livello di thread e chi è legato indissolubilmente a XPath.

Una nota sul suo ruolo in uno stack di scraping reale, e su dove si colloca il nostro strumento. BeautifulSoup presume che tu abbia già l’HTML in mano. Non scarica pagine, non renderizza JavaScript e non affronta difese anti-bot o CAPTCHA — è un altro lavoro, e nel web moderno è davvero difficile. È qui che un’API di scraping AI si colloca a un livello diverso: lo stack per sviluppatori di Thunderbit — una REST API, un server MCP e una CLI — gestisce il fetch, il rendering JS e il problema anti-bot, poi restituisce Markdown pulito (POST /distill) oppure JSON strutturato in base allo schema (POST /extract) senza che tu debba scrivere selettori. I due strumenti non sono concorrenti; si completano a vicenda. bs4 analizza l’HTML che hai già ottenuto; l’API, MCP e CLI di Thunderbit ti portano all’HTML che altrimenti sarebbe difficile raggiungere. Se il tuo collo di bottiglia è il parsing, bs4 è una risposta sensata. Se il collo di bottiglia è l’acquisizione, allora serve un altro livello.

Prova Thunderbit per l’estrazione di dati web

In sintesi

BeautifulSoup ti offre l’API più amichevole, la miglior tolleranza all’HTML malformato e il motore CSS più completo tra quelli analizzati — il tutto pagato con un costo in velocità di circa un ordine di grandezza e con l’ingombro di memoria più alto. Questo è l’accordo, detto senza giri di parole. Il backend di default html.parser è la vera trappola: rovina in silenzio tabelle e liste non chiuse, quindi passa sempre "lxml" o "html5lib" quando l’input potrebbe essere sporco. I thread non lo accelerano — il multiprocessing sì. E nei loop lunghi, usa decompose() su ogni pagina per evitare l’accumulo dei cicli di riferimento.

Due limiti finali da tenere presenti. Tutto qui è stato misurato su una singola piattaforma (macOS arm64, Python 3.14, wheel precompilate), e i moltiplicatori di timing sono riutilizzati dal banco di selectolax (stesso benchmark, aggiornato al 2026-07-13) invece di essere rieseguiti — quindi ereditano quel limite di piattaforma singola, e una configurazione Linux x86_64 o compilata da sorgente potrebbe spostare i numeri esatti. E nei risultati non c’è nessuna scoperta rivoluzionaria: bs4 è una libreria con 20 anni di storia, quindi ogni comportamento testato è già documentato o pubblicamente registrato. Il valore non sta nello scoop. Sta nel mettere un numero reale a compromessi che la documentazione descrive solo in modo qualitativo.

Domande frequenti

BeautifulSoup è lento? Sì, in modo misurabile. In un compito di parsing più estrazione funziona circa 12–17x più lentamente di un parser C come selectolax-Lexbor con il backend di default html.parser, e 10.5–14x più lentamente con il backend lxml, perché crea un oggetto Python per ogni nodo. Quanto questo pesi dipende dalla scala: su una pagina da 1 MB parliamo di 232 ms contro 15 ms, irrilevante per qualche migliaio di pagine ma decisivo per una pipeline da un milione di pagine.

Quale parser di BeautifulSoup dovrei usare — html.parser, lxml o html5lib? Per siti mainstream e ben formati, il parser predefinito html.parser va bene e non aggiunge dipendenze. Ma non implementa i tag di chiusura opzionali dell’HTML5, quindi su tabelle o liste non chiuse mescola il testo adiacente senza segnalare errori. Quando l’input può essere malformato, scritto a mano o vecchio, passa esplicitamente "lxml" o "html5lib" — entrambi hanno ottenuto un 15/15 perfetto nella matrice sull’HTML malformato, mentre html.parser si è fermato a 12/15.

BeautifulSoup può fare parsing in parallelo con i thread? No. La costruzione dell’albero in bs4 è puro Python e mantiene il GIL, quindi aggiungere thread lo rende più lento, non più veloce — nei test, quattro thread hanno eseguito un parsing da 1 MB circa 3.9x più lentamente di un thread singolo. Per parallelizzare bs4, usa il multiprocessing (ProcessPoolExecutor). Le librerie con core C, come selectolax e lxml, sono quelle che possono davvero trarre vantaggio dal parallelismo a livello di thread.

BeautifulSoup gestisce bene l’HTML rotto? In generale sì — su un ampio insieme di campioni malformati (tag annidati male, struttura di base mancante, attributi non quotati e altro), tutti e tre i backend hanno recuperato correttamente. Il punto debole è il backend di default html.parser e i tag di chiusura opzionali: <td> e <li> non chiusi vengono annidati invece di essere chiusi, corrompendo il testo estratto. Passa al backend lxml o html5lib e questo tipo di problema scompare.

BeautifulSoup vs lxml — quale è migliore? Sono strumenti diversi. lxml è molto più veloce sia nella costruzione dell’albero sia nelle query, e supporta XPath. BeautifulSoup avvolge lxml (tra gli altri) in un’API molto più amichevole e ha davvero una copertura CSS più ampia grazie a soupsieve. Però non aspettarti che il backend lxml trasformi bs4 in lxml: il backend accelera solo il parsing, mentre query e navigazione continuano a pagare il costo dell’oggetto Python per nodo di bs4, lasciandolo ancora circa 6–7.5x più lento nelle selezioni batch su grandi volumi.

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

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.
Indice
Thunderbit · Agente AI per dati web

Estrai dati da qualsiasi pagina in 1 clic

Scelto da oltre 250.000 utenti
piano gratuito disponibile
Estrai dati usando l'AI
Trasferisci facilmente i dati su Fogli Google, Airtable o Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week