Шесть библиотек, один размеченный набор тестов, один оценщик. В этих 22 синтетических примерах библиотека с самым высоким уровнем утечки служебного текста — Mozilla's Readability, 23,5% — оказалась единственной, которая восстановила каждый помеченный фрагмент статьи.
Вот и весь компромисс в одной фразе, и большинство материалов на эту тему его даже не показывает, потому что почти все останавливаются на точности и на этом заканчивают.
Что именно измерялось
Каждый тестовый пример в этом наборе имеет покомпонентную разметку. Каждый блок страницы — абзацы статьи, навигация, реклама, сайдбар, комментарии, промо-блок — помечен как article или boilerplate и снабжен уникальным маркером. Поэтому вопрос «восстановил ли экстрактор этот фрагмент» решается точно по наличию подстроки, а не по близости результата. Маркер либо сохраняется в выводе, либо нет.
22 тестовых примера, 91 блок. Шесть экстракторов: Mozilla Readability 0.6.0 (через jsdom 30.0.1), trafilatura 2.2.0, resiliparse 1.0.9, newspaper4k 0.9.6, goose3 3.1.22 и jusText 3.0.2. Python 3.14.2 и Node 22 запускались на одной и той же машине. В статье не сохранены ОС/CPU, точные команды запуска, число повторов и политика прогрева, поэтому столбец с временем — это локальное наблюдение, а не переносимый бенчмарк.
Перед запуском я задал себе два правила. Каждая Python-библиотека ставилась в собственный пустой virtualenv, чтобы ее размер зависимостей не был унаследован от того, что подтянул сосед. И ни один раннер не считает метрику сам — каждый просто выгружает сырой извлеченный текст, а один общий оценщик считает все числа, так что шесть инструментов сравниваются на идентичной арифметике, а не на шести похожих, но разных определениях «точности».
Главная таблица
| Библиотека | Полнота восстановления статьи (все 22) | Утечка служебного текста | Точность по контент-токенам | Сколько раз был ответ по точности | Загрязняющие токены |
|---|---|---|---|---|---|
| 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 |
Полнота восстановления усреднена по всем 22 тестовым примерам. Уровень утечки, общая точность по контент-токенам и загрязнение считаются по 11 примерам, где одновременно есть article и boilerplate; «ответ был» показывает, на скольких из них библиотека вообще вернула результат. Полные данные по каждому примеру лежат в sixway-scores.json.
Одна строка в этой таблице — не стандартное значение. У resiliparse функция extract_plain_text по умолчанию использует main_content=False, а я вызывал ее с main_content=True. Разница существенная: на значениях по умолчанию она пропускает 17 из 17 блоков служебного текста во всем наборе — всю навигацию, рекламу, сайдбары, комментарии и промо — против 1 из 17 при включенном флаге. Все остальные библиотеки в таблице запускались с дефолтными настройками. Поэтому показатель утечки 0.0588 у resiliparse — это поведение в режиме извлечения основного контента, а extract_plain_text(html) без параметра — уже другой продукт (default-vs-main-content.json).
Смотрите на первый и второй столбцы вместе, потому что по отдельности они легко уводят к неправильному выбору.
Readability не пропускает ничего. Идеальная полнота на всех 22 примерах, и в этом она единственная. Но за это приходится платить: утекли 4 из 17 блоков служебного текста, 35 загрязняющих токенов, что в четыре раза выше, чем у trafilatura. Три из четырех утечек выглядят одинаково — нейтрально помеченный промо-блок стоит рядом со статьей, и срабатывает эвристика присоединения соседей. Если подавать такой вывод в модель, ты платишь за эти токены, а модель читает их как часть статьи.
newspaper4k — самый сбалансированный вариант. Нулевая утечка, ноль загрязняющих токенов, полнота 0.9865 и ответ на всех 22 примерах. Если бы мне пришлось выбрать один инструмент, не зная задачи, я бы выбрал его, хотя его обычно и не вспоминают первым.
goose3 показывает идеальную точность, но худшую полноту в тесте. Каждый контентный токен, который он вернул, действительно был частью статьи. Но на двух примерах он не восстановил вообще ничего и на тех же двух не выдал никакого результата. Идеальная точность дешева, если тебе разрешено просто отказаться от ответа.
Та точность, которая польстила двум библиотекам
Этот момент стоит показать отдельно, потому что здесь легко попасть в ловушку — я сам едва не опубликовал неверный вывод.
Здесь точность и F1 зависят от того, был ли вообще выдан результат. Если библиотека возвращает пустую строку на примере, она не попадает ни в числитель, ни в знаменатель — поэтому молчание бесплатно, а у осторожного экстрактора точность выглядит лучше, чем у тщательного, просто потому, что он не отвечает.
goose3 получил агрегированную точность 1.0000 на 10 тестовых примерах, где он вообще что-то вернул. У jusText было 0.8760 на 10 из 11. Readability, trafilatura, resiliparse и newspaper4k ответили на 11 из 11. В таблице я теперь показываю рядом с точностью и число ответов, чтобы отказ от ответа не прятался за красивой долей.
Была и более плохая версия этого эффекта. В первой версии оценщика я усреднял полноту восстановления статьи по тому же набору из 11 примеров, где измеряется качество контента — это корректно для метрики утечки, потому что туда не включаются примеры без boilerplate. И там resiliparse показывал 1.0000 полноты. Но по всем 22 примерам у него 0.9054, потому что на примере, где статья целиком находится внутри элементов <li> и нет ни одного <p>, библиотека все же возвращает результат, но восстанавливает 0 из 6 блоков статьи. В этом примере нет служебного текста, поэтому он не попал в усреднение, и реальный провал спрятался за идеальным числом.
Где именно ломается каждый инструмент
| Тестовый пример | Что проверяется | У кого вообще нет восстановления |
|---|---|---|
Статья целиком в <li>, без <p> | предположение о структуре | resiliparse (0/6), goose3 (нет вывода) |
| Один блок статьи длиной 129 символов | порог короткого контента | jusText |
| Десять коротких абзацев, без длинного | порог короткого контента | jusText |
| Почти пустой документ | истинная граница пустоты | goose3, jusText |
Каждый из этих случаев — это конкретное, воспроизводимое поведение, а не просто «хуже извлекает»:
- resiliparse и goose3 оба предполагают наличие абзацев. Если дать им страницу, где тело состоит из списка — changelog, спецификация, FAQ, рецепт — resiliparse вернет текст без содержимого списка, а goose3 не вернет ничего. Здесь resiliparse опаснее, потому что возврат хоть чего-то выглядит как успех.
- У jusText есть резкий порог по длине. Ниже — подробнее.
- Почти пустой документ — это как раз тот случай, когда возврат пустоты, возможно, и корректен, так что предъявлять это как недостаток библиотеке я бы не стал.
jusText: не склон, а обрыв
jusText выдал результат на 19 из 22 примеров и пропустил 47% служебного текста — это самый высокий показатель в тесте, что противоположно его репутации. Но интереснее всего оказалось число, из-за которого мне пришлось все пересчитать.
jusText сначала классифицирует блоки по плотности стоп-слов относительно языкового списка, а затем делает контекстный проход, который повышает блок neargood до good только если рядом уже есть good. Самостоятельно блок становится good только если он длиннее length_high, а по умолчанию это 200 символов. Если в документе ни один блок не пересекает этот порог, то нечему запускать повышение, и вся страница деградирует до служебного текста.
Я прогнал его на документе, где самый длинный абзац — 151 символ:
length_high | Хороших абзацев | Возвращено символов |
|---|---|---|
| 200 (по умолчанию) | 0 | 0 |
| 150 | 8 | 832 |
| 120 | 8 | 832 |
| 100 | 8 | 832 |
| 80 | 8 | 832 |
С 0 до 832 символов — стоит одному абзацу перейти через порог, и весь документ сразу открывается. А дальше, как ни ослабляй настройку, результат уже не меняется.
Прежде чем делать вывод, я также прогнал length_low по четырем значениям и max_link_density по двум — восемь комбинаций, и все дали ноль. Правило этого проекта такое: отрицательное утверждение о возможностях должно быть проверено минимум по трем формам параметров, либо подтверждено самим сообщением об ошибке от вендора о конкретном поле; один бесполезный параметр еще не доказывает проблему библиотеки. Данные лежат в justext-length-threshold.json.
Это не означает, что jusText плохо извлекает текст. На реальной англоязычной странице с настройками по умолчанию он вернул 1 190 символов чистого текста статьи. Это значит, что у jusText есть документированный параметр, который ведет себя как переключатель, и что положение этого переключателя по умолчанию плохо подходит для страниц с короткими абзацами.
Что вы устанавливаете и сколько стоит импорт

