Six bibliothèques, un jeu de fixtures annotées, un seul scoreur. Sur ces 22 fixtures synthétiques, la bibliothèque qui a laissé passer le plus de boilerplate — Readability de Mozilla, avec 23,5 % — était aussi la seule à récupérer chaque unité d’article étiquetée.
C’est le compromis en une phrase, et la plupart des articles sur le sujet ne le montrent jamais, parce qu’ils s’arrêtent souvent à la précision.
Ce qui a réellement été mesuré
Chaque fixture de ce jeu contient une vérité terrain au niveau de l’unité. Chaque bloc de la page — paragraphes d’article, navigation, publicité, barre latérale, fil de commentaires, promo — est étiqueté article ou boilerplate et associé à un jeton sentinelle unique. Donc, « l’extracteur a-t-il récupéré cette unité ? » revient à tester une appartenance exacte à une sous-chaîne, pas un score de similarité. Une sentinelle survit dans la sortie, ou elle ne survit pas.
Vingt-deux fixtures, 91 unités. Six extracteurs : Mozilla Readability 0.6.0 (via jsdom 30.0.1), trafilatura 2.2.0, resiliparse 1.0.9, newspaper4k 0.9.6, goose3 3.1.22 et jusText 3.0.2. Python 3.14.2 et Node 22 tournaient sur la même machine. L’article ne conserve pas l’OS/CPU, les invocations exactes, le nombre de répétitions ni la politique d’échauffement ; la colonne temps est donc une observation locale, pas un benchmark portable.
Deux règles que je me suis imposées avant tout lancement. Chaque bibliothèque Python a été installée dans son propre virtualenv vide, pour que son empreinte lui appartienne vraiment et ne vienne pas d’un voisin. Et aucun runner ne calcule de métrique : chacun ne produit que du texte brut extrait, puis un scoreur unique génère tous les chiffres, afin que les six outils soient comparés avec la même arithmétique plutôt qu’avec six définitions à peine différentes de la « précision ».
Le tableau principal
| Bibliothèque | Rappel des articles (22/22) | Fuite de boilerplate | Précision des jetons de contenu | Fixtures avec réponse en précision | Jetons parasites |
|---|---|---|---|---|---|
| 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 |
Le rappel est agrégé sur les 22 fixtures. Le taux de fuite, la précision agrégée des jetons de contenu et la contamination utilisent les 11 fixtures contenant à la fois des unités d’article et de boilerplate ; « fixtures avec réponse » indique combien ont produit une sortie. Les chiffres détaillés par fixture sont dans sixway-scores.json.
Une ligne de ce tableau ne correspond pas à la valeur par défaut. extract_plain_text de resiliparse prend main_content=False par défaut, et je l’ai appelé avec main_content=True. La différence est loin d’être anodine : avec les réglages par défaut, il laisse passer 17 unités de boilerplate sur 17 dans le jeu — toute la navigation, les pubs, les barres latérales, les fils de commentaires et les promos — contre 1 sur 17 avec le flag activé. Toutes les autres bibliothèques ci-dessus ont été appelées avec leurs réglages par défaut. Le taux de fuite de 0,0588 de resiliparse est donc celui obtenu quand on demande le contenu principal ; extract_plain_text(html) seul correspond à un produit différent (default-vs-main-content.json).
Lisez ensemble la première et la deuxième colonne, car lire l’une sans l’autre conduit à choisir le mauvais outil.
Readability ne manque jamais. Rappel parfait sur les 22 fixtures, et il est seul dans ce cas. Mais il le paie : 4 unités de boilerplate sur 17 ont fui, 35 jetons parasites, soit un taux de fuite quatre fois supérieur à celui de trafilatura. Trois de ses quatre fuites ont la même forme : un bloc promo de classe neutre placé en frère de l’article, que son heuristique d’ajout des frères absorbe. Si vous envoyez sa sortie à un modèle, vous payez ces jetons, et le modèle les lit comme de l’article.
newspaper4k est le plus équilibré. Zéro fuite, zéro jeton parasite, rappel à 0,9865 et sortie sur les 22 fixtures. Si je devais en choisir un sans connaître la charge de travail, ce serait celui-là, et ce n’est pourtant pas celui que la plupart des gens choisissent spontanément.
goose3 a une précision parfaite et le pire rappel du test. Chaque mot de contenu qu’il a renvoyé venait bien de l’article. Il n’a aussi rien récupéré du tout sur deux fixtures, et n’a produit aucune sortie sur ces mêmes deux. La précision parfaite coûte peu si vous avez le droit de vous taire.
Le chiffre de précision qui a embelli deux bibliothèques
Ce dernier point mérite d’être rendu concret, parce que c’est un piège que j’ai failli publier.
Ici, la précision et le F1 sont conditionnels à la production d’une sortie. Une bibliothèque qui renvoie une chaîne vide sur une fixture n’ajoute rien ni au numérateur ni au dénominateur — donc refuser de répondre est gratuit, et la précision d’un extracteur prudent paraît meilleure que celle d’un extracteur complet, pour aucune autre raison que le silence.
goose3 affiche une précision agrégée de 1,0000 sur les 10 fixtures scorées où il a renvoyé quelque chose. jusText est à 0,8760 sur 10 des 11. Readability, trafilatura, resiliparse et newspaper4k ont répondu sur 11/11. Le tableau affiche maintenant ce dénominateur à côté de la précision, pour que l’abstention ne disparaisse pas derrière un ratio flatteur.
Il existait une version pire de ce calcul. Mon premier scoreur moyennait le rappel d’article sur le même ensemble de 11 fixtures consacrées à la fidélité du contenu — l’ensemble qui exclut les fixtures sans boilerplate, ce qui est correct pour mesurer les fuites. Il renvoyait resiliparse à 1,0000 de rappel. Sur l’ensemble des 22 fixtures, resiliparse est à 0,9054, car sur la fixture dont l’article vit entièrement dans des éléments <li> sans aucun <p>, il renvoie une sortie et ne récupère 0 unité sur 6. Cette fixture n’a pas de boilerplate, donc elle sortait de la moyenne, et un vrai échec se cachait derrière un score parfait.
Où chacun casse réellement
| Fixture | Ce qu’elle teste | Qui ne récupère rien |
|---|---|---|
Article entièrement dans <li>, sans <p> | hypothèses de structure | resiliparse (0/6), goose3 (aucune sortie) |
| Une seule unité d’article de 129 caractères | seuil de contenu court | jusText |
| Dix paragraphes courts, aucun long | seuil de contenu court | jusText |
| Document presque vide | vraie frontière du néant | goose3, jusText |
Chacun de ces comportements est spécifique et reproductible, plutôt qu’un vague « moins bon en extraction » :
- resiliparse et goose3 supposent tous deux des paragraphes. Placez l’un ou l’autre face à une page dont le corps est une liste — un changelog, une spec, une FAQ, une recette — et resiliparse renvoie un texte sans le contenu de la liste, tandis que goose3 ne renvoie rien du tout. resiliparse est le plus risqué des deux ici, parce que renvoyer quelque chose ressemble à un succès.
- jusText a une falaise de longueur, et elle est nette. J’y reviens juste après.
- Le document presque vide est le seul cas où renvoyer rien peut se défendre ; je ne le reprocherais donc ni à l’une ni à l’autre.
jusText : une falaise, pas une pente
jusText a produit une sortie sur 19 fixtures sur 22 et a laissé passer 47 % du boilerplate — le plus mauvais taux du test, à l’opposé de sa réputation. Mais le chiffre intéressant est celui qui m’a forcé à tout relancer.
jusText classe chaque bloc selon la densité de mots vides par rapport à une liste de stopwords de la langue, puis applique un second passage sensible au contexte qui promeut un bloc neargood en good seulement s’il est voisin d’un bloc déjà good. Un bloc devient good tout seul uniquement au-dessus de length_high, qui vaut 200 caractères par défaut. Sur un document où rien ne franchit cette barre, rien n’amorce la promotion et toute la page retombe en boilerplate.
Je l’ai balayé sur un document dont le plus long paragraphe fait 151 caractères :
length_high | Paragraphes bons | Caractères renvoyés |
|---|---|---|
| 200 (défaut) | 0 | 0 |
| 150 | 8 | 832 |
| 120 | 8 | 832 |
| 100 | 8 | 832 |
| 80 | 8 | 832 |
De zéro à 832 caractères dès qu’un seul paragraphe franchit le seuil, puis plus rien ne change, même en relâchant davantage le paramètre. Un seul paragraphe au-dessus de la ligne débloque tout le document.
Avant d’en conclure là, j’ai fait varier length_low sur quatre valeurs et max_link_density sur deux — huit combinaisons, toutes à zéro. La règle de ce projet est qu’une affirmation de capacité négative doit être testée sur au moins trois formes de paramètres, ou être confirmée par le propre nom d’erreur du vendeur pour ce champ ; un paramètre improductif ne suffit pas à conclure quoi que ce soit sur la bibliothèque. Les chiffres sont dans justext-length-threshold.json.
Rien de tout cela ne dit que jusText extrait mal. Sur une vraie page en langage naturel, avec les réglages par défaut, il a renvoyé 1 190 caractères de texte d’article propre. Cela veut dire que jusText possède un réglage documenté qui agit comme un interrupteur, et que la position par défaut de cet interrupteur est mauvaise pour les documents à paragraphes courts.
Ce que vous installez, et ce que l’import vous coûte

