Mozilla's Readability to samodzielna, portowana do JavaScript wersja ekstraktora stojącego za Firefox Reader View. Pakiet jest wydawany jako Apache-2.0 pod nazwą @mozilla/readability i wyciąga treść artykułu z działającego DOM. W środowisku Node potrzebuje więc implementacji DOM, takiej jak jsdom. Nie pobiera stron, nie uruchamia ich JavaScriptu ani nie robi ekstrakcji schematów.
Testowana wersja to npm latest w wydaniu 0.6.0, opublikowanym 3 marca 2025. Kiedy sprawdzałem repo 27 lipca 2026, miało 11 361 gwiazdek (ostatni push był 9 lipca 2026, więc main jest już wyraźnie dalej niż wydany pakiet). Uruchomiłem je na jsdom 29.1.1 oraz Node v22.22.3 na macOS arm64, na 22 oznaczonych fixture’ach HTML przygotowanych specjalnie do tego celu, i wszystkie wyniki w tym tekście pochodzą właśnie z tego zestawu. W praktyce to najlżejsze narzędzie w tej kategorii: dwie minuty instalacji, brak binarek, brak przeglądarki wiszącej w cache, identyczny wynik przy każdym uruchomieniu. Ciekawe nie jest to, jak się go używa — tylko to, że jego błędy da się przewidzieć z kilku stałych w kodzie źródłowym, a jedna z nich robi więcej, niż sugeruje dokumentacja.
W 22 kontrolowanych, syntetycznych fixture’ach Readability odzyskało wszystkie 74 oznaczone bloki artykułu. Ten ograniczony wynik nie znaczy jednak, że nigdy nie gubi tekstu: publiczny benchmark stron realnych raportuje recall 0,982, a znane typy błędów nie odtworzyły się tutaj. Najbardziej widoczny błąd na fixture’ach to dodatkowa zawartość rodzeństwa dopuszczona przez filtr gęstości linków na poziomie źródła ustawiony na 0,25. To, jak poważnie to wygląda, zależy od testbedu.
Czym właściwie jest readability.js, a czym nie jest — w trzech punktach
Readability to rule-based scoring uruchamiany na DOM-ie. Przechodzi po kandydatach, przypisuje każdemu wynik treści, propaguje te wyniki do przodków, wybiera najwyżej punktowane poddrzewo, a potem wykonuje czyszczenie, usuwając to, co wygląda na elementy interfejsu strony. To cała strategia ekstrakcji artykułu — bez modelu, bez danych treningowych, bez reguł per witryna. Dzięki temu działa na stronie, której nigdy wcześniej nie widziało — i dlatego jego błędy są przewidywalne z kodu źródłowego, a to właśnie jest najciekawsze.
Trzy rzeczy, którymi nie jest i które regularnie wprowadzają ludzi w błąd:
- Nie jest pobieraczem stron. Oczekuje
document, a nie URL-a. Pobieranie, retry, anty-bot i nagłówki to Twój problem. - Nie jest rendererem. Nie uruchamia JavaScriptu. Widzi tylko to, co jest w DOM-ie, który mu podasz.
- Nie jest ekstraktorem strukturalnym. Dostajesz
title,byline,excerpt,content(HTML),textContent,length,siteName. Bez schematu, bez typowanych wierszy, bez{name, price}.
Cztery stałe wykonują większość pracy