Те же тестовые примеры, та же машина, каждая библиотека — в своем пустом virtualenv.
| Библиотека | Пакеты | site-packages | Холодный импорт | 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 |
В этом прогоне у resiliparse были самые низкие значения и по холодному импорту, и по медиане извлечения: 15 мс и 0.06 мс. Но точные кросс-рантаймные коэффициенты здесь были бы слишком смелым выводом для неполного протокола, особенно учитывая, что самый медленный единичный вызов у него достигал 1 098 мс. Прежде чем использовать эти цифры для serverless-оценки, нужны отдельные распределения для старта, первого вызова и устойчивого режима.
trafilatura и resiliparse почти равны по качеству — 0.9697 против 0.9681 по F1 на контент-токенах, одинаковая утечка 0.0588 — и я не буду назначать победителя при такой малой разнице. Но по размеру они уже совсем не рядом: 21.0 MiB против 69.9 MiB, 5 пакетов против 17. Реальный компромисс здесь — слепота resiliparse к спискам против трех дополнительных зависимостей trafilatura.
Две ошибки в собственном стенде, найденные до публикации
Сравнение выше почти не состоялось, и причина этого важнее, чем любая отдельная строка в таблице.
Набор тестов вообще не видел две из шести библиотек. В исходных тестах каждый блок был набором уникальных бессмысленных токенов — zzart01vf64 zzart01v56i, — и именно это делало полноту восстановления точной. Но из-за этого в тестах не было ни одного английского служебного слова. Readability, trafilatura и resiliparse принимают решения структурно, по DOM, поэтому это не мешало. goose3 и jusText принимают решения лексически, по числу стоп-слов, а считать было нечего: обе библиотеки возвращали пустую строку на всех 22 примерах.
Таблица с двумя нулями выглядела бы убедительно и ничего бы не значила. Я проверил это до публикации на реальной странице: goose3 вернул 1 017 символов, а jusText — 1 190. С библиотеками все было в порядке. Проблема была в самом стенде.
Поэтому тесты перестроили на английской прозе с теми же маркерами — та же структура, те же классы, те же позиции в DOM, те же границы блоков, те же маркеры, 1 568 токенов заменены один к одному. goose3 при этом поднялся с 0 до 20 из 22.
Затем перестройка сломала уже две вещи, и обе по моей вине. Английское слово — это примерно 6 символов; zzart01vf64 — примерно 12. Замена один-к-одному вдвое сократила каждый блок — 21 646 символов текста блоков превратились в 10 986, а самый длинный блок уменьшился с 1 513 до 622. Это незаметно переписало именно те тесты, смысл которых — проверка длины. У jusText, поведение которого зависит от резкого порога длины, результат из-за этого упал с 19 из 22 до 6 из 22. Если бы я опубликовал укороченную версию, число по jusText оказалось бы неверным почти в три раза и выглядело бы так, будто библиотека хуже, чем есть на самом деле.
Вторая проблема: если все блоки брать из одного общего корпуса, возвращается плотность стоп-слов, но теряется свойство, на котором держится токеновый скоринг. Словарь article и словарь boilerplate должны быть непересекающимися, иначе в счет «извлеченные токены, которые являются токенами служебного текста» попадет слово the. В 10 из 22 тестовых примеров словари начали пересекаться, тогда как в оригинале пересечений не было вовсе. Исправление было таким: к контентным словам каждого блока добавили суффиксы, а служебные слова оставили как есть — реальные стоп-слова для лексических библиотек и непересекающийся контентный словарь для оценщика.
Поэтому столбцы с токенами здесь названы content_token_*, а не переиспользуют значения из опубликованных сравнений Readability и trafilatura. Это уже другая величина, измеряемая только по контентным словам, и подменять одно другим было бы неверно.
Во время переработки всплыло и еще одно несоответствие, уже не с моей стороны: в трех тестах с плотностью ссылок тег </a> находится внутри слова — <a href="/x">zzsibp015qlhf zzsi</a>bp015qbht — потому что якорь был смещен по символьной позиции, чтобы попасть в точное соотношение. Отрендеренный текст не меняется, поэтому исходный скоринг этого не заметил, но любой экстрактор, работающий по элементам, а не по текстовым фрагментам, видит уже два куска там, где остальные видят одно слово. Это исправлено, а разница по связанным символам зафиксирована, а не тихо поглощена.
Что кому использовать
Подаете текст в модель и платите за токены? newspaper4k или goose3. Оба не пропустили ни одного блока служебного текста и не добавили загрязняющих токенов. newspaper4k — если нужен ответ на каждой странице; goose3 — если лучше молчание, чем догадка, и у тебя страницы действительно состоят из абзацев.
Нужен минимальный latency в Python-пайплайне? Включите resiliparse в сравнение. В этом прогоне у него были самые низкие значения по импорту и медиане извлечения, и по качеству он почти сравнялся с trafilatura — но только при main_content=True, а это не дефолт. Сначала проверь списко-ориентированные страницы и не превращай эти локальные числа в точное сравнение скорости между рантаймами.
Архивация или любая задача, где потерять контент хуже, чем пропустить лишнее? Readability. Это единственный инструмент, который восстановил каждый фрагмент статьи на каждом тесте, и 35 посторонних токенов — небольшая плата, если альтернатива — потерять абзац.
Многоязычные задачи? jusText можно рассматривать, потому что он поставляется со списками стоп-слов для разных языков. В этом исследовании мультиязычное извлечение не проверялось, так что это повод его оценить, а не доказательство, что он лучший. Проверь length_high на абзацах с твоей реальной длиной.
Все, что не является статьей? Ни один из этих инструментов. Все они предполагают, что на странице есть один основной текстовый блок, а карточка товаров, страница результатов поиска или дашборд ломают это предположение так, что никакой параметр уже не спасает.
Где в этой картине место управляемого API
Все сказанное выше — это библиотеки, которые вы запускаете сами: вы отдаете HTML и получаете текст. Их слабые места зависят от формы страницы, поэтому выбранные значения по умолчанию нужно проверять на своем корпусе. Извлечение структурированных полей и этапы загрузки/рендеринга в это сравнение не входят.
Примечание автора: Thunderbit — это наш управляемый сервис для сценариев «URL на входе» и структурированного вывода. Он не прогонялся через эти тесты, поэтому никакого сравнения качества здесь не подразумевается. Граница выбора проста: либо HTML уже у вас и нужен локальный текстовый экстрактор, либо хочется, чтобы сервис сам занимался загрузкой, рендерингом и эксплуатацией.
Честная формулировка такая: если HTML уже у вас и нужен именно текст, одна из этих шести библиотек бесплатна и хороша, а таблица выше подсказывает какая. Если же вы собираете страницы на масштабе или вам нужны строки, а не проза, это уже совсем другая покупка.
Если вы выбираете между hosted-fetcher решениями, наш обзор web scraping API roundup покрывает этот рынок, а сравнение стоимости SEO- и data-API — их цены. Для self-hosted стороны полезен пиллар по open-source scraper-ам, а если вам на самом деле нужен Markdown, а не plain text, то конвертация HTML в Markdown в Python — это место, где теряется больше всего.
Попробовать Thunderbit для извлечения веб-данных
Итог
Победителя здесь нет, и таблица, которая назвала бы одного, солгала бы о реальном компромиссе.
Перед выбором собери небольшой набор приемки: включи статьи только из списков, короткие абзацы, соседние промо-блоки, почти пустую страницу и примеры, где лучше ничего не вернуть, чем вернуть загрязненный текст. Считай отдельно восстановление статьи, утечку служебного текста и отказ от ответа. На этих тестах Readability выиграл по полноте, newspaper4k дал самый сбалансированный результат, а resiliparse оказался кандидатом по скорости с провалом на списках; эти ярлыки нельзя переносить на другие типы страниц без проверки.
На самом деле я бы сформулировал совет уже: прогони эти тесты на своих типах страниц до того, как выберешь инструмент. Две из шести библиотек вообще не видели тот тестовый стенд, с которого я начал, а одна из них показала идеальную полноту, скрывавшую полный провал. Сравнительная таблица — это точка старта, а не замена проверке.
Попробовать Thunderbit для извлечения веб-данных Get Started Free
Часто задаваемые вопросы
Можно ли сравнивать эти цифры с опубликованными бенчмарками для этих библиотек? Нет, и я бы не стал так их цитировать. Здесь используются контролируемые тестовые примеры с синтетическими, но размеченными блоками, поэтому все шесть видели одинаковые данные, и сравнение между ними честное. Публичные метрики вроде benchmark от scrapinghub по article extraction используют реальные корпуса, а это уже другая и более сложная задача. Используй эту таблицу, чтобы сравнить эти шесть библиотек между собой, а не с числом из статьи.
Почему у Readability утечка выше, чем у trafilatura, если обе библиотеки работают по DOM? Потому что у них по-разному проходит граница статьи. Три из четырех утечек Readability — это промо-блоки нейтрального типа, стоящие рядом со статьей; эвристика присоединения соседей подтягивает их из соображения, что соседний длинный контент с низкой ссылочностью, скорее всего, относится к истории. Часто так и есть. Но в этих тестах это промо. trafilatura строже относится к тому, что она добавляет, и пропустила только один из таких блоков.
Можно ли доверять показателям точности у goose3 и jusText? Только вместе с количеством примеров. Обе библиотеки считались только на 10 из 11 тестов, где одновременно есть article и boilerplate, потому что на одном из них они не вернули ничего, а пример без вывода не участвует ни в числителе, ни в знаменателе. Точность goose3 на уровне 1.0000 реальна для тех страниц, где он ответил; полнота 0.8243 по всем 22 тестам — это вторая половина того же факта.
Имеет ли порог длины у jusText значение на реальных страницах?
Это полностью зависит от длины ваших абзацев. Новостная статья с абзацами по 300 символов пересечет length_high уже на первом и будет работать нормально — именно поэтому jusText и вернул 1 190 чистых символов на реальной странице в дефолтной конфигурации. А страница с короткими абзацами, пунктами списка или короткими описаниями товаров может никогда не пересечь этот порог, и тогда jusText вернет пустую строку вместо частичного ответа. Лучше задавать его явно, чем выяснять это уже в проде.
Что здесь не тестировалось? Вообще реальные страницы. Многоязычное извлечение, хотя именно списки стоп-слов у jusText — его главное преимущество. Память под нагрузкой. Любые страницы, которые не являются статьями — без карточек товаров, без выдачи поиска, без дашбордов. Краевые случаи кодировки. И еще: экосистемы Node и Python сравнивались по поведению библиотек, а не по производительности рантайма, так что миллисекунды между ними стоит читать как порядок величины, а не как точное соотношение.


