最も長い段落が151文字しかない文書では、jusText をデフォルト設定で実行すると 0文字 しか返ってきません。途中まで抜けるのではなく、まるごと空文字列です。閾値を1つ下げて、ちょうど1つの段落だけがその基準を超えるようにすると、同じ文書でも 832文字 が返ってきます。さらに下げても、結果はやはり832文字のままです。
このテストケースでは、挙動はなだらかな坂ではなく、断崖のようにガラッと変わります。デフォルトが適切かどうかは、対象コーパスにおける段落長とボイラープレートの分布しだいです。
jusText とは何か、なぜ語彙密度が重要なのか
この比較対象の中でも jusText は、言語ごとのストップワード密度 にかなり強く依存しています。the、and、of、was のような機能語が多い塊ほど、本文っぽいと判断されやすい仕組みです。ただし、この語彙シグナルだけで見ているわけではありません。段落長、リンク密度、HTML 由来のブロック境界、見出しとの距離、隣接ブロックのクラス、さらに文脈を踏まえた後処理も結果に効いてきます。
公式リポジトリ: jusText's official repository。

この分類器は言語別ストップリストを使うため、jusText には 100言語分 が同梱されています。今回の比較でいちばん際立つのは、この明示的な「言語依存の語彙面」ですが、多言語での品質そのものは検証していません。
検証したバージョン: 3.0.2、BSD 2-Clause、GitHub スター 822件。Python 3.14.2。
断崖を測る

jusText の分類器は2段階で動きます。文脈を見ない第1段階で、各段落を good、bad、short、neargood のいずれかに振り分けます。続く文脈依存の第2段階では、neargood を good に昇格させますが、それはすでに good になっているブロックの隣にある場合だけです。さらに、段落が単独で good を取れるのは length_high を超えたときだけで、既定値は 200文字 です。
200文字を超える段落が1つもない文書では、昇格の起点がないため、neargood も最終的には全部ボイラープレート扱いになります。結果としてページ全体が空で返ってきます。
最長段落が151文字のテストケースで、閾値を動かしてみました。
length_high | good と判定された段落数 | 返された文字数 |
|---|---|---|
| 200 (既定) | 0 | 0 |
| 150 | 8 | 832 |
| 120 | 8 | 832 |
| 100 | 8 | 832 |
| 80 | 8 | 832 |
justext-length-threshold.json。
この閾値テストでは、1つの段落が基準を超えただけで対象の8段落すべてが出力され、それ以上ゆるめても増えませんでした。この推移は、既存の good ブロックの周囲にある条件を満たした neargood を文脈処理が昇格させる挙動と整合していますが、どのページでも隣接ブロックが無条件に昇格するという意味ではありません。
length_high の影響だと断定する前に、length_low を4通り、max_link_density を2通りに振って確認しました。8通りの組み合わせすべてで結果は0。つまり、このテストケースでは、これら2つの設定を変えても出力は復活しませんでした。
22件のテストケース全体で見ると、この傾向は一貫しています。length_high=200 では 22件中2件、150では 22件中9件、120では 22件中15件 が出力を返しました。
ここで述べていることを2点だけはっきりさせておきます。まず、jusText の抽出精度が低いと言っているわけではありません。実際の自然言語ページでは、デフォルト設定でも 1,190文字 のきれいな記事本文を返しています。というのも、実際のニュース本文なら、1つ目の段落の時点で200文字を超えることが多いからです。次に、デフォルトが間違っていると言っているわけでもありません。デフォルトが「長い段落を前提にしている」だけで、実行前に自分のコーパスがその前提に合うかを見極める必要がある、という話です。
このテストセットのもう一方: デフォルトでのリークが多い
このラベル付きテストセットでは、デフォルト設定の jusText が最も多くボイラープレートを漏らしました。
| ライブラリ | 記事再現率(全22件) | ボイラープレート漏出 | 本文トークン精度 | 混入トークン数 |
|---|---|---|---|---|
| Readability | 1.0000 | 0.2353 | 0.9109 | 35 |
| trafilatura | 0.9865 | 0.0588 | 0.9411 | 4 |
| newspaper4k | 0.9865 | 0.0000 | 0.9452 | 0 |
| resiliparse | 0.9054 | 0.0588 | 0.9381 | 7 |
| jusText | 0.8378 | 0.4706 | 0.8760 | 74 |
| goose3 | 0.8243 | 0.0000 | 1.0000 | 0 |
sixway-scores.json。1つのテストセット、1つの採点器。各単位には一意のセントネルが付いているため、回収判定は正確な部分文字列一致です。