Wystarczy przeczytać Readability.js w node_modules, żeby zrozumieć zachowanie lepiej niż z jakiejkolwiek strony dokumentacji. Za większość działania biblioteki odpowiadają cztery elementy mechaniki:
- Wynik treści dla każdego punktowanego akapitu:
1 + (commaCount + 1) + min(floor(len / 100), 3). Akapity krótsze niż 25 znaków w ogóle nie są liczone. Wyniki propagują się do przodków z dzielnikami — rodzic dostaje pełny wynik, dziadek połowę, a głębsze poziomylevel · 3. DEFAULT_CHAR_THRESHOLD = 500— minimalna długość artykułu dla „udanego” parsowania. Poniżej tego progu uruchamia się ponownie przesiewanie z mniejszą liczbą reguł czyszczenia.- Regex
unlikelyCandidates— dopasowuje fragmenty class i id typucomment,footer,menu,related,sidebar,social,sponsor. Dopasowane węzły są usuwane jeszcze przed scoringiem. - Reguła dołączania rodzeństwa w
grabArticle— po wybraniu najlepszego kandydata rozpatruje się jego rodzeństwo do dołączenia. Rodzeństwo trafia do wyniku, jeśli jego własny score przekracza próg, albonodeLength > 80 && linkDensity < 0.25, albonodeLength < 80 && nodeLength > 0 && linkDensity === 0 && zawiera kropkę.
Gęstość linków to Σ(linkText.length · coef) / textLength, gdzie coef = 0,3 dla pustych # w href i 1 w pozostałych przypadkach. To właśnie ta reguła tłumaczy przecieki rodzeństwa zmierzone tutaj; nie jest to cały algorytm ekstrakcji.
Konfiguracja i zależność, o której nie mówi się w haśle reklamowym
npm install @mozilla/readability jsdom i można działać. Dwie minuty, bez binarek, bez pobierania po instalacji, bez przeglądarki siedzącej w cache. Na osi instalacji to niemal ideał.
Ale hasło „zero dependencies” dotyczy algorytmu, a nie runtime. Readability działa na żywym document, a w Node trzeba samemu dostarczyć implementację DOM — tutaj był to jsdom 29.1.1. jsdom nie jest mały i w większości pipeline’ów to on, a nie samo ekstraktowanie, jest głównym kosztem pętli. Trzeba to uwzględnić w budżecie.
Jeszcze jeden potknięcie, które kosztowało mnie ponowne uruchomienie: Readability.parse() modyfikuje DOM, który dostaje. Jeśli sparsujesz ten sam obiekt jsdom dwa razy, drugie wywołanie widzi już dokument rozebrany przez pierwsze. W moim harnessie każdy parse tworzy świeży jsdom. Jeśli w pętli przetwarzasz strony i używasz tego samego obiektu dokumentu, żeby oszczędzić czas, to właśnie ten błąd za chwilę zgłosisz.
Jak to testowałem
Nie kierowałem tego na żywe serwisy newsowe. Na stronach realnych dostajesz wynik, ale bez możliwości sprawdzenia dlaczego — a przy heurystyce to „dlaczego” jest sednem. Zamiast tego wygenerowałem 22 fixture’y HTML z 91 oznaczonymi blokami (74 artykułowe, 17 boilerplate), gdzie każde słowo w bloku miało prefiks w postaci unikalnego ciągu sentinelowego przypisanego do tego bloku. Słownictwo między blokami nie nakłada się, więc wyekstrahowany token można przypisać dokładnie do jednego bloku, a „odzyskany” lub „przeciekający” to test przynależności, a nie przybliżone dopasowanie.
Etap ekstrakcji i etap scoringu są celowo rozdzielone. Node runner wypisuje wyłącznie surowy wyekstrahowany tekst, wartości logiczne isProbablyReaderable oraz zmierzone gęstości linków. Całe precision i recall liczone są później przez osobny skrypt, na podstawie tego surowego tekstu i etykiet. Żadna stała metryki nie jest wpisana ręcznie w harnessie — to jedyny sposób, w jaki ufam własnym liczbom.
Następnie podałem identyczne bajty do trafilatura 2.1.0, żeby porównać je w tym samym środowisku testowym. Każdy fixture został sparsowany trzy razy; wszystkie 22 zwróciły za każdym razem tekst identyczny bajt w bajt.
Każdy etap można sprawdzić zamiast wierzyć w tabelę podsumowującą. tests/build_fixtures.mjs tworzy anotowany HTML i ground truth; tests/run_readability.mjs zapisuje ekstrakcję i wynik predyktora; a tests/metrics.py ocenia te dane po fakcie. Surowy output Readability, obliczone metryki i porównanie na tych samych danych są zachowane w artifacts/raw/. Ten podział ma znaczenie, gdy wynik wygląda podejrzanie: można sprawdzić, czy parser zwrócił nieoczekiwany tekst, czy zestaw etykiet był zły, czy kod scoringu go źle sklasyfikował. Reprodukcja na tym pakiecie potwierdza tu stawiane tezy, ale pozostaje sprawdzeniem harnessu — nie dowodem, że realne strony w Twoim wdrożeniu mają taki sam rozkład błędów.
Ograniczenie zakresu jest realne i kluczowe: to są syntetyczne, kontrolowane strony, a nie rzeczywisty korpus. Oficjalne liczby dla stron realnych pochodzą z publicznego article-extraction-benchmark, który dla readability_js 0.6.0 — dokładnie tej wersji testowanej tutaj — podaje word-F1 0,947 ± 0,005 (precision 0,914 ± 0,008, recall 0,982 ± 0,003) na około 181 prawdziwych stronach. Cytuję to; nie odtwarzałem tego wyniku.
W tym miejscu używane są wiersze z aktualnego benchmarku; starsze, zastąpione wiersze historyczne zostały wykluczone. Kontrolowane fixture’y dodają dekompozycję per blok, pokazującą, który kształt treści uruchamia którą regułę, zamiast zastępować publiczny korpus stron realnych.
W syntetycznym pakiecie fixture’ów recall był idealny
74 z 74. We wszystkich 22 fixture’ach Readability nie zgubiło ani jednego oznaczonego bloku artykułu — a na jedenastu czystych, syntetycznych fixture’ach mieszających artykuł i boilerplate mikro-średnia token recall wyniosła 1,000. Nie zniknęło ani jedno zdanie artykułu.
Ale do tego wyniku trzeba dodać dwa zastrzeżenia:
To są czyste, jednoszpaltowe strony syntetyczne. Prawdziwe artykuły bywają głębiej zagnieżdżone, mieszają reklamy w środku tekstu i czasem tracą pierwszy akapit przez artefakt scoringu — taki typ błędu jest opisywany w trackerze (#437, #901 oraz ubytki treści przed tabelą w #922). Moje fixture’y nie wywołały żadnego z nich, więc nie twierdzę, że zostały naprawione — mówię tylko, że mój test do nich nie dotarł. Na stronach realnych recall dla tej wersji wynosi 0,982, a nie 1,000.
Mimo to kierunek wyniku jest najważniejszy. Problem Readability nie polega na tym, że wyrzuca Twój artykuł. Chodzi o to, co zabiera ze sobą.
Wynik precision i dlaczego wymaga trzech etykiet

Jedną liczbę łatwo przytoczyć, ale trudno obronić. Na jedenastu mieszanych fixture’ach Readability zatrzymało 5 z 17 bloków boilerplate — leak rate 0,294.
To nie jest realny wskaźnik przecieku. Trzy różne konfiguracje mierzą trzy różne rzeczy i tylko jedna opisuje zwykłe strony:
| Co mierzy liczba | Wynik |
|---|---|
| Zestaw fixture’ów z wagą adversarialną — 6 z 11 stron mieszanych było zbudowanych tak, by oszukać regułę rodzeństwa | 5 z 17 bloków boilerplate zachowanych (0,294) |
Jedna realistyczna strona — treść w article otoczona nawigacją, banerem reklamowym, sidebar’em, komentarzami i stopką, plus jeden promocyjny blok z neutralną klasą | 5 z 6 bloków interfejsu usunięte; 1 zachowany |
| ~181 prawdziwych stron, publiczny benchmark (nie mój test) | precision 0,914, recall 0,982, word-F1 0,947 — readability_js 0.6.0 |
Pierwszy wiersz czytaj jako test odporności, nie prognozę. Readability nie przepuszcza 29% boilerplate w realnym użyciu. Na realistycznej stronie wszystko z klasami dopasowanymi przez regex unlikelyCandidates — nav-menu, ad-banner, sidebar, comments, site-footer — zostało czysto usunięte, wszystkie pięć elementów. Jedyny ocalały blok to ten, który celowo zaprojektowałem tak, by ominąć ten regex.
Bramka 0,25: gdzie kończy się usuwanie boilerplate
Reguła dołączania rodzeństwa jest opisana w źródle. Nikt jednak, o ile wiem, nie zmierzył dokładnie, w którym miejscu się przełącza. Zbudowałem więc gradient: jeden neutralnie sklasyfikowany <p class="teaser-block"> poza <article>, jednoznaczny artykuł z czterema akapitami, który na pewno wygrywa jako top candidate, i nic oprócz długości oraz gęstości linków promocyjnego bloku. Gęstość liczona jest zgodnie z własnym wzorem Readability, a pomiar odbywa się w runtime, nie na założeniu:
| Blok promocyjny | Długość inner-textu | Powyżej 80 znaków | Zmierzona gęstość linków | Wynik |
|---|---|---|---|---|
| Bez linków | 126 | tak | 0,000 | zachowany |
| Jeden krótki link | 126 | tak | 0,143 | zachowany |
| Jeden dłuższy link | 126 | tak | 0,278 | usunięty |
| Połowa tekstu jako link | 126 | tak | 0,476 | usunięty |
| Jedno zdanie, kończy się kropką | 60 | nie | 0,000 | zachowany |
| Ten sam tekst, bez kropki | 59 | nie | 0,000 | usunięty |
Warunek w kodzie źródłowym używa progu 0,25; próbki pomiarowe objęły go z obu stron — 0,143 zostało zachowane, 0,278 usunięte. Osobna gałąź zachowała zdanie o długości 60 znaków kończące się kropką i odrzuciła wersję 59-znakową bez kropki. Recall artykułu pozostał 4/4 w każdej gałęzi, więc te próbki izolują efekt precision.
Poza test harness’em ta bramka mówi w praktyce: długi, neutralny prose obok artykułu i z małą liczbą linków to też artykuł. A to opisuje wiele rzeczy, które artykułem nie są — zajawkę „Related reading” napisaną jak akapit, pitch newslettera, notatkę redaktorską, sponsorowany teaser napisany pełnymi zdaniami, z linkiem usuniętym do trackingu.
W indeksie RAG taki promocyjny akapit z małą liczbą linków może trafić jako wyekstrahowany chunk i sprawić, że retrieval albo generacja potraktuje go jak treść artykułu. Reguła źródłowa sprawia, że taki błąd jest realny; w tej recenzji nie wykonywałem end-to-end oceny retrieval ani cytowania przez model.
Dla twardego filtra specyficznego dla witryny można prefiltrzyć znane kontenery w DOM-ie źródłowym, zachować pochodzenie węzłów źródłowych do porównania przed serializacją albo zastosować później ostrożnie zweryfikowane filtrowanie wzorców tekstowych. Sam zwrócony HTML może już nie zachowywać informacji, czy węzeł pierwotnie znajdował się poza głównym kontenerem. Reguła rodzeństwa nie jest strojoną opcją publiczną.
Trzy założenia, które fixture’y obaliły
Fixture’y obaliły trzy założenia: że charThreshold odrzuca krótkie artykuły, że znaczniki semantyczne są konieczne i że krótka nietekstowa zawartość zostaje usunięta. Poniższe dowody są tu częścią istotną; nie potrzeba żadnej deklaracji prerejestracji.
charThreshold = 500 nie jest urwiskiem
Popularna interpretacja mówi, że artykuł poniżej 500 znaków zwróci null. To nieprawda. Przeskanowałem długość treści od 120 do 1500 znaków przy wartościach charThreshold 200, 500 i 1000:
| Długość treści | Parse udany przy każdym progu | Długość wyekstrahowana |
|---|---|---|
| 120 | tak | 161 |
| 300 | tak | 342 |
| 460 | tak | 509 |
| 520 | tak | 569 |
| 800 | tak | 841 |
| 1500 | tak | 1555 |
Płasko. Identyczna długość wyjścia przy wszystkich trzech ustawieniach progu, dla każdej wielkości treści. Próg nie decyduje o wartości zwracanej — tylko o tym, czy parser uruchomi ponownie pobieranie z wyłączonymi flagami czyszczenia. Na czystej stronie nie ma czego usuwać, więc przesiew zwraca to samo w obu wariantach. Prawdziwa granica null to „nie ma żadnego tekstu do wyekstrahowania”.
I właśnie to produkuje tu faktyczną awarię, i to gorszą niż fałszywe null. Podałem mu prawie pustą stronę — pasek nawigacji i czterowyrazowy tekst. Zwrócił wynik sukcesem, a „artykuł”, który zwrócił, zawierał nawigację. Gdy nie ma prawdziwego artykułu, Readability podaje boilerplate jako artykuł. Jeśli crawlujesz na dużą skalę i traktujesz wynik różny od null jako „ta strona miała treść”, to jest błędne założenie.
Tagi semantyczne nie wykonują tu pracy
Spodziewałem się spadku recall po zdjęciu semantycznego rusztowania. Ten sam tekst artykułu, dwa warianty: jeden z <main><article><h1> i opisowymi nazwami klas, drugi z <div class="x1"> i akapitami jako zwykłe <div>. Wynik: 4 z 4 bloków artykułu odzyskane w obu przypadkach, zero przecieku boilerplate w obu. Gdy artykuł jest po prostu najbardziej gęstym blokiem tekstowym na stronie, scoring oparty na długości i przecinkach znajduje go bez żadnej pomocy semantyki. „Readability potrzebuje tagów <article>” to mit.
Uczciwe ograniczenie tej tezy: moja strona miała jeden oczywisty blok treści. Tam, gdzie semantyka rzeczywiście mogłaby pomóc, jest strona z dwoma konkurującymi gęstymi poddrzewami, a tego tie-breaka nie testowałem.
Treść nietekstowa zostaje nienaruszona
Reguła „akapitów krótszych niż 25 znaków nie liczymy” kazała mi spodziewać się strat w tabelach i podpisach. Znowu błąd — ta reguła wpływa na scoring, nie na zachowanie treści. Gdy kontener już wygra, wszystko, co w nim jest, idzie z nim:
| Typ treści wewnątrz artykułu | Readability | trafilatura |
|---|---|---|
| Akapity prozatorskie (×2) | zachowane | zachowane |
| Komórki tabeli danych (×2) | zachowane | zachowane |
Blok kodu <pre> | zachowany | zachowany |
Jednowierszowy <p> krótszy niż 25 znaków (×2) | zachowany | zachowany |
<figcaption> | zachowany | usunięty |
| Razem | 8/8 | 7/8 |
To oś, na której cięższy cleaner przegrywa. Jeśli Twoje strony to dokumentacja, tutoriale albo cokolwiek z blokami kodu i podpisanymi ilustracjami, zachowujące całe wygrywające poddrzewo zachowanie Readability jest zaletą.
isProbablyReaderable mówi „nie”, gdy parse() mówi „tak`

README sugeruje użycie isProbablyReaderable(doc) jako taniego testu przed pełnym parse. W moich testach ta bramka odrzuciła trzy różne kształty stron, które potem parse() obsłużyło bez problemu:
| Kształt strony | Werdykt predyktora | parse() | Który parametr to naprawia |
|---|---|---|---|
Treść wyłącznie w elementach <li> | false | sukces | żaden — false przy każdym minScore 1–80 i każdym minContentLength 40–200 |
| Dziesięć akapitów, każdy krótszy niż 140 znaków | false | sukces | minContentLength ≤ 100 (minScore nic nie daje) |
| Jeden akapit o długości 408 znaków | false | sukces | minScore ≤ 10 (score wynosi ok. 16,4) |
| Normalny artykuł (kontrola) | true | sukces | — |
Te trzy błędy mają trzy różne przyczyny i tylko dwie da się dostroić. Przypadek z <li> jest strukturalny: predyktor ocenia tylko węzły p, pre i article (plus rodziców div > br), więc strona, której treść siedzi w elementach listy, nie dopasowuje niczego, dostaje zero punktów i żadne strojenie progu jej nie odzyskuje — taki kształt jest już opisany w issue #662. Wariant z wieloma krótkimi akapitami to bramka minContentLength, która pomija każdy akapit przed scoringiem, więc dziesięć sensownych akapitów sumuje się do zera; obniżenie tej wartości pomaga, a zmiana minScore nie. Wariant z jednym akapitem to arytmetyka: score to sqrt(408 − 140) ≈ 16,4, czyli poniżej domyślnego minScore 20 — pojedynczy akapit musi mieć 540 znaków (140 + 20²), by samodzielnie przejść próg.
README ostrzega, że predyktor daje fałszywe negatywy. Ja dodałbym praktyczną zasadę: nie używaj go jako jedynej bramki. Jeśli strona ma znaczenie, sparsuj ją i sprawdź długość wyniku. Parsowanie nie jest aż tak kosztowne w porównaniu z budową jsdom, za którą już zapłaciłeś.
Te same bajty, dwa ekstraktory
Uruchomienie trafilatura 2.1.0 na identycznych fixture’ach daje czytelniejszy obraz niż zestawianie dwóch liczb zmierzonych na dwóch różnych testbedach, bo wejście jest identyczne bajt w bajt:
| Metryka (11 mixed fixtures) | @mozilla/readability | trafilatura |
|---|---|---|
| Recall bloków artykułu | 1,000 | 1,000 |
| Zatrzymane bloki boilerplate | 5/17 (0,294) | 1/17 (0,059) |
| Token F1 (micro) | 0,948 | 0,969 |
| Recall treści nietekstowej | 8/8 | 7/8 |
| Bardzo krótki artykuł (120 znaków), token F1 | 0,800 | 0,571 |
Żadne z tych narzędzi nie dominuje w tych fixture’ach. Trafilatura zatrzymała mniej bloków rodzeństwa, a Readability zachowało więcej krótkiej i nietekstowej treści. Absolutna precision tokenów w obu narzędziach jest zaniżona przez nieetykietowany tekst nagłówków, więc liczba przecieków na poziomie bloku jest czystszym sygnałem bezpośrednim. Publiczny benchmark stron realnych porządkuje ich word-F1 podobnie, ale korpusy i metryki są inne i nie jest to walidacja między testbedami.
Odporność, krótko: uruchomiłem celowo uszkodzoną bliźniaczą wersję kanonicznej strony (niezamknięte <p>, źle zagnieżdżone <b>/<i>, zbędne </div>) i recall wyniósł 3/3 przy zerowym leak’u, tak samo jak w wersji poprawnej. Zasługa należy się jsdom, którego HTML5 tree builder naprawia bałagan, zanim Readability w ogóle go zobaczy. Żaden fixture nie wysadził parsera.
Plusy i minusy
Plusy
- Recall artykułu to mocna strona: 74/74 oznaczonych bloków odzyskane w 22 syntetycznych fixture’ach, token recall 1,000 na zestawie mieszanym.
- Regex dla klas strony usuwa wiarygodnie elementy interfejsu — nawigację, baner reklamowy, sidebar, komentarze i stopkę — wszystko zniknęło na realistycznej stronie (5 z 6).
- Treść nietekstowa jest zachowywana w całości: tabele, kod w
<pre>, podpisy pod grafikami i linie krótsze niż 25 znaków przetrwały (8/8), podczas gdy trafilatura usunęła podpis. - Brak zależności od semantycznego markup’u — neutralny
<div>z artykułem został oceniony tak samo jak wersja z<article>/<main>. - Krótkie artykuły nie są fałszywie odrzucane: czysta treść odzyskiwana już od 120 znaków, identycznie dla
charThreshold200/500/1000. - Pełna deterministyczność: wszystkie 22 fixture’y zwróciły identyczny tekst w trzech uruchomieniach.
- Dwuminutowa instalacja, Apache-2.0 i wersja na npm zgodna z testowaną (0.6.0), więc nic z tego nie jest nieaktualne.
Minusy
- Bramka dołączania rodzeństwa jest podatna na obejście: długie, neutralnie oznaczone promo-prose z małą liczbą linków jest nie do odróżnienia od tekstu artykułu i przechodzi przy
linkDensity < 0,25. - Na stronach ubogich w treść zwraca boilerplate jako artykuł zamiast
null— prawie pusty fixture wrócił z paskiem nawigacji jako body. isProbablyReaderabledaje fałszywe negatywy na trzech różnych kształtach stron, z czego jednego nie da się naprawić żadnym strojenem.- Wymaga pełnego DOM-u w runtime — narracja o „braku zależności” ukrywa koszt jsdom, który dominuje pętlę.
parse()modyfikuje wejściowy dokument, więc trzeba odbudowywać DOM dla każdej strony.- Brak pobierania, brak renderowania JavaScriptu, brak strukturalnego outputu. To jeden etap pipeline’u, nie cały pipeline.
- Missy na stronach realnych raportowane w trackerze (utrata pierwszego akapitu i zawartości przed tabelą) nie odtworzyły się w moich fixture’ach, więc nie mogę powiedzieć, czy są rzadkie, czy po prostu moje strony do nich nie dotarły.
Kto powinien używać, a kto powinien ominąć
Sięgnij po Readability, jeśli masz już HTML i chcesz wyciągnąć z niego artykuł czystym JavaScriptem, w serwisie Node, gdzie dokładanie zależności Pythona byłoby niewygodne. (Nie zbierałem czasu jako pełnego rozkładu, więc nie formułuję żadnej tezy o szybkości poza tym, że „budowa jsdom dominuje pętlę, nie sama ekstrakcja”.) Funkcje trybu czytania, archiwizacja artykułów offline, newslettery e-mailowe, przyciski „widoku uproszczonego”, rozszerzenia przeglądarki, pipeline’y dokumentacyjne z blokami kodu i podpisami — to jest właściwy obszar, a liczby recall pokazują, że to dobry wybór. Zachowanie narzędzia można też odczytać bezpośrednio ze źródła, a to ma większą wartość, niż się wydaje, gdy trzeba współpracownikowi wyjaśnić, dlaczego właśnie ten blok przeszedł.
Pomiń je, jeśli Twoją metryką jest precyzja usuwania boilerplate’u, zwłaszcza jeśli zasilasz indeks dla LLM-a, gdzie przypadkowy akapit promocyjny może stać się odzyskiwalnym chunk’iem. Pomiń, jeśli strony renderują treść po stronie klienta, bo narzędzie czyta tylko DOM, który mu podasz, i nie uruchamia JavaScriptu. Pomiń, jeśli potrzebujesz {title, price, sku}, a nie prozy — żadna konfiguracja nie zrobi z ekstraktora treści ekstraktora schematów. I jeśli przetwarzasz strony, przy których pytanie „czy ta strona w ogóle miała artykuł” jest realne, nie traktuj zwrotu różnego od null jako odpowiedzi.
Alternatywy i gdzie mieści się stack Thunderbit
Nie jest to przytyk do darmowej biblioteki Apache-2.0 utrzymywanej przez Mozillę — Readability to infrastruktura, działa w Firefoxie od lat i w ekstrakcji reader-mode jest referencją nie bez powodu. Jeśli chcesz szerszego obrazu rynku, prowadzę zestawienie w open-source scrapers roundup oraz szerszy przegląd w the best web scraping tools.
Dla tych samych fixture’ów we wszystkich sześciu ekstraktorach zobacz six-library extraction comparison.
Uwaga autora: Thunderbit to nasza zarządzana opcja do renderowania i ekstrakcji z URL-i. Nie była uruchamiana na tych fixture’ach, więc nie należy z tego wyciągać żadnej porównywalnej tezy jakościowej. Istotna granica brzmi: czy masz już DOM i chcesz lokalnej ekstrakcji artykułu, czy potrzebujesz pobierania/renderowania i strukturalnego outputu jako usługi. Self-hosting unika opłat za użycie u dostawcy, ale nadal niesie koszty infrastruktury i utrzymania.
Uczciwy trade-off: Readability jest darmowe, przejrzyste i Twoje do uruchamiania — możesz przeczytać dokładną bramkę, która zdecydowała o wyniku, a to nie jest coś, co daje zarządzane API. Zarządzany stack kosztuje i ukrywa mechanizm, ale obejmuje etapy fetch-render-structure, które inaczej musiałbyś składać sam. Jeśli interesuje Cię AI-assisted koniec tego spektrum, pisałem też o scraping any website with AI oraz o AI crawlers. Wybieraj według tego, które etapy naprawdę chcesz posiadać.
Wypróbuj Thunderbit do ekstrakcji danych z sieci
Werdykt
Readability to dobry kandydat, jeśli masz już DOM, używasz JavaScript/Node i wolisz okazjonalny nadmiar treści rodzeństwa niż agresywne pomijanie. W tym zestawie fixture’ów odzyskało wszystkie 74 oznaczone bloki artykułu i zachowało tabele, kod oraz podpisy. Ten wynik jest jednak ograniczony do syntetycznych, jednoszpaltowych stron; publiczny recall na stronach realnych wynosi 0,982, znane błędy przy początku artykułu i przy tabelach nie odtworzyły się, a strony ubogie w treść mogą zwrócić boilerplate jako artykuł.
Wystarczy jednak poprawnie zrozumieć słabość. Główna powierzchnia błędu w tych fixture’ach dotyczy precision i znajduje się w konkretnym, opisanym w źródle miejscu: rodzeństwo dłuższe niż 80 znaków i z gęstością linków poniżej 0,25 zostanie dołączone do artykułu, niezależnie od tego, czy naprawdę do niego należy. Widziałem przełączenie przy 0,143 kontra 0,278 na identycznym tekście. Benchmark stron realnych raportuje precision 0,914 i recall 0,982. Jeśli wrzucasz wyekstrahowany tekst do indeksu, z którego później będzie cytował model, sprawdzaj zarówno zachowany boilerplate, jak i pominiętą treść, zamiast zakładać, że żadna z tych klas błędu nie występuje.
Wypróbuj Thunderbit do ekstrakcji danych z sieci Get Started Free
FAQ
Czy Mozilla Readability usuwa cały boilerplate?
Nie, a wynik mocno zależy od tego, co mierzysz. W publicznym benchmarku stron realnych readability_js 0.6.0 osiąga precision 0,914 — czyli około 8,6% tego, co zwraca, nie jest treścią body. Na mojej realistycznej stronie testowej usunęło 5 z 6 bloków interfejsu (nawigacja, baner reklamowy, sidebar, komentarze i stopka zniknęły), zostawiając tylko promo-akapit z neutralną klasą. Na zestawie fixture’ów, który celowo obciążyłem blokami zaprojektowanymi tak, by oszukać heurystykę, zachowało 5 z 17 — ale ta ostatnia liczba to test odporności, nie realny wskaźnik.
Czy do używania readability.js w Node potrzebuję jsdom?
Tak, albo innej implementacji DOM. Readability jest czystym JavaScriptem, ale działa na żywym obiekcie document, więc w Node musisz dostarczyć DOM samodzielnie — w mojej konfiguracji był to jsdom 29.1.1. Określenie „bez zależności” odnosi się do algorytmu, a nie do runtime. Warto też pamiętać, że parse() modyfikuje dokument, który dostaje, więc dla każdej strony twórz świeży DOM zamiast ponownie używać jednego.
Co właściwie robi opcja charThreshold?
Nie to, co większość osób zakłada. Nie sprawia, że krótkie artykuły zwracają null — odzyskiwałem czyste artykuły już od 120 znaków, z identyczną długością wyniku dla charThreshold ustawionego na 200, 500 i 1000. Próg decyduje o tym, czy parser ponownie uruchomi pobieranie z wyłączonymi flagami czyszczenia; na czystej stronie nie ma czego usuwać, więc wynik jest taki sam. Prawdziwe null pojawia się tylko wtedy, gdy na stronie nie ma żadnego tekstu do wyekstrahowania, a nawet strona składająca się wyłącznie z nawigacji wróciła jako nie-null, z nawigacją w roli artykułu.
Czy przed parse() powinienem wywoływać isProbablyReaderable?
Traktuj to jako wskazówkę, nie bramkę. Funkcja zwróciła false na trzech kształtach stron, na których potem parse() zakończyło się sukcesem: treść w elementach <li>, dziesięć akapitów krótszych niż 140 znaków oraz jeden akapit o długości 408 znaków. Przypadku <li> nie da się naprawić tuningiem, bo predyktor ocenia tylko węzły p, pre i article; wariant z wieloma krótkimi akapitami wymaga niższego minContentLength; wariant z jednym długim akapitem potrzebuje niższego minScore, bo pojedynczy akapit musi mieć 540 znaków, by przejść domyślny próg. Jeśli strona ma znaczenie, sparsuj ją i sprawdź wynik.
Readability czy trafilatura do ekstrakcji artykułów?
Na identycznych bajtach fixture’ów trafilatura zatrzymała mniej boilerplate’u (1/17 bloków wobec 5/17), a Readability odzyskało więcej krótkiej treści i zachowało <figcaption>, które trafilatura usunęła. Wybór zależy od tolerancji błędu i runtime. Publiczny benchmark to osobny kontekst, nie walidacja tego wyniku na fixture’ach.


