Sei librerie, un set di fixture annotato, un solo scorer. In queste 22 fixture sintetiche, la libreria con la maggiore perdita di boilerplate — Readability di Mozilla, al 23,5% — è stata anche l’unica a recuperare ogni singola unità di articolo etichettata.
In una frase, questo è il vero trade-off. E la maggior parte dei contenuti che trovi su questo tema non lo mette in chiaro, perché si ferma alla precisione.
Cosa è stato misurato davvero
Ogni fixture di questo set include un ground truth a livello di unità. Ogni blocco della pagina — i paragrafi dell’articolo, la navigazione, l’annuncio, la sidebar, i commenti, il promo — è etichettato come article o boilerplate e contrassegnato con un token sentinella univoco. Quindi “l’estrattore ha recuperato questa unità?” non è una stima di somiglianza, ma un’appartenenza esatta a sottostringa. Una sentinella c’è nell’output oppure no.
Ventidue fixture, 91 unità. Sei estrattori: Mozilla Readability 0.6.0 (tramite jsdom 30.0.1), trafilatura 2.2.0, resiliparse 1.0.9, newspaper4k 0.9.6, goose3 3.1.22 e jusText 3.0.2. Python 3.14.2 e Node 22 sono stati eseguiti sulla stessa macchina. L’articolo non conserva sistema operativo/CPU, invocazioni esatte, numero di ripetizioni o politica di warm-up, quindi la colonna dei tempi va letta come osservazione locale, non come benchmark portabile.
Prima di eseguire qualsiasi test, mi sono imposto due regole. Ogni libreria Python è stata installata nel proprio virtualenv vuoto, così il footprint è solo suo e non ereditato da ciò che avrebbe installato una dipendente. Inoltre nessun runner calcola una metrica: ognuno esporta il testo estratto grezzo, e un unico scorer produce tutti i numeri. In questo modo si confrontano sei strumenti con la stessa aritmetica, invece di sei definizioni “quasi uguali” di precisione.
La tabella principale
| Libreria | Recall degli articoli (tutte le 22) | Perdita di boilerplate | Precisione dei token di contenuto | Fixture con precisione calcolata | Token contaminanti |
|---|---|---|---|---|---|
| Readability | 1.0000 | 0.2353 | 0.9109 | 11/11 | 35 |
| trafilatura | 0.9865 | 0.0588 | 0.9411 | 11/11 | 4 |
| newspaper4k | 0.9865 | 0.0000 | 0.9452 | 11/11 | 0 |
| resiliparse | 0.9054 | 0.0588 | 0.9381 | 11/11 | 7 |
| jusText | 0.8378 | 0.4706 | 0.8760 | 10/11 | 74 |
| goose3 | 0.8243 | 0.0000 | 1.0000 | 10/11 | 0 |
Il recall è aggregato su tutte le 22 fixture. Il tasso di leakage, la precisione complessiva sui token di contenuto e la contaminazione usano le 11 fixture che contengono sia unità di articolo sia unità di boilerplate; “fixture con precisione calcolata” indica quante di queste hanno prodotto output. I valori completi per ogni fixture sono in sixway-scores.json.
Una riga di quella tabella non è un valore di default. extract_plain_text di resiliparse usa main_content=False come impostazione predefinita, e io l’ho chiamato con main_content=True. La differenza è tutt’altro che marginale: con i default perde 17 unità su 17 di boilerplate nell’intero set — ogni nav, annuncio, sidebar, thread di commenti e promo — contro 1 su 17 con l’opzione attiva. Tutte le altre librerie qui sopra sono state eseguite con i rispettivi default. Quindi il tasso di leakage di resiliparse allo 0,0588 è quello che ottieni quando chiedi il contenuto principale; extract_plain_text(html) da solo è un prodotto diverso (default-vs-main-content.json).
Leggi insieme la prima e la seconda colonna, perché prenderne in considerazione solo una ti porta a scegliere lo strumento sbagliato.
Readability non sbaglia mai il contenuto. Recall perfetto su tutte le 22 fixture, e da solo in questa categoria. Ma paga il prezzo: 4 unità di boilerplate su 17 sono finite nell’output, 35 token contaminanti, un tasso di leakage quattro volte superiore a quello di trafilatura. Tre delle quattro perdite hanno la stessa forma: un blocco promo classificato come neutro, fratello dell’articolo, che il suo heuristic di append del sibling ingloba. Se il suo output finisce in un modello, stai pagando quei token e il modello li legge come articolo.
newspaper4k è quello più bilanciato. Leakage zero, token contaminanti zero, recall 0,9865 e output su tutte le 22 fixture. Se dovessi sceglierne uno senza conoscere il carico di lavoro, sarebbe questo — e non è quello che la maggior parte delle persone sceglie per primo.
goose3 ha precisione perfetta e il peggior recall del test. Ogni parola di contenuto che ha restituito apparteneva davvero all’articolo. Ma su due fixture non ha recuperato nulla e su quelle stesse due non ha prodotto output. La precisione perfetta costa poco se puoi permetterti di non rispondere.
Il numero di precisione che ha abbellito due librerie
Vale la pena dirlo in modo esplicito, perché è una trappola che quasi avevo pubblicato.
Qui precisione e F1 dipendono dal fatto che esista un output. Una libreria che restituisce una stringa vuota su una fixture non contribuisce né al numeratore né al denominatore: quindi il rifiuto è gratis, e la precisione di un estrattore prudente sembra migliore di quella di uno più completo, solo perché tace.
goose3 ha ottenuto una precisione aggregata di 1.0000 sulle 10 fixture valutate in cui ha prodotto output. jusText è arrivato a 0,8760 su 10 delle 11. Readability, trafilatura, resiliparse e newspaper4k hanno risposto a 11 su 11. La tabella ora mostra quel denominatore accanto alla precisione, così l’astensione non può sparire dietro un rapporto troppo lusinghiero.
Esisteva anche una versione peggiore di questo problema. Il mio primo scorer faceva la media del recall degli articoli sullo stesso insieme di 11 fixture usato per la fidelity dei contenuti — l’insieme che esclude le fixture senza boilerplate, cosa corretta per misurare il leakage. In quel caso resiliparse risultava con 1.0000 di recall. Su tutte le 22 fixture, invece, è a 0,9054, perché nella fixture il cui articolo vive interamente dentro elementi <li> senza nemmeno un <p>, restituisce output e recupera 0 su 6 unità di articolo. Quella fixture non ha boilerplate, quindi era fuori dalla media, e un fallimento reale restava nascosto dietro a un punteggio perfetto.
Dove ciascuno si rompe davvero
| Fixture | Cosa testa | Chi non recupera nulla |
|---|---|---|
Articolo interamente in <li>, senza <p> | assunzioni sulla struttura | resiliparse (0/6), goose3 (nessun output) |
| Singola unità di articolo da 129 caratteri | soglia per contenuti brevi | jusText |
| Dieci paragrafi brevi, nessuno lungo | soglia per contenuti brevi | jusText |
| Documento quasi vuoto | il vero limite del nulla | goose3, jusText |
Ognuno di questi è un comportamento specifico e riproducibile, non un generico “estrae peggio”:
- resiliparse e goose3 partono entrambi dall’ipotesi che ci siano paragrafi. Punta uno dei due su una pagina il cui corpo è una lista — changelog, specifica, FAQ, ricetta — e resiliparse ti restituisce testo senza i contenuti della lista, mentre goose3 non restituisce nulla. resiliparse è quello più insidioso dei due, perché restituire qualcosa sembra un successo.
- jusText ha un precipizio di lunghezza, ed è netto. Più sotto lo analizzo.
- Il documento quasi vuoto è l’unico caso in cui non restituire nulla è plausibilmente corretto, quindi non lo considererei un difetto.
jusText: un precipizio, non una pendenza
jusText ha prodotto output su 19 delle 22 fixture e ha lasciato filtrare il 47% del boilerplate — il valore più alto del test, l’esatto contrario della sua reputazione. Ma il numero interessante è quello che mi ha costretto a rieseguire tutto.
jusText classifica ogni blocco in base alla densità di stopword rispetto a una stoplist linguistica, poi esegue una seconda passata sensibile al contesto che promuove un blocco neargood a good solo quando è accanto a un blocco già good. Un blocco diventa good da solo solo oltre length_high, che per default è 200 caratteri. In un documento in cui nessuna parte supera quella soglia, nulla innesca la promozione e l’intera pagina degrada a boilerplate.
L’ho testato su un documento il cui paragrafo più lungo è di 151 caratteri:
length_high | Paragrafi buoni | Caratteri restituiti |
|---|---|---|
| 200 (default) | 0 | 0 |
| 150 | 8 | 832 |
| 120 | 8 | 832 |
| 100 | 8 | 832 |
| 80 | 8 | 832 |
Da zero a 832 caratteri quando un solo paragrafo supera la soglia, e poi niente cambia anche abbassandola ancora. Un paragrafo oltre la linea sblocca l’intero documento.
Prima di concluderlo, ho variato length_low su quattro valori e max_link_density su due: otto combinazioni, tutte a zero. La regola di questo progetto è che una dichiarazione di incapacità negativa richiede almeno tre forme di parametro testate, oppure l’errore del vendor che nomina il campo; un parametro improduttivo non basta per parlare della libreria. I numeri sono in justext-length-threshold.json.
Questo non significa che jusText estragga male. Su una pagina reale, in lingua naturale, con i default, ha restituito 1.190 caratteri puliti di testo dell’articolo. Significa che jusText ha una manopola documentata che si comporta come un interruttore, e che la posizione predefinita di quell’interruttore è sbagliata per documenti fatti di paragrafi brevi.
Cosa installi, e quanto costa importarlo