Mêmes fixtures, même machine, chaque bibliothèque dans son propre virtualenv vide.
| Bibliothèque | Paquets | site-packages | Import à froid | 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 |
Dans ce test, resiliparse a affiché les plus faibles valeurs d’import à froid et de médiane d’extraction : 15 ms et 0,06 ms. Des ratios exacts entre runtimes surestimeraient ce que le protocole incomplet permet d’affirmer, d’autant plus que sa plus lente extraction isolée montait à 1 098 ms. Il faudrait des distributions séparées pour le démarrage, le premier appel et l’état stable avant d’utiliser ces chiffres pour dimensionner du serverless.
trafilatura et resiliparse sont à égalité technique sur la qualité — 0,9697 contre 0,9681 de F1 sur les jetons de contenu, avec un taux de fuite identique de 0,0588 — et je ne vais pas désigner un vainqueur pour une différence aussi faible. Côté empreinte, en revanche, ils ne jouent pas dans la même catégorie : 21,0 MiB contre 69,9 MiB, 5 paquets contre 17. Le vrai compromis est donc celui entre l’aveuglement de resiliparse sur les listes et les trois dépendances supplémentaires de trafilatura.
Deux bugs de mon propre banc d’essai, trouvés avant publication
La comparaison ci-dessus a failli ne jamais voir le jour, et la raison mérite plus d’attention qu’une simple ligne du tableau.
Le jeu de fixtures ne pouvait pas voir deux des six bibliothèques. Les fixtures d’origine écrivent chaque unité sous la forme d’une suite de jetons absurdes uniques — zzart01vf64 zzart01v56i — ce qui est précisément ce qui rend le rappel exact. Mais cela veut aussi dire qu’il n’y a aucun mot-outil anglais dans les fixtures. Readability, trafilatura et resiliparse décident structurellement, via le DOM, donc cela ne les gênait pas. goose3 et jusText décident lexicalement, en comptant les stopwords, et il n’y en avait aucun à compter : les deux renvoyaient une chaîne vide sur les 22 fixtures.
Un tableau avec deux bibliothèques à zéro aurait eu l’air autoritaire sans rien prouver. J’ai vérifié avant de l’écrire, sur une vraie page : goose3 a renvoyé 1 017 caractères et jusText 1 190. Les bibliothèques allaient bien. Le banc d’essai, lui, ne pouvait pas les représenter.
Les fixtures ont donc été reconstruites avec de la prose anglaise portant les sentinelles — même structure, mêmes classes, mêmes positions DOM, mêmes frontières d’unité, mêmes sentinelles, 1 568 jetons remplacés à l’identique. goose3 est passé de 0 à 20 sur 22.
Puis la reconstruction a cassé deux choses à elle seule, et elles étaient de mon fait. Un mot anglais fait environ six caractères ; zzart01vf64 en fait environ douze. Remplacer l’un par l’autre a divisé par deux la longueur de chaque unité — 21 646 caractères de texte d’unité sont passés à 10 986, et l’unité la plus longue est tombée de 1 513 à 622. Cela a réécrit en silence les fixtures dont l’enjeu est précisément la longueur. jusText, dont le comportement suit une falaise de longueur, est passé de 19/22 à 6/22 rien que pour cette raison. Si j’avais publié la version divisée par deux, le chiffre de jusText aurait été faux d’un facteur trois, dans le sens qui le fait paraître pire.
Le second problème : puiser toutes les unités dans un corpus partagé a rétabli la densité de stopwords, mais a détruit la propriété dont dépend le score au niveau du jeton. Les vocabulaires article et boilerplate doivent être disjoints, sinon « jetons extraits qui sont des jetons de boilerplate » compte le mot the. Dix fixtures sur 22 ont fini avec des vocabulaires qui se chevauchent, contre zéro dans l’original. La correction a consisté à suffixer les mots de contenu pour chaque unité et à laisser les mots-outils nus — de vrais stopwords à compter pour les bibliothèques lexicales, et un vocabulaire de contenu disjoint pour le scoreur.
C’est aussi pour cela que les colonnes au niveau des jetons ici s’appellent content_token_* et ne réutilisent pas les chiffres Readability-vs-trafilatura publiés auparavant. Ce n’est pas la même mesure, elle porte sur les mots de contenu seulement, et prétendre le contraire serait faux.
Pendant la reconstruction, une autre chose est apparue et ne venait pas de moi : trois fixtures à densité de liens placent </a> au milieu d’un mot — <a href="/x">zzsibp015qlhf zzsi</a>bp015qbht — parce que l’ancre avait été positionnée par offset de caractères pour atteindre un ratio exact. Le texte rendu reste inchangé, donc le score d’origine ne l’a jamais remarqué, mais tout extracteur qui travaille par élément plutôt que par flux de texte voit deux fragments là où les autres voient un seul mot. Corrigé, avec le delta lié aux caractères conservé au lieu d’être absorbé en silence.
Qui devrait utiliser quoi
Vous alimentez un modèle et payez au jeton ? newspaper4k ou goose3. Les deux n’ont laissé passer aucune unité de boilerplate ni aucun jeton parasite. newspaper4k si vous voulez une réponse sur chaque page ; goose3 si vous préférez le silence à l’approximation, et que vos pages ont des paragraphes.
Vous optimisez un chemin Python sensible à la latence ? Incluez resiliparse dans la compétition. C’est lui qui a affiché ici l’import et l’extraction médiane les plus faibles, tout en restant très proche de trafilatura sur la qualité — mais seulement avec main_content=True, ce qui n’est pas le réglage par défaut. Vérifiez d’abord les mises en page riches en listes, et ne transformez pas ces temps locaux en ratio de vitesse exact entre runtimes.
Archivage, ou tout cas où rater du contenu est pire que d’en récupérer un peu trop ? Readability. C’est le seul à avoir récupéré chaque unité d’article sur chaque fixture, et 35 jetons errants restent un faible coût si l’alternative est de perdre un paragraphe.
Travail multilingue ? jusText mérite d’être inclus parce qu’il embarque des stoplists par langue. Cette étude n’a pas testé l’extraction multilingue, donc cette fonctionnalité est une raison de l’évaluer, pas une preuve qu’il gagne. Testez length_high sur des longueurs de paragraphes représentatives.
Tout ce qui n’est pas un article ? Aucun de ces outils. Ils reposent tous sur l’hypothèse qu’une page a un corps principal de prose, et une fiche produit, une page de résultats ou un tableau de bord cassent cette hypothèse d’une manière qu’aucun paramètre ne répare.
Où une API managée trouve sa place
Tout ce qui précède concerne des bibliothèques que vous exécutez vous-même : vous fournissez du HTML et recevez du texte. Leurs modes d’échec observés varient selon la forme de la page, donc il faut valider les valeurs par défaut choisies sur votre corpus. L’extraction de champs structurés, ainsi que le fetch et le rendu, sortent du cadre de cette comparaison.
Note d’auteur : Thunderbit est notre service managé pour les flux URL-in et la sortie structurée. Il n’a pas été exécuté sur ces fixtures, donc aucune comparaison de qualité n’est suggérée. La vraie frontière de décision est de savoir si vous possédez déjà le HTML et voulez un extracteur local de texte, ou si vous voulez confier le fetch/rendu et l’exploitation à un service.
Formulé honnêtement : si vous avez déjà le HTML et que vous voulez du texte, l’un de ces six outils est gratuit et bon, et ce tableau vous dit lequel. Si vous récupérez des pages à grande échelle, ou si vous voulez des lignes plutôt qu’un texte continu, c’est un autre achat.
Si vous hésitez plutôt entre des fetchers hébergés, notre tour d’horizon des API de web scraping couvre ce terrain et notre comparatif des coûts des API SEO et data détaille leurs tarifs. Côté auto-hébergé, le pilier sur les scrapers open source offre une vue plus large, et si ce qu’il vous faut est du Markdown plutôt que du texte brut, convertir du HTML en Markdown en Python est l’étape où se perd le plus de contenu.
Essayez Thunderbit pour l’extraction de données web
Verdict
Il n’y a pas de vainqueur, et un tableau qui en désignerait un mentirait sur un vrai compromis.
Construisez d’abord un petit corpus d’acceptation : incluez des articles uniquement en liste, des paragraphes courts, des frères promo, une page presque vide et des cas où renvoyer rien vaut mieux qu’ajouter du bruit. Évaluez séparément la récupération d’articles, la fuite de boilerplate et l’abstention. Sur ces fixtures, Readability favorisait le rappel, newspaper4k donnait la ligne la plus équilibrée, et resiliparse était un candidat intéressant pour la latence mais avec un angle mort sur le contenu en liste ; ces étiquettes ne doivent pas sortir des formes testées sans validation.
Ce que je vous dirais vraiment, c’est plus étroit que tout cela : testez les fixtures sur vos propres formes de page avant de choisir. Deux des six ne voyaient pas le banc d’essai de départ, et l’un d’eux affichait un rappel parfait qui masquait un échec total. Un tableau comparatif est un point de départ pour cela, pas un substitut.
Essayez Thunderbit pour l’extraction de données web Get Started Free
FAQ
Ces chiffres sont-ils comparables aux benchmarks publiés pour ces bibliothèques ? Non, et je ne les citerais pas ainsi. Ce sont des fixtures contrôlées avec des unités synthétiques mais annotées, donc les six ont vu des octets identiques et la comparaison entre elles est équitable. Les chiffres publiés, comme le benchmark d’extraction d’articles de scrapinghub, utilisent des corpus réels, ce qui mesure autre chose, et quelque chose de plus difficile. Utilisez ce tableau pour comparer ces six outils entre eux, pas face à un chiffre issu d’un papier.
Pourquoi le taux de fuite de Readability est-il si supérieur à celui de trafilatura alors qu’ils sont tous deux basés sur le DOM ? À cause de la manière dont chacun trace la frontière. Trois des quatre fuites de Readability sont des blocs promo de classe neutre placés comme frères de l’article ; son heuristique d’ajout des frères les absorbe en partant du principe qu’un contenu long, peu lié et adjacent fait probablement partie du récit. C’est souvent vrai. Ici, c’est une promo. trafilatura est plus strict sur ce qu’il ajoute et n’a laissé fuir qu’une seule de ces unités.
Dois-je faire confiance aux chiffres de précision pour goose3 et jusText ? Seulement avec le nombre d’échantillons à côté. Les deux ont été scorés sur 10 des 11 fixtures contenant à la fois article et boilerplate, parce qu’ils n’ont rien renvoyé sur l’une d’elles, et une fixture sans sortie n’alimente ni le numérateur ni le dénominateur. La précision à 1,0000 de goose3 est réelle pour les pages où il répond ; son rappel de 0,8243 sur les 22 fixtures est l’autre moitié du même constat.
Le seuil de longueur de jusText compte-t-il sur de vraies pages ?
Cela dépend entièrement de la longueur de vos paragraphes. Un article de presse avec des paragraphes de 300 caractères franchira length_high dès le premier et se comportera normalement — c’est pour cela que jusText a renvoyé 1 190 caractères propres sur une vraie page avec les réglages par défaut. Une page de paragraphes courts, d’éléments de liste ou de fiches produit peut ne jamais le franchir, et jusText renverra alors une chaîne vide plutôt qu’une réponse partielle. Réglez-le explicitement plutôt que de le découvrir en production.
Qu’est-ce qui n’a pas été testé ici ? Les vraies pages, totalement. L’extraction multilingue, malgré les stoplists de jusText qui constituent son principal argument. La mémoire sous charge. Toute page qui n’est pas un article — aucun listing produit, aucune page de résultats, aucun tableau de bord. Les cas limites d’encodage. Et les écosystèmes Node et Python n’ont été comparés que sur le comportement des bibliothèques, pas sur la performance des runtimes ; les chiffres en millisecondes à travers cette frontière doivent donc être lus comme des ordres de grandeur, pas comme des ratios précis.