47%のボイラープレート漏出と74件の混入トークン。このラベル付き合成セットでは、Readability の2倍の漏出率、2倍を超える混入量です。ただしこれはデフォルト設定での診断結果であって、製品全体の安定した順位づけではありません。
観測されたリークは、さっきの断崖と同じ文脈パスと整合しています。周囲のブロッククラスや距離条件が許せば、条件を満たした neargood ブロックは昇格されます。そのため、記事本文の横にある説明文やコメントのような塊が境界を越えることがあります。ここでのテスト結果は、どのラベル付きブロックが漏れたかを示しているだけで、単純化した仕組み自体は jusText の厳密な文脈ルールに沿って条件付きで動いています。
再現率は 0.8378 で、下から3番目です。失われたのはすべて閾値の問題で、129文字しかない記事では何も回収できず、短い段落が10個並ぶページでも回収できず、ほぼ空の文書でも同じでした。
言語ストップリストの内訳
他の99個のストップリスト。
justext.get_stoplists() は 100言語 を返します。分類器は英語のヒューリスティックを翻訳したものではなく、最初から言語パラメータ前提で設計されています。言語の切り替えは引数1つで済みます。
import justext
paragraphs = justext.justext(html, justext.get_stoplist("Czech"))
text = "\n".join(p.text for p in paragraphs if not p.is_boilerplate)
trafilatura や goose3 も言語に関する挙動は持っていますが、このレビューではどの抽出器も英語以外の正解データでは採点していません。jusText に同梱されている100個のストップリストは、多言語評価の有力候補であることを示しますが、一覧があるだけでそれらの言語での抽出品質までは証明できませんし、競合製品がそれらをうまく扱えていないことも意味しません。
API は2つの関数と9つの調整可能な定数で構成されています。length_low 70、length_high 200、stopwords_low 0.30、stopwords_high 0.32、max_link_density 0.20、max_heading_distance 200、そしてエンコーディング処理です。小さく、読みやすく、署名を見ればだいたい全部分かります。
インストールと速度
pip install justext で入るのは 3パッケージ だけで、この比較では最少です。容量は 22.4 MiB、所要時間は2秒未満です。
公式参照: jusText on PyPI。
| ライブラリ | パッケージ数 | 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 |
install-and-import.json。各ライブラリはそれぞれ別の空の virtualenv で計測。
3パッケージで22.4 MiBというのは、この比較ではかなり軽い依存関係ですし、中央値0.56msの抽出速度も、このハーネスでは trafilatura の0.51msにかなり近い値です。ただし環境計測ではファイル種別ごとの容量内訳までは取っていないため、ディスク使用量のうちどれだけがストップリスト由来かは分かりません。
保守性の問いに、慎重に答える
ここで使う日付ベースの保守指標は、リポジトリへの最後の push と PyPI の最後のリリースで、どちらも 2025-02-25 です。これはテストの17か月前でした。リリースは合計8回、フォーク91、オープン issue は9件、アーカイブ済みではありません。
PyPI の分類には Python 3.9 までしか書かれていませんでしたが、実際には 3.14.2 で動かせて、0.777秒でインポートし、22件中19件のテストケースを例外なく抽出しました。
つまり、メタデータは実態よりPython 5世代分古く、現実にはちゃんと動いています。この差は大事です。静かなリポジトリは、サポート状況 を示すサインであって、すぐに 機能不全 を意味するわけではありません。公開済みの2011年の手法と単語リストの集合だけで成り立つライブラリなら、「完成している」状態は十分ありえます。止め方の回避策みたいに、ストップリストは腐りません。
ただし、静かなリポジトリが意味することもあります。もしバグに当たったら、自分で直すか fork することになる、ということです。未解決 issue が9件しかない点を見ても、未処理の問題が山積みという状況ではありません。
メモリ、そして壊れたHTMLが与える影響
ここでは、2つの運用上の問いを別々に測っています。
より広いストレステストの文脈は、10ライブラリのメモリと不正HTML比較 にあります。
ピーク常駐メモリ は /usr/bin/time -l で測定しました。各セルごとに新しいプロセスを1つ起動しています。インポート時の下限値はライブラリを読み込んで待機しているだけでかかるコスト、ピーク値には文書本体も含まれます。
| ライブラリ | 実行環境 | インポート下限 | 226 KB ピーク | 10 MB ピーク |
|---|---|---|---|---|
| html2text | python3.14 | 18.7 | 19.9 | 71.2 |
| pyquery | python3.14 | 30.3 | 33.9 | 172.5 |
| resiliparse | python3.14 | 20.5 | 25.1 | 225.1 |
| markdownify | python3.14 | 23.9 | 28.9 | 278.5 |
| goose3 | python3.14 | 44.1 | 52.4 | 398.5 |
| cheerio | node22 | 66.8 | 76.5 | 398.5 |
| justext | python3.14 | 30.3 | 36.6 | 431.2 |
| newspaper4k | python3.14 | 52.6 | 61.8 | 668.5 |
| trafilatura | python3.14 | 52.5 | 64.8 | 927.1 |
| turndown | node22 | 47.8 | 68.4 | 2947.1 |
memory-results.json。Python と Node のベースラインは互いに比較できません。どちらにもインタプリタ自体が含まれているからです。
jusText のインポート下限は30.3 MiB、10MBのこのテストケースでのピークは431.2 MiBでした。これはインポート下限の約14.2倍で、入力サイズに対しては RSS ベースで43.1倍です。1つのテストケースと1プロセスだけでは一般的なスケーリング曲線は示せませんし、Python と Node のベースラインもやはり比較不能です。
不正HTML。 12個の文書はそれぞれ1つだけ壊しています。未閉じタグ、誤ったネストのインライン要素、スペースを含む引用なし属性、余計な閉じタグ、<html> がまったくないもの、属性の重複、タグの途中で切れた文書、壊れたエンティティ、閉じられていない <script>、嘘の charset 宣言、マークアップを含むコメント、そして600階層のネストです。加えて、2つの正常なコントロール を同じサイズで用意しました。なぜなら、「何も返さなかった」という事実は、同じサイズの正常文書でも無言なら、不正性について何も語れないからです。
justext は 14件中0件 で例外を投げず、13件 で空を返しました。壊れたケースでのセントネル回収は 0/33 です (malformed-results.json)。正常な短いコントロールも0文字で、正常な長いコントロールだけが 1,333文字を返しました。12件の不正テストはすべて空のままで、未閉じ <script> のケースはセントネル生存率の採点対象から外しています。したがって、この結果はパーサーの耐性、つまり例外を起こさないことは示していますが、無言の原因が不正HTMLなのか、それとも既に測定済みのサイズ閾値なのかは切り分けられません。
長所と短所
良い点。 100個の言語ストップリストがあり、しかもそれを翻訳するのではなく本当にその上に構築された分類器であること。比較対象の中で依存パッケージ数が最少の3個。中央値0.56msの抽出速度。9個の文書化された、読みやすい調整定数。BSD 2-Clause。PyPI の分類は 3.9 で止まっているのに、Python 3.14 でも問題なく動くこと。
気になる点。 この合成テストでデフォルト時のボイラープレート漏出が最も多く、47%で混入トークンは74件。短い段落のテストケースでは、どの段落も length_high を超えなかったため空文字列を返しました。再現率はこのセットで0.8378。最後に記録されたリポジトリ更新とリリースはテストの17か月前で、保守主体には注意が必要です。
どんな人に向き、どんな人には向かないか
jusText を評価すべきなのは、コーパスが多言語である場合、あるいは段落長の分布が明示的な閾値調整に向いている場合です。100個のストップリストは確かに特徴的ですが、多言語での精度自体は今回測っていません。導入先に3パッケージの依存関係と明示的な閾値制御が合う場合にも候補になります。
避けたほうがいいのは、1トークン単位でモデルに入力する用途です。74件の混入トークンは、goose3 や newspaper4k の0と比べるとそのままコストになります。また、商品説明、一覧ページ、更新履歴、FAQ のような短い段落のページにも向きません。length_high を意図的に調整していない限りは避けるべきです。さらに、コンプライアンスや調達の理由で、現在も積極的に保守されている依存関係が必要なら、それも避ける理由になります。コードが動いていても、そこは現実的な制約です。
使うなら、デフォルトやパーセンタイルのルールをそのまま使うのではなく、ラベル付きの検証サンプルで length_high を調整してください。妥当な閾値を一通り試し、記事再現率とボイラープレート精度の両方を測るのが大切です。閾値を下げれば短文を拾える一方で、不要な隣接ブロックまで昇格させることもあります。
マネージドAPIが入る場所
jusText は、この比較にある他のライブラリと同じく、すでに手元にある HTML を処理します。どれもページの取得はせず、JavaScript のレンダリングもしませんし、アンチボット層も扱いません。
同じ6つの抽出器での比較は、6ライブラリの抽出比較 をご覧ください。
自社の Thunderbit を含むホスト型の取得・レンダリング・抽出サービスは、別の責務境界を担当します。Thunderbit はここではベンチマークしていません。つまり、ここでの話は「渡されたHTMLを本文として分類する」か、それとも「URLを取りに行って処理するサービス」かの違いです。この文章では、同一指標での品質比較まではしていません。
正直に言うと、jusText の多言語ストップリストは、無料で使えるかなり実力ある機能です。もし問題が「多言語ページを取得すること」なのであって、「多言語ページの本文を分類すること」ではないなら、それは別の買い物になります。
より広い視点では、web scraping API roundup でホスト型の選択肢を、open-source scraper pillar でセルフホスト型の選択肢を紹介しています。出力先がモデルなら、HTML を Python で Markdown に変換する方法 が、いちばん fidelity を失いやすい工程です。
jusText を使うべきか?
コーパスが多言語である、または段落長の分布が明示的な閾値調整に向いているなら候補になります。導入前に、その両方を検証してください。
同梱されている100個のストップリストは実際の設計上の強みですが、多言語精度は今回未検証です。閾値の断崖は調整可能であり、低めの設定が許容できるかどうかは、ラベル付きサンプルで測った再現率とボイラープレート精度しだいです。
この英語の合成セットでは、デフォルト時の newspaper4k がラベル付きボイラープレートを0件漏らし、記事単位の0.9865を回収しました。今回のワークロードでは有力な比較候補ですが、万能な置き換え先という意味ではありません。
Web データ抽出に Thunderbit を試す Get Started Free
FAQ
なぜ jusText は部分抽出ではなく空文字列を返すのですか?
分類器が2段階だからです。段落が単独で good を得るのは length_high(既定200文字)を超えた場合だけで、第2段階がその周囲の neargood ブロックを昇格させます。閾値を超える段落が1つもなければ起点がなく、候補はすべてボイラープレートに落ちるため、結果は空になります。これは部分的な答えを見つけられなかった失敗ではなく、設計上のオール・オア・ナッシングです。
jusText は放置されていますか? 最後のリポジトリ更新と最後の PyPI リリースはいずれも2025-02-25で、テストの17か月前でした。PyPI の分類も Python 3.9 で止まっています。ただし、Python 3.14.2 では例外なくインストール・実行でき、オープン issue が9件というのも大きな未処理山積みではありません。日付は壊れている証拠ではなく、保守リスクとして読むべきです。実務上のリスクは、自分の不具合を自分で直す必要があるかもしれない、という点です。
なぜ Readability よりボイラープレートを多く漏らすのですか? このテストセットでは、デフォルトの文脈ルールの下で、本文っぽい隣接ブロックが出力境界を越えました。昇格はすべての隣接ブロックで自動ではなく、ブロックのクラス、距離、文脈に依存します。観測結果は、ボイラープレート単位で47%の漏出、混入トークン74件でした。
別の言語ではどう使いますか?
justext.justext(html, justext.get_stoplist("German")) です。get_stoplists() は利用可能な100言語を返します。ストップリストは、段落長、リンク密度、HTML由来の分割、見出し距離、隣接ブロックの文脈も使う分類器に対する、言語固有の語彙入力です。つまり言語入力は変わりますが、それだけでドイツ語の抽出品質が保証されるわけではありません。
ここで何が未検証でしたか?
現実のページは一切扱っていません。これはラベル付き単位を持つ制御済みのテストケースです。ライブラリの主な売りである多言語対応は、100個のストップリストとしては列挙しましたが、非英語テキストで採点していません。encoding、default_encoding、enc_errors を公開しているにもかかわらず、エンコーディングの境界ケースも未検証です。また、max_heading_distance と2つのストップワード比率の閾値は、通して既定値のままでした。