Stesse fixture, stessa macchina, ogni libreria nel proprio virtualenv vuoto.
| Libreria | Pacchetti | site-packages | Importazione a freddo | Extraction p50 |
|---|---|---|---|---|
| resiliparse | 5 | 21,0 MiB | 0,015 s | 0,06 ms |
| jusText | 3 | 22,4 MiB | 0,777 s | 0,56 ms |
| goose3 | 16 | 44,3 MiB | 2,181 s | 1,85 ms |
| newspaper4k | 22 | 47,5 MiB | 2,812 s | 2,69 ms |
| trafilatura | 17 | 69,9 MiB | 1,584 s | 0,51 ms |
| Readability + jsdom | 32 (npm) | 26 MiB | 0,473 s | 6,37 ms |
In questa esecuzione, resiliparse ha avuto i valori più bassi sia per l’importazione a freddo sia per l’estrazione mediana: 15 ms e 0,06 ms. I rapporti esatti tra runtime diversi sarebbero però fuorvianti, perché il protocollo è incompleto, soprattutto visto che la singola estrazione più lenta ha toccato 1.098 ms. Prima di usare questi numeri per dimensionare un sistema serverless servono distribuzioni separate per startup, prima chiamata e regime stabile.
trafilatura e resiliparse sono praticamente alla pari sulla qualità — 0,9697 contro 0,9681 di F1 sui token di contenuto, stesso tasso di leakage a 0,0588 — e non proclamo vincitore per una differenza così piccola. Sul footprint, però, la distanza è enorme: 21,0 MiB contro 69,9 MiB, 5 pacchetti contro 17. Il vero trade-off è la cecità di resiliparse verso le liste contro le tre dipendenze aggiuntive di trafilatura.
Due bug nel mio testbed, trovati prima della pubblicazione
Il confronto qui sopra ha rischiato di non vedere la luce, e il motivo vale più di qualsiasi singola riga della tabella.
Il set di fixture non riusciva a vedere due delle sei librerie. Le fixture originali scrivono ogni unità come una sequenza di token inventati univoci — zzart01vf64 zzart01v56i — ed è proprio questo che rende il recall esatto. Ma significa anche che non ci sono parole funzionali inglesi. Readability, trafilatura e resiliparse decidono in base alla struttura del DOM, quindi non ne sono state influenzate. goose3 e jusText decidono in base al lessico, contando le stopword, e lì non c’era nulla da contare: entrambe restituivano stringa vuota su tutte le 22 fixture.
Una tabella con due librerie a zero sarebbe sembrata autorevole ma non avrebbe detto nulla. Ho verificato prima di scriverla, su una pagina reale: goose3 ha restituito 1.017 caratteri e jusText 1.190. Le librerie funzionavano. Era il testbed a non poterle rappresentare.
Così le fixture sono state ricostruite con prosa inglese che conteneva le sentinelle — stessa struttura, stesse classi, stesse posizioni nel DOM, stessi confini delle unità, stesse sentinelle, 1.568 token sostituiti uno a uno. goose3 è passato da 0 a 20 su 22.
Poi la ricostruzione ha rotto due cose, ed erano entrambe colpa mia. Una parola inglese è lunga circa sei caratteri; zzart01vf64 circa dodici. Sostituire uno a uno ha dimezzato ogni unità — 21.646 caratteri di testo unitario sono diventati 10.986, e l’unità più lunga è scesa da 1.513 a 622. Questo ha riscritto in silenzio proprio le fixture il cui scopo era misurare la lunghezza. jusText, il cui comportamento è un precipizio legato alla lunghezza, è passato da 19 su 22 a 6 su 22 per questo solo motivo. Se avessi pubblicato la versione dimezzata, il numero di jusText sarebbe stato sbagliato di un fattore tre, e nel verso che lo fa sembrare peggiore.
Il secondo problema: attingere ogni unità da un corpus condiviso ha ripristinato la densità di stopword, ma ha distrutto la proprietà su cui si basa il punteggio a livello di token. Il vocabolario dell’articolo e quello del boilerplate devono essere disgiunti, altrimenti “token estratti che sono token di boilerplate” finisce per contare la parola the. Dieci fixture su 22 sono finite con vocabolari sovrapposti, contro zero nelle originali. La correzione è stata suffissare le parole di contenuto per unità e lasciare nude le parole funzionali — stopword reali per le librerie lessicali, vocabolario di contenuto disgiunto per lo scorer.
Per questo anche le colonne a livello di token qui si chiamano content_token_* e non riusano i numeri pubblicati nel confronto Readability-versus-trafilatura. Sono una quantità diversa, misurata solo sulle parole di contenuto, e spacciarne una per l’altra sarebbe sbagliato.
Durante la ricostruzione è emersa anche un’altra cosa, non colpa mia: tre fixture con densità dei link inseriscono </a> in mezzo a una parola — <a href="/x">zzsibp015qlhf zzsi</a>bp015qbht — perché il link era stato posizionato tramite offset di carattere per raggiungere un rapporto esatto. Il testo renderizzato non cambia, quindi il punteggio originale non se n’è accorto, ma qualsiasi estrattore che lavori per elemento invece che per flusso di testo vede due frammenti dove gli altri vedono una parola sola. Corretto, con il delta dei caratteri linkati registrato invece di essere assorbito in silenzio.
Chi dovrebbe usare cosa
Stai alimentando un modello e paghi per token? newspaper4k o goose3. Entrambi hanno lasciato fuori zero unità di boilerplate e zero token contaminanti. newspaper4k se vuoi una risposta su ogni pagina; goose3 se preferisci il silenzio a un’ipotesi e le tue pagine hanno paragrafi.
Stai ottimizzando un percorso Python sensibile alla latenza? Metti resiliparse nel confronto. In questo test ha avuto l’importazione e l’estrazione mediana più basse, e sulla qualità ha tenuto testa a trafilatura — ma solo con main_content=True, che non è il default. Controlla prima i layout ricchi di liste, e non trasformare questi tempi locali in un rapporto esatto di velocità tra runtime diversi.
Archiviazione, o qualunque scenario in cui perdere contenuto è peggio che aggiungerne troppo? Readability. È l’unico che ha recuperato ogni unità di articolo in ogni fixture, e 35 token fuori posto sono un prezzo basso se l’alternativa è perdere un paragrafo.
Lavori multilingue? jusText è un candidato da includere perché fornisce stoplist per lingua. Questo studio però non ha testato l’estrazione multilingue, quindi quella caratteristica è un motivo per valutarlo, non una prova che vinca. Prova length_high su lunghezze di paragrafo rappresentative.
Qualsiasi cosa che non sia un articolo? Nessuno di questi. Tutti partono dall’assunto che una pagina abbia un corpo principale di prosa, e una scheda prodotto, una pagina di risultati di ricerca o una dashboard rompono quell’assunto in modi che nessun parametro risolve.
Dove entra in gioco un’API gestita
Tutto quanto sopra riguarda librerie che esegui tu: fornisci HTML e ricevi testo. Le loro modalità di fallimento osservate cambiano in base alla forma della pagina, quindi i default vanno verificati sul tuo corpus. L’estrazione strutturata dei campi e il fetching/rendering non fanno parte di questo confronto.
Nota dell’autore: Thunderbit è il nostro servizio gestito per flussi URL-in e output strutturato. Non è stato eseguito su queste fixture, quindi non implica alcun confronto di qualità. La vera soglia decisionale è se hai già l’HTML e vuoi un estrattore locale, oppure se vuoi che fetching/rendering e operatività siano gestiti da un servizio.
La formulazione onesta è questa: se hai già l’HTML e vuoi il testo, una di queste sei è gratuita e valida, e questa tabella ti dice quale. Se invece stai recuperando pagine su larga scala, o vuoi righe di dati invece che prosa, allora stai facendo un acquisto diverso.
Se stai scegliendo tra fetcher gestiti, il nostro roundup delle API di web scraping copre quel mercato e il nostro confronto dei costi tra SEO API e data API copre quanto fanno pagare. Sul lato self-hosted, il pillar sugli scraper open source offre una visione più ampia; e se ciò che ti serve davvero è Markdown e non testo semplice, convertire HTML in Markdown in Python è il punto in cui si perde di più.
Prova Thunderbit per l’estrazione di dati web
Verdetto
Non c’è un vincitore, e una tabella che ne nomina uno mentirebbe su un trade-off reale.
Prima di scegliere, costruisci un piccolo corpus di accettazione: includi articoli composti solo da liste, paragrafi brevi, promo come fratelli dell’articolo, una pagina quasi vuota ed esempi in cui non rispondere è meglio di contaminare. Valuta separatamente recupero degli articoli, leakage di boilerplate e astensione. Su queste fixture, Readability ha favorito il recall, newspaper4k ha prodotto la riga più equilibrata e resiliparse è stato un candidato per la latenza con un punto cieco sui contenuti in lista; queste etichette non devono essere generalizzate oltre le forme testate senza ulteriore validazione.
Quello che ti direi davvero è più ristretto di tutto questo: prova le fixture sulle tue forme di pagina prima di scegliere. Due delle sei non riuscivano a “vedere” il testbed con cui avevo iniziato, e una di esse ha ottenuto un recall perfetto che nascondeva un fallimento totale. Una tabella di confronto è un punto di partenza, non un sostituto.
Prova Thunderbit per l’estrazione di dati web Get Started Free
FAQ
Questi numeri sono confrontabili con i benchmark pubblicati per queste librerie? No, e non li citerei in quel modo. Queste sono fixture controllate con unità sintetiche ma etichettate, quindi tutte e sei hanno visto gli stessi byte e il confronto tra loro è corretto. I valori pubblicati, come il benchmark di article extraction di scrapinghub, usano corpus reali e misurano qualcosa di diverso e più difficile. Usa questa tabella per confrontare queste sei librerie tra loro, non contro un numero preso da un paper.
Perché il leakage di Readability è così più alto di quello di trafilatura se entrambe si basano sul DOM? Perché il confine è tracciato in punti diversi. Tre delle quattro perdite di Readability sono blocchi promo classificati come neutri, appaiati come fratelli dell’articolo, che il suo heuristic di sibling-append ingloba nella teoria che contenuti lunghi, poco linkati e adiacenti facciano probabilmente parte della storia. Spesso è vero. In queste fixture, invece, è un promo. trafilatura è più severa su ciò che aggiunge e ha perso una delle stesse unità.
Devo fidarmi dei numeri di precisione per goose3 e jusText? Solo insieme al numero di campioni. Entrambi sono stati valutati su 10 delle 11 fixture che contengono sia articolo sia boilerplate, perché su una di esse non hanno restituito nulla, e una fixture senza output non contribuisce a nessun lato del rapporto. La precisione di goose3 a 1.0000 è reale per le pagine a cui ha risposto; il suo recall di 0.8243 su tutte le 22 fixture è l’altra metà dello stesso fatto.
La soglia di lunghezza di jusText conta davvero su pagine reali?
Dipende interamente dalla lunghezza dei paragrafi. Un articolo di news con paragrafi da 300 caratteri supera length_high al primo blocco e si comporta normalmente — ed è per questo che jusText ha restituito 1.190 caratteri puliti su una pagina reale con i default. Una pagina fatta di paragrafi brevi, elementi di lista o descrizioni prodotto può non superarla mai, e a quel punto jusText restituisce una stringa vuota invece di una risposta parziale. Impostala esplicitamente, invece di scoprirlo in produzione.
Cosa non è stato testato qui? Le pagine reali, in generale. L’estrazione multilingue, nonostante le stoplist siano il principale punto di forza di jusText. La memoria sotto carico. Qualsiasi pagina che non sia un articolo — niente listing di prodotti, niente risultati di ricerca, niente dashboard. I casi limite di encoding. E i due ecosistemi Node e Python sono stati confrontati sul comportamento delle librerie, non sulle prestazioni del runtime, quindi i tempi in millisecondi oltre quel confine vanno letti come ordini di grandezza, non come rapporti precisi.


