Sześć bibliotek, jeden oznaczony zestaw fixture’ów, jeden scorer. W tych 22 syntetycznych testach to biblioteka z największym wyciekiem boilerplate’u — Mozilla Readability, z wynikiem 23,5% — jako jedyna odzyskała każdy oznaczony fragment artykułu.
To jest sedno kompromisu w jednym zdaniu, a większość tekstów na ten temat nigdy go nie pokazuje, bo zwykle kończą się na pomiarze precyzji.
Co tak naprawdę mierzyliśmy
Każdy fixture w tym zestawie ma ground truth na poziomie pojedynczych jednostek. Każdy blok strony — akapity artykułu, nawigacja, reklama, sidebar, wątek komentarzy, promo — jest oznaczony jako article albo boilerplate i ma przypisany unikalny znacznik sentinel. Dlatego pytanie „czy ekstraktor odzyskał tę jednostkę” oznacza tu dokładne sprawdzenie wystąpienia podciągu, a nie podobieństwo. Sentinel albo zostaje w wyniku, albo nie.
Dwadzieścia dwa fixture’y, 91 jednostek. Sześć ekstraktorów: Mozilla Readability 0.6.0 (przez jsdom 30.0.1), trafilatura 2.2.0, resiliparse 1.0.9, newspaper4k 0.9.6, goose3 3.1.22 oraz jusText 3.0.2. Python 3.14.2 i Node 22 działały na tej samej maszynie. W artykule nie zachowano systemu operacyjnego, CPU, dokładnych wywołań, liczby powtórzeń ani polityki warm-up, więc kolumna z czasem jest lokalną obserwacją, a nie przenośnym benchmarkiem.
Dwie zasady ustaliłem sobie zanim cokolwiek uruchomiłem. Każda biblioteka Pythona trafiała do własnego pustego virtualenv, więc jej footprint jest wyłącznie jej własny, a nie odziedziczony po zależnościach doinstalowanych przez sąsiada. I żaden runner nie liczył metryki — każdy wypisywał surowy tekst, a jeden scorer generował wszystkie liczby, tak żeby sześć narzędzi porównywać na identycznej arytmetyce, a nie na sześciu podobnie brzmiących definicjach „precision”.
Tabela główna
| Biblioteka | Recall artykułu (wszystkie 22) | Wycieki boilerplate’u | Precision tokenów treści | Fixture’y z wynikiem | Tokenty zanieczyszczające |
|---|---|---|---|---|---|
| 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 |
Recall jest agregowany ze wszystkich 22 fixture’ów. Wskaźnik wycieku, łączna precision tokenów treści oraz contamination używają 11 fixture’ów zawierających zarówno jednostki artykułu, jak i boilerplate’u; „answered” pokazuje, ile z nich zwróciło wynik. Pełne dane per-fixture są w sixway-scores.json.
Jeden wiersz w tej tabeli nie jest domyślny. extract_plain_text w resiliparse ma domyślnie main_content=False, a ja uruchomiłem go z main_content=True. Różnica jest ogromna: przy domyślnych ustawieniach przepuszcza 17 z 17 jednostek boilerplate’u w całym zestawie — każdą nawigację, reklamę, sidebar, wątek komentarzy i promo — wobec 1 z 17 po włączeniu flagi. Wszystkie pozostałe biblioteki wyżej uruchomiłem na ustawieniach domyślnych. Dlatego leak rate resiliparse na poziomie 0.0588 opisuje zachowanie po poproszeniu o main content, a extract_plain_text(html) bez parametru to inny produkt (default-vs-main-content.json).
Czytaj pierwszy i drugi słupek razem, bo osobno dają złą odpowiedź.
Readability nigdy nie pomija treści. Perfekcyjny recall we wszystkich 22 fixture’ach i tylko ono osiągnęło taki wynik. Płaci za to wyciekiem: 4 z 17 jednostek boilerplate’u przedostały się do wyniku, 35 zanieczyszczających tokenów, czyli cztery razy więcej niż w trafilaturze. Trzy z czterech wycieków mają ten sam kształt — neutralnie oznaczony blok promo stojący jako rodzeństwo artykułu, który heurystyka dopinania rodzeństwa po prostu wciąga. Jeśli karmisz tym model, płacisz za te tokenty, a model czyta je jak artykuł.
newspaper4k jest najbardziej zrównoważony. Zero wycieku, zero zanieczyszczających tokenów, recall 0.9865 i wynik na wszystkich 22 fixture’ach. Gdybym miał wybrać jedno narzędzie bez znajomości workloadu, wybrałbym właśnie to — i nie jest to narzędzie, po które większość osób sięga najpierw.
goose3 ma idealną precyzję i najgorszy recall w teście. Każde słowo treści, które zwrócił, faktycznie było treścią artykułu. Tyle że na dwóch fixture’ach nie odzyskał nic, a na tych samych dwóch nie zwrócił żadnego wyniku. Idealna precision jest tania, jeśli wolno ci po prostu odmówić odpowiedzi.
Liczba precision, która upiększyła dwie biblioteki
Ten ostatni punkt warto pokazać konkretnie, bo to była pułapka, przez którą prawie opublikowałem błędny wniosek.
Precision i F1 są tu warunkowe względem zwrócenia wyniku. Biblioteka, która na fixture zwraca pusty string, nie dokłada nic do licznika i nic do mianownika — więc odmowa jest „za darmo”, a zachowawczy ekstraktor wygląda na lepszy od dokładniejszego tylko dlatego, że milczy.
goose3 miało więc zagregowaną precision 1.0000 na 10 ocenionych fixture’ach, na których zwróciło wynik. jusText miał 0.8760 na 10 z 11. Readability, trafilatura, resiliparse i newspaper4k odpowiedziały na 11 z 11. Tabela pokazuje teraz obok precision także mianownik, żeby abstynencja nie znikała za ładnym ułamkiem.
Była jeszcze gorsza wersja tego błędu. Mój pierwszy scorer uśredniał recall artykułu na tym samym 11-fixture’owym zbiorze fidelity treści — zbiorze, który wyklucza fixture’y bez boilerplate’u, co jest poprawne przy mierzeniu wycieku. Wyszedł resiliparse z 1.0000 recall. W całym zestawie 22 fixture’ów resiliparse ma 0.9054, bo na fixture, którego artykuł żyje wyłącznie w elementach <li> i nie ma nigdzie <p>, biblioteka zwraca wynik i odzyskuje 0 z 6 jednostek artykułu. Ten fixture nie ma boilerplate’u, więc wypadł poza średnią, a realna porażka schowała się za perfekcyjnym wynikiem.
Gdzie każda biblioteka naprawdę się wykłada
| Fixture | Co testuje | Kto nie odzyskuje nic |
|---|---|---|
Artykuł wyłącznie w <li>, bez <p> | założenia strukturalne | resiliparse (0/6), goose3 (brak wyniku) |
| Pojedyncza jednostka artykułu o długości 129 znaków | próg krótkiej treści | jusText |
| Dziesięć krótkich akapitów, bez jednego długiego | próg krótkiej treści | jusText |
| Dokument prawie pusty | prawdziwa granica zera | goose3, jusText |
Każdy z tych przypadków to konkretne, odtwarzalne zachowanie, a nie ogólne „gorsze w ekstrakcji”:
- resiliparse i goose3 zakładają akapity. Jeśli wskażesz im stronę, której body składa się z listy — changelog, spec, FAQ, przepis — resiliparse odda tekst bez zawartości listy, a goose3 zwróci pustkę. resiliparse jest tu bardziej zdradliwy, bo zwrócenie czegoś wygląda jak sukces.
- jusText ma próg długości i jest on brutalny. Więcej o tym niżej.
- Dokument prawie pusty to jedyny przypadek, w którym brak wyniku można uznać za poprawny, więc nie traktowałbym tego jako wady którejkolwiek biblioteki.
jusText: krawędź, nie stok
jusText zwrócił wynik na 19 z 22 fixture’ów i przepuścił 47% boilerplate’u — najwięcej w teście, czyli odwrotnie niż sugeruje jego reputacja. Ale najciekawsza jest liczba, przez którą musiałem wszystko uruchomić ponownie.
jusText klasyfikuje każdy blok na podstawie gęstości stopwordów względem językowej listy stop-słów, a potem wykonuje przejście kontekstowe, które promuje blok neargood do good tylko wtedy, gdy sąsiaduje z istniejącym blokiem good. Samodzielnie blok trafia do good dopiero powyżej length_high, którego domyślny próg to 200 znaków. Jeśli w dokumencie nic nie przekracza tej granicy, nic nie inicjuje promocji i cała strona degraduje do boilerplate’u.
Przetestowałem to na dokumencie, którego najdłuższy akapit ma 151 znaków:
length_high | Dobre akapity | Zwrócone znaki |
|---|---|---|
| 200 (domyślnie) | 0 | 0 |
| 150 | 8 | 832 |
| 120 | 8 | 832 |
| 100 | 8 | 832 |
| 80 | 8 | 832 |
Zera do 832 znaków dokładnie w momencie, gdy jeden akapit przekracza próg, a potem już nic się nie zmienia, nawet jeśli dalej go obniżasz. Jeden akapit ponad linię odblokowuje cały dokument.
Zanim to ogłosiłem, przetestowałem jeszcze length_low w czterech wartościach i max_link_density w dwóch — osiem kombinacji, wszystkie z wynikiem zero. Zasada tego projektu mówi, że claim o negatywnej zdolności wymaga sprawdzenia przynajmniej trzech kształtów parametrów albo własnego komunikatu błędu dostawcy wskazującego pole, a jeden nieproduktywny parametr nie jest jeszcze wnioskiem o bibliotece. Liczby są w justext-length-threshold.json.
To nie znaczy, że jusText działa źle. Na prawdziwej stronie z naturalnym tekstem i przy ustawieniach domyślnych zwrócił 1 190 znaków czystej treści artykułu. To znaczy, że jusText ma udokumentowany suwak, który zachowuje się jak przełącznik, a domyślna pozycja tego przełącznika jest zła dla dokumentów z krótkimi akapitami.
Co instalujesz i ile kosztuje samo zaimportowanie

Te same fixture’y, ta sama maszyna, każda biblioteka we własnym pustym virtualenv.
| Biblioteka | Pakiety | site-packages | Cold import | 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 |
W tym przebiegu resiliparse miał najniższe wartości cold importu i mediany ekstrakcji: 15 ms i 0.06 ms. Dokładne stosunki między runtime’ami zawyżyłyby to, co da się obronić przy tym niepełnym protokole, zwłaszcza że jego najwolniejsza pojedyncza ekstrakcja trwała 1 098 ms. Zanim użyje się tych liczb do sizingu serverless, potrzebny jest osobny rozkład startu, pierwszego wywołania i steady-state.
trafilatura i resiliparse są praktycznie remisowe pod względem jakości — 0.9697 wobec 0.9681 w content-token F1, identyczny leak rate 0.0588 — i nie ogłaszam zwycięzcy przy tak małej różnicy. Na footprint nie są nawet blisko: 21.0 MiB wobec 69.9 MiB, 5 pakietów wobec 17. Prawdziwy kompromis to listowa ślepota resiliparse kontra trzy dodatkowe zależności trafilatury.
Dwa błędy w moim własnym testbedzie, wykryte przed publikacją
Powyższe porównanie prawie nigdy by nie powstało i powód jest ważniejszy niż jakikolwiek pojedynczy wiersz w tabeli.
Zestaw fixture’ów nie mógł zobaczyć dwóch z sześciu bibliotek. Oryginalne fixture’y zapisują każdą jednostkę jako ciąg unikalnych bezsensownych tokenów — zzart01vf64 zzart01v56i — i właśnie to daje dokładny recall. Ale oznacza też, że w fixture’ach nie ma ani jednego angielskiego słowa funkcyjnego. Readability, trafilatura i resiliparse działają strukturalnie, na DOM-ie, więc to nie miało znaczenia. goose3 i jusText działają leksykalnie, licząc stopwordy, a tam nie było czego liczyć: oba zwracały pusty string na wszystkich 22 fixture’ach.
Tabela z dwiema bibliotekami ocenionymi na zero wyglądałaby profesjonalnie, ale nic by nie znaczyła. Sprawdziłem to przed pisaniem, na prawdziwej stronie: goose3 zwrócił 1 017 znaków, a jusText 1 190. Biblioteki były w porządku. To testbed nie potrafił ich reprezentować.
Więc fixture’y przebudowano na angielską prozę z zachowanymi sentinelami — ta sama struktura, te same klasy, te same pozycje w DOM, te same granice jednostek, te same sentinel’e, 1 568 tokenów wymienionych jeden do jednego. goose3 przeszedł z 0 do 20 z 22.
Potem rebuild zepsuł jeszcze dwie rzeczy, i obie były moją winą. Angielskie słowo ma około sześciu znaków; zzart01vf64 ma około dwunastu. Zamiana jeden do jednego zmniejszyła długość każdej jednostki o połowę — 21 646 znaków tekstu jednostek spadło do 10 986, a najdłuższa jednostka z 1 513 do 622. To po cichu przepisało fixture’y, których cały sens polega na długości. jusText, którego zachowanie jest właśnie takim progiem długości, spadł z 19 z 22 do 6 z 22 wyłącznie z tego powodu. Gdybym opublikował wersję połówkową, wynik jusText byłby zły trzykrotnie i w kierunku, który czyni bibliotekę gorszą, niż była naprawdę.
Druga rzecz: zbudowanie wszystkich jednostek z jednego wspólnego korpusu przywróciło gęstość stopwordów, ale zniszczyło własność, od której zależy scoring na poziomie tokenów. Słownictwo artykułu i boilerplate’u musi być rozłączne, inaczej „zwrócone tokeny, które są tokenami boilerplate’u” policzą słowo the. Dziesięć z 22 fixture’ów skończyło z nakładającym się słownictwem, podczas gdy w oryginale było ich zero. Naprawa polegała na dopisywaniu sufiksów do słów treści w każdej jednostce i pozostawieniu słów funkcyjnych bez zmian — prawdziwych stopwordów do liczenia przez biblioteki leksykalne i rozłącznego słownictwa treści dla scorera.
Dlatego właśnie kolumny na poziomie tokenów nazywają się tu content_token_*, a nie zostały wzięte wprost z opublikowanych wyników Readability kontra trafilatura. To inna wielkość, mierzona tylko na słowach treści, a podawanie jednej jako drugiej byłoby błędem.
Podczas przebudowy wyszło jeszcze jedno, ale już nie moje: trzy fixture’y z link density umieszczają </a> wewnątrz słowa — <a href="/x">zzsibp015qlhf zzsi</a>bp015qbht — bo anchor ustawiano po offsetcie znakowym, żeby uzyskać dokładny stosunek. Renderowany tekst się nie zmienia, więc oryginalny scoring tego nie widział, ale każdy ekstraktor pracujący na poziomie elementu, a nie całych przebiegów tekstu, widzi dwa fragmenty tam, gdzie inni widzą jedno słowo. Naprawione, zarejestrowane jako zmiana długości związana z linkiem, a nie cicho wchłonięte.
Kto powinien używać czego
Karmisz model i płacisz za token? newspaper4k albo goose3. Obie biblioteki nie przepuściły żadnych jednostek boilerplate’u i żadnych zanieczyszczających tokenów. newspaper4k, jeśli chcesz odpowiedź na każdej stronie; goose3, jeśli wolisz brak odpowiedzi od zgadywania, a twoje strony mają akapity.
Optymalizujesz ścieżkę Pythona wrażliwą na latencję? Włącz resiliparse do porównania. W tym teście miało najniższy zaobserwowany import i medianę ekstrakcji oraz było bardzo blisko trafilatury pod względem jakości — ale tylko z main_content=True, a to nie jest domyślne ustawienie. Najpierw sprawdź układy z dużą liczbą list i nie zamieniaj lokalnych timingów na dokładny ratio między runtime’ami.
Archiwizacja albo cokolwiek, gdzie gorszy jest brak treści niż jej nadmiar? Readability. To jedyna biblioteka, która odzyskała każdą jednostkę artykułu na każdym fixture, a 35 zbłąkanych tokenów to niewielka cena, jeśli alternatywą jest utrata akapitu.
Praca wielojęzyczna? jusText jest kandydatem do rozważenia, bo dostarcza listy stop-słów dla języków. Ten test nie obejmował ekstrakcji wielojęzycznej, więc ta funkcja jest powodem do ewaluacji, a nie dowodem na zwycięstwo. Przetestuj length_high na reprezentatywnych długościach akapitów.
Coś, co nie jest artykułem? Żadna z tych bibliotek. Wszystkie zakładają, że strona ma jedno główne jądro prozy, a listing produktów, wyniki wyszukiwania czy dashboard łamią to założenie w sposób, którego żaden parametr nie naprawi.
Gdzie pasuje zarządzane API
Wszystko powyżej dotyczy bibliotek uruchamianych lokalnie: podajesz HTML i dostajesz tekst. Ich obserwowane tryby awarii zależą od kształtu strony, więc zweryfikuj domyślne ustawienia na własnym korpusie. Ekstrakcja pól strukturalnych oraz pobieranie/renderowanie nie są częścią tego porównania.
Uwaga autora: Thunderbit to nasza usługa zarządzana do workflowów URL-in i ustrukturyzowanego outputu. Nie uruchamiano jej na tych fixture’ach, więc nie sugerujemy porównania jakości. Granica decyzyjna jest prosta: albo masz już HTML i chcesz lokalny ekstraktor tekstu, albo chcesz, by pobieranie/renderowanie i operacje były obsługiwane przez usługę.
Uczciwie rzecz ujmując: jeśli masz już HTML i chcesz z niego tekst, jedna z tych sześciu opcji jest darmowa i dobra, a ta tabela mówi ci która. Jeśli pobierasz strony na dużą skalę albo potrzebujesz wierszy zamiast prozy, to już inny zakup.
Jeśli zamiast tego wybierasz między hostowanymi fetcherami, nasze zestawienie API do web scrapingu obejmuje ten rynek, a porównanie kosztów SEO i API danych pokazuje, ile one kosztują. Po stronie self-hosted szerszy obraz daje pillar o scraperach open-source, a jeśli tak naprawdę potrzebujesz Markdown zamiast zwykłego tekstu, konwersja HTML do Markdown w Pythonie pokazuje, gdzie zwykle znika najwięcej.
Wypróbuj Thunderbit do ekstrakcji danych z sieci
Werdykt
Nie ma zwycięzcy i tabela, która ogłaszałaby jednego, po prostu kłamałaby o realnym kompromisie.
Zanim wybierzesz, zbuduj mały korpus akceptacyjny: dodaj artykuły tylko z listami, krótkie akapity, sąsiednie promo, stronę prawie pustą i przykłady, w których lepiej nie dostać nic niż dostać zanieczyszczony wynik. Oceniaj osobno odzyskanie treści, wyciek boilerplate’u i odmowę odpowiedzi. Na tych fixture’ach Readability stawiał na recall, newspaper4k dawał najbardziej zrównoważony wynik, a resiliparse był kandydatem pod kątem latencji, ale z martwym punktem na treści listowe; tych etykiet nie należy przenosić poza testowane kształty bez walidacji.
To, co powiedziałbym ci naprawdę, jest węższe niż wszystko powyżej: uruchom fixture’y na własnych kształtach stron, zanim wybierzesz. Dwie z sześciu bibliotek nie widziały testbedu, od którego zaczynałem, a jedna z nich dostała perfekcyjny recall, który ukrył całkowitą porażkę. Tabela porównawcza jest tu punktem startowym, nie substytutem.
Wypróbuj Thunderbit do ekstrakcji danych z sieci Get Started Free
FAQ
Czy te liczby da się porównywać z opublikowanymi benchmarkami tych bibliotek? Nie i nie cytowałbym ich w ten sposób. To kontrolowane fixture’y z syntetycznymi, ale oznaczonymi jednostkami, więc wszystkie sześć narzędzi widziało identyczne bajty, a porównanie między nimi jest uczciwe. Opublikowane wyniki, takie jak benchmark ekstrakcji artykułów w scrapinghub, używają realnych korpusów, więc mierzą coś innego i trudniejszego. Używaj tej tabeli do porównania tych sześciu bibliotek między sobą, nie do zestawiania ich z liczbą z artykułu naukowego.
Dlaczego leak rate Readability jest tak dużo wyższy niż w trafilaturze, skoro obie działają na DOM-ie? Bo każda z nich inaczej wyznacza granicę. Trzy z czterech wycieków Readability to neutralnie klasyfikowane bloki promo stojące jako rodzeństwo artykułu, które heurystyka dopinania rodzeństwa wciąga na założeniu, że sąsiednia długa treść z małą liczbą linków jest pewnie częścią historii. Często tak bywa. Na tych fixture’ach to promo. trafilatura jest bardziej restrykcyjna w tym, co dopina, i przepuściła jedną z tych samych jednostek.
Czy mogę ufać liczbom precision dla goose3 i jusText? Tylko razem z liczbą próby. Obie biblioteki oceniano na 10 z 11 fixture’ów, które mają zarówno artykuł, jak i boilerplate, bo na jednym z nich zwróciły pusty wynik, a fixture bez wyniku nie dokłada nic do żadnej strony ułamka. Precision goose3 na poziomie 1.0000 jest prawdziwa dla stron, na które odpowiedziało; jego recall 0.8243 na wszystkich 22 fixture’ach to druga połowa tego samego faktu.
Czy próg długości w jusText ma znaczenie na prawdziwych stronach?
To zależy wyłącznie od długości akapitów. Artykuł newsowy z akapitami po 300 znaków przekroczy length_high już na pierwszym i będzie działał normalnie — dlatego jusText zwrócił 1 190 czystych znaków na prawdziwej stronie przy ustawieniach domyślnych. Strona z krótkimi akapitami, elementami listy albo krótkimi opisami produktów może nigdy tego progu nie przekroczyć, a wtedy jusText zwróci pusty string zamiast częściowej odpowiedzi. Ustaw to jawnie, zamiast odkrywać w produkcji.
Czego tutaj nie testowano? Realnych stron — w ogóle. Ekstrakcji wielojęzycznej, mimo że to właśnie listy stop-słów są główną zaletą jusText. Zużycia pamięci pod obciążeniem. Jakiejkolwiek strony, która nie jest artykułem — bez listingów produktów, bez wyników wyszukiwania, bez dashboardów. Skrajnych przypadków kodowania. I ekosystemy Node oraz Python porównywano tu pod kątem zachowania bibliotek, a nie wydajności runtime’u, więc milisekundowe wartości po obu stronach granicy należy czytać jako rzędy wielkości, a nie precyzyjne proporcje.


