Mozilla Readability レビュー:完全な本文回収、隣接要素の混入、予測器の偽陰性

最終更新日 August 20, 2026
Mozilla Readability レビュー:完全な本文回収、隣接要素の混入、予測器の偽陰性
AI要約
Mozilla の Readability は、Firefox の Reader View を支える抽出エンジンを単体の JavaScript に移植したものです。Apache-2.0 のパッケージ @mozilla/readability として公開されており、ライブ DOM から記事本文を選び出します。そのため Node 上で使うには、jsdom のような DOM 実装が必要です。ページ取得、JavaScript 実行、スキーマ抽出は行いません。検証した版は npm の latest 0.6.0 で、2025 年 3 月 3 日公開、2026 年 7 月 27 日確認時点でリポジトリのスター数は 11,361、最終 push は 2026 年 7 月 9 日でした。つまり main は公開済みパッケージよりかなり先行しています。

Mozilla の Readabilityは、Firefox の Reader Viewを支えている抽出エンジンを、単独のJavaScriptライブラリとして切り出したものです。Apache-2.0のパッケージ@mozilla/readabilityとして公開されており、ライブなDOMから記事本文を選び出します。そのためNode上で使う場合はjsdomのようなDOM実装が必要になります。ページの取得、JavaScriptの実行、スキーマ抽出は行いません。

今回検証したのはnpmのlatestバージョンである0.6.0です。2025年3月3日に公開され、2026年7月27日に確認した時点でリポジトリのスター数は11,361、最終pushは2026年7月9日でした。つまりmainブランチは公開済みパッケージよりかなり先に進んでいます。私はjsdom 29.1.1Node v22.22.3、macOS arm64環境で、このために作成したラベル付きHTMLフィクスチャ22件に対して実行しました。ここに載せている数値はすべてこのセットアップに基づくものです。実際に触ってみると、このカテゴリの中では最も扱いやすいツールです。インストールは2分で終わり、バイナリも不要、ブラウザをキャッシュに抱える必要もなく、何度実行しても結果は同じです。面白いのは運用のしやすさそのものではなく、失敗のパターンがソースコード中のわずかな定数から予測できてしまう点、そしてそのうちの一つの定数がドキュメントの説明以上に大きく効いてくる点です。

22件の制御された合成フィクスチャ全体で、Readabilityはラベル付けされた記事ブロック74個をすべて回収しました。ただしこの結果には上限があり、記事本文を絶対に取りこぼさないという意味ではありません。公開されている実ページベンチマークではrecallが0.982で、既知の失敗パターンもここでは再現しませんでした。今回最も目立った失敗は、ソースコード上のリンク密度ゲート0.25によって、余分な隣接コンテンツが一緒に取り込まれてしまうケースでした。この見かけ上の深刻さは、テスト環境によって変わります。

readability.jsは実際には何で、何ではないのか

ReadabilityはDOMに対するルールベースのスコアリング処理です。候補となる要素を巡回してそれぞれにコンテンツスコアを付け、そのスコアを祖先要素に伝播させ、最もスコアの高いサブツリーを選んだうえで、ページの装飾に見えるものを取り除くクリーンアップを行います。記事抽出の戦略はこれがすべてです。モデルも、学習データも、サイトごとのルールもありません。だからこそ初めて見るページでもきちんと動きますし、失敗がソースコードから予測できるのもこの設計のおかげです。ここが面白いところです。

誤解されがちな点が3つあります。

  • フェッチャーではありません。 受け取るのはURLではなくdocumentです。取得、リトライ、ボット対策、ヘッダーの扱いはすべて自分で用意する必要があります。
  • レンダラーではありません。 JavaScriptは実行しません。渡したDOMに含まれているものだけを見ます。
  • 構造化抽出器ではありません。 得られるのはtitlebylineexcerptcontent(HTML)、textContentlengthsiteNameです。スキーマも、型付きの行も、{name, price}のような形式もありません。

実際の処理をほぼ決めている4つの定数

System diagram: Four constants do most of the work

node_modulesの中のReadability.jsを読むと、どんなドキュメントよりも実際の挙動が見えてきます。ライブラリの動作のほとんどは、次の4つの仕組みで説明できます。

  • 段落ごとのコンテンツスコア: 1 + (commaCount + 1) + min(floor(len / 100), 3)で計算されます。25文字未満の段落はそもそもカウントされません。スコアは区切りごとに祖先へ伝播し、親には全体がそのまま渡り、祖父母には半分、それより上の祖先にはlevel · 3が渡ります。
  • DEFAULT_CHAR_THRESHOLD = 500 — 「成功」と判定するパースの最小記事長です。これを下回ると、クリーンアップの処理を減らして取得をやり直すふるいが動きます。
  • unlikelyCandidatesの正規表現commentfootermenurelatedsidebarsocialsponsorといったclassやidの部分文字列にマッチします。マッチしたノードはスコアリング前に除外されます。
  • grabArticle内のsibling追加ゲート — トップ候補が決まったあと、その*隣接要素(sibling)*を含めるかどうかを判定します。sibling自体のスコアがしきい値を超えている場合、あるいはnodeLength > 80 && linkDensity < 0.25の場合、あるいはnodeLength < 80 && nodeLength > 0 && linkDensity === 0 && ピリオドを含む場合に、一緒に取り込まれます。

リンク密度はΣ(linkText.length · coef) / textLengthで計算され、#だけのhrefはcoef = 0.3、それ以外は1です。このゲートが、今回計測されたsiblingの混入を説明していますが、これが抽出アルゴリズムのすべてというわけではありません。

セットアップと、宣伝文句には出てこない依存関係

npm install @mozilla/readability jsdomを実行すればすぐに動きます。2分、バイナリなし、インストール後の追加ダウンロードなし、ブラウザをキャッシュに置く必要もなし。インストールという軸で見れば、ほぼ理想的です。

ただし「依存関係ゼロ」という主張はアルゴリズムについてのものであり、実行環境についてのものではありません。Readabilityはライブなdocumentを対象に動作するため、Node上ではDOM実装を自分で用意する必要があります。今回はjsdom 29.1.1を使いました。jsdomは決して軽くはなく、多くのパイプラインでは抽出そのものよりもループ内の主要なコストになります。そこは見込んでおく必要があります。

もう一つ、再実行の原因になった落とし穴があります。Readability.parse()は渡されたDOMを破壊的に書き換えます。 同じjsdomのdocumentを2回parseすると、2回目は1回目ですでに壊された後のdocumentを見ることになります。私のテストハーネスでは、parseのたびに新しいjsdomを作り直しています。もしページをループしながらdocumentオブジェクトを使い回して時間を節約しようとしているなら、それは後で踏むバグです。

どうやってテストしたか

実際のニュースサイトには向けていません。ライブページではスコアは見えても、なぜそうなったのかはほとんど見えません。ヒューリスティックにおいては「なぜ」こそが価値の大半を占めます。そこで私は、91個のラベル付きブロック(記事74個、定型コンテンツ17個)を含む22件のHTMLフィクスチャを生成しました。各ブロック内のすべての単語には、そのブロック固有の識別子文字列を付けてあります。ブロック間で語彙が重ならないため、抽出されたトークンは必ず一つのブロックに対応し、「回収された」か「漏れた」かは曖昧なマッチではなく厳密な所属判定になります。

抽出のステップと採点のステップは意図的に分離しています。Nodeのランナーが出力するのは、生の抽出テキストisProbablyReaderableのtrue/false、そして計測されたリンク密度だけです。precisionとrecallは、別のスクリプトがその生テキストをラベルと突き合わせて後から計算します。ハーネスのどこにも手書きの指標定数は入っていません。それが自分の数値を信頼するための唯一の方法です。

そのうえで、まったく同じバイト列をtrafilatura 2.1.0にも流し込み、同一テストベッドでの比較を行いました。各フィクスチャは3回ずつパースし、22件すべてで毎回バイト単位で同一のテキストが返りました。

要約表だけを信じる必要はなく、各段階を個別に確認できます。tests/build_fixtures.mjsが注釈付きHTMLと正解データを作成し、tests/run_readability.mjsが抽出結果と予測器の出力を記録し、tests/metrics.pyがそれらの記録を後から採点します。生のReadability出力、計算済みの指標、同一入力での比較結果はartifacts/raw/に残してあります。この分離は、結果が怪しく見えたときに重要になります。パーサーが想定外のテキストを返したのか、ラベルセットが間違っていたのか、採点コードが誤分類したのかを切り分けられるからです。このパッケージで再現できるのはここに書いた主張の確認までであり、実運用のページの混在で同じ失敗分布になるという証拠ではありません。

範囲の限界ははっきりしていて、しかも重要です。ここにあるのは合成された制御ページであって、実データのコーパスではありません。 公開されている実ページの指標はarticle-extraction-benchmarkが基準です。そこではreadability_js 0.6.0(今回テストしたのと同じバージョン)が、約181件の実ページに対して**word-F1 0.947 ± 0.005(precision 0.914 ± 0.008、recall 0.982 ± 0.003)**を記録しています。これは引用しただけで、自分で再計測したものではありません。

ここでは現行リリースのベンチマーク行を使い、置き換えられた過去の行は除外しています。 制御されたフィクスチャは公開の実ページコーパスを置き換えるものではなく、どのコンテンツ形状がどのルールを発動させるかをブロック単位で分解して見せるためのものです。

合成フィクスチャパックではrecallが完璧だった

74件中74件です。22件のフィクスチャすべてで、Readabilityはラベル付けされた記事ブロックを一つも取りこぼしませんでした。記事と定型コンテンツが混在する11件のクリーンな合成フィクスチャでは、マイクロ平均のトークンrecallが1.000でした。記事の文が一文たりとも欠けませんでした。

ここには2つの留保が付きます。

これらはクリーンな単一カラムの合成ページです。実際の記事はもっと深くネストし、本文の途中に広告が挟まり、時にはスコアリングの副作用で冒頭段落が失われることもあります。この種の欠落はトラッカーに報告されています(#437#901、表の直前で落ちるケースの#922)。私のフィクスチャではこれらのどれも発動しませんでした。 ですので、これらが修正済みだと主張しているわけではなく、私のテストがそこまで届かなかったというだけです。実ページでは、このバージョンのベンチマークrecallは1.000ではなく0.982です。

それでも、結果の方向性自体は有用です。Readabilityの問題は記事を丸ごと捨ててしまうことではありません。一緒に持ってきてしまうもののほうです。

precisionの数値と、なぜ3つのラベルが必要なのか

Measured results chart: Three scopes behind the precision story

数値は一つだけ引用するのは簡単ですが、それを擁護するのは難しいものです。11件の混在フィクスチャでは、Readabilityは17個の定型コンテンツブロックのうち5個を保持しました。leak rateにすると0.294です。

しかしこれは現実世界でのleak rateではありません。3つの異なるセットアップはそれぞれ異なるものを測定しており、そのうち平均的なページを表しているのは一つだけです。

何を測定しているか結果
意図的に不利に設計したフィクスチャ集合 — 11件の混在ページのうち6件はsiblingゲートを突破するように作り込んだもの17個中5個の定型コンテンツブロックを保持(0.294)
現実的な1ページ<article>本文の周りにnav、広告バナー、サイドバー、コメント、フッター、さらに中立的なclass名のプロモ1個6ブロック中5ブロックを除去、1ブロックを保持
約181件の実ページ、公開ベンチマーク(今回の実行結果ではない)precision 0.914、recall 0.982、word-F1 0.947readability_js 0.6.0

1行目は予測ではなく、ストレステストとして読むべきです。Readabilityが実際の環境で定型コンテンツの29%を漏らすわけではありません。 現実的なページでは、unlikelyCandidatesの正規表現にマッチするクラスを持つnav-menuad-bannersidebarcommentssite-footerはすべてきれいに除去され、5つとも取り除かれました。唯一残った1つは、私がその正規表現を意図的にすり抜けるように作ったブロックです。

0.25というゲート:定型コンテンツの除去が止まる境界

sibling追加のルールはソースコードに明記されています。ただ、それが正確にどこで反転するのかを測定した例は、私が調べた限り見当たりませんでした。そこでグラデーションを作りました。<article>の外に置いた中立的なclass名の<p class="teaser-block">、トップ候補として確実に勝つ4段落構成の明確な記事、そして変化させるのはプロモ部分の長さとリンク密度だけです。密度はReadability自身の計算式を使い、想定ではなく実行時に実測しています。

プロモブロック本文の長さ80文字超実測リンク密度結果
リンクなし126yes0.000保持
短いリンク1個126yes0.143保持
やや長いリンク1個126yes0.278除外
文の半分がリンク126yes0.476除外
1文、ピリオドで終わる60no0.000保持
同じ文、ピリオドなし59no0.000除外

ソースコード上の条件は0.25をしきい値としています。実測したサンプルはその両側にあり、0.143は保持、0.278は除外でした。別の分岐では、ピリオドで終わる60文字の文は保持され、ピリオドのない59文字版は除外されました。記事のrecallはどのパターンでも4/4のままだったので、これらのサンプルはprecisionへの影響だけを切り分けて示しています。

テストハーネスの外側で見ると、このゲートは実質的にこう言っています。記事の隣にある、長くてリンクの少ない中立的な文章は記事であると。しかしこれは記事ではないものの多くにも当てはまってしまいます。段落形式で書かれた「関連記事」の紹介文、ニュースレターの案内、編集者からの注記、あるいはトラッキング用のリンクを取り除いて完全な文章で書かれたスポンサー付きのティザーなどです。

RAGのインデックスでは、リンクの少ないプロモ段落が抽出されたチャンクとして扱われ、検索や生成がそれを記事本文として扱ってしまうリスクが生まれます。ソースコードのルールはこの失敗モードを十分に現実的なものにしていますが、このレビューでは検索やモデルによる引用のエンドツーエンド評価までは行っていません。

サイト固有の厳密なフィルターを作るなら、既知のソースDOMコンテナを事前に除外する、シリアライズ前にソースノードの祖先関係を保持して比較する、あるいは慎重に検証したテキストパターンフィルターを後段で適用する、といった方法が考えられます。返されたHTMLだけでは、そのノードが元々primaryコンテナの外にあったかどうかを保証できない場合があります。siblingゲートは公開されているオプションでは調整できません。

フィクスチャが覆した3つの前提

フィクスチャは、charThresholdが短い記事を弾く、セマンティックタグが必須である、短い非本文コンテンツは落ちる、という3つの前提を覆しました。以下の証拠がその核心であり、事前登録のような手続きは必要ありません。

charThreshold = 500は崖ではない

よくある理解では、500文字未満の記事はnullを返すとされています。実際にはそうではありません。本文の長さを120文字から1500文字まで変化させ、charThresholdの値を200、500、1000で確認しました。

本文の長さすべてのしきい値でparseが成功抽出後の長さ
120yes161
300yes342
460yes509
520yes569
800yes841
1500yes1555

結果はフラットでした。本文の長さがどうであれ、3つのしきい値設定すべてで抽出後の長さは同一でした。このしきい値は戻り値の有無を制御しているのではなく、クリーンアップのフラグを外して取得をやり直すかどうかを決めているだけです。クリーンなページでは削るものが何もないので、どちらの経路をたどっても結果は同じになります。本当のnullの境界は「抽出可能なテキストがまったく存在しない」場合です。

そして、ここで実際に見つかった失敗は、偽のnullよりもやっかいなものでした。ほぼ空のページ、つまりnavバーと4語だけの説明文を入力したところ、成功として返ってきて、その「記事」にはnavが含まれていました。 本当の記事がない場合、Readabilityは定型コンテンツを記事として返してしまいます。大規模にクロールしていて、非nullの結果を「このページにはコンテンツがあった」という意味で扱っているなら、その前提は間違っています。

セマンティックタグが本質を決めているわけではない

構造を取り除けばrecallが下がるだろうと予想していました。同じ記事本文に対して、2つの見た目を用意しました。一つは<main><article><h1>に説明的なclass名が付いたもの、もう一つは<div class="x1">と、段落もただの<div>で構成したものです。結果はどちらも記事ブロック4/4を回収し、定型コンテンツの混入はゼロでした。記事がページ内で明らかに最も密度の高いテキストブロックである場合、長さとカンマの数によるスコアリングだけで、セマンティックな手がかりがなくても見つけ出せます。「Readabilityには<article>タグが必要」というのは俗説です。

この主張には正直な限界もあります。今回のページには明確なコンテンツブロックが一つしかありませんでした。セマンティクスが本当に効いてくるのは、密度の高いサブツリーが2つ競合するページであり、そのタイブレークは今回テストしていません。

非本文コンテンツはそのまま生き残る

「25文字未満の段落はカウントしない」というルールから、表やキャプションは失われるだろうと予想していました。これも間違いでした。このルールが影響するのは候補のスコアリングであって、保持ではありません。コンテナが勝てば、その中身はすべて一緒についてきます。

記事内のコンテンツ種別Readabilitytrafilatura
本文段落(×2)保持保持
データ表のセル(×2)保持保持
<pre>コードブロック保持保持
25文字未満の1行<p>(×2)保持保持
<figcaption>保持除外
合計8/87/8

ここでは、より積極的にクリーンアップするツールのほうが分が悪くなっています。ドキュメント、チュートリアル、あるいはコードブロックやキャプション付きの図を含むページを扱うなら、Readabilityの「勝ったサブツリーを丸ごと残す」という挙動はむしろ利点になります。

parse()が成功してもisProbablyReaderableはfalseを返す

System diagram: isProbablyReaderable says no when parse() says yes

READMEでは、本格的なparseに進む前の軽い事前チェックとしてisProbablyReaderable(doc)を呼ぶことを勧めています。私のテストでは、このゲートが3種類のページ形状をfalseと判定した一方で、その後のparse()は問題なく成功しました。

ページの形状予測器の判定parse()どのレバーで直るか
コンテンツが<li>要素だけにある場合false成功なしminScore 1〜80、minContentLength 40〜200のどの組み合わせでもfalseのまま
140文字未満の段落が10個ある場合false成功minContentLength ≤ 100(minScoreは無効)
408文字の段落が1個だけの場合false成功minScore ≤ 10(スコアは約16.4)
通常の記事(対照群)true成功

この3つの失敗はそれぞれ原因が異なり、調整で直せるのは2つだけです。<li>のケースは構造上の問題です。予測器がスコアを付けるのはpprearticleノード(およびdiv > brの親)だけなので、コンテンツがリスト項目の中にあるページは何にもマッチせずスコアが0になり、しきい値をどう調整しても救えません。 この形状はすでにissue #662として報告されています。短い段落が多いケースはminContentLengthゲートの問題で、各段落がスコアリングの前に弾かれてしまうため、内容のある段落が10個あっても合計はゼロになります。この値を下げれば直り、minScoreを変えても効果はありません。単一の長い段落のケースは単純な算数の問題です。スコアはsqrt(408 − 140) ≈ 16.4で、デフォルトのminScoreである20を下回っています。単独の段落が自力でしきい値を超えるには140 + 20² = 540文字が必要です。

READMEも予測器がfalse negativeを出す可能性があると警告しています。私が付け加えるとすれば、実務的なルールとしてこれを唯一の関門にしないことです。そのページが重要なら、parseして結果の長さを確認してください。すでにjsdomの構築コストを払っているなら、parse自体はそれほど高くつきません。

同じバイト列、2つの抽出器

同一のフィクスチャに対してtrafilatura 2.1.0を実行すると、異なる2つのテストベッドで測定した数値を並べるよりもずっとクリーンに比較できます。入力がバイト単位でまったく同じだからです。

指標(11件の混在フィクスチャ)@mozilla/readabilitytrafilatura
記事ブロックrecall1.0001.000
保持された定型コンテンツブロック数5/17 (0.294)1/17 (0.059)
Token F1(micro)0.9480.969
非本文recall8/87/8
ごく短い記事(120文字)のtoken F10.8000.571

このフィクスチャでは、どちらか一方が完全に優れているわけではありません。trafilaturaはsiblingブロックの保持を少なく抑え、Readabilityは短いコンテンツと非本文コンテンツをより多く保持しました。どちらのツールも、ラベル付けされていない見出しテキストのせいで絶対的なprecisionはやや低く出ているため、ブロック単位の漏れ件数のほうが素直な指標になります。公開されている実ページベンチマークでもword-F1の順序はたまたま似ていますが、コーパスも指標も異なるため、これはテストベッドをまたいだ検証にはなりません。

堅牢性についても一言触れておくと、意図的に壊した正規ページの双子バージョン(閉じられていない<p>、誤ってネストした<b>/<i>、余分な</div>)を流しても、recallは3/3で漏れゼロとなり、整形されたバージョンと変わりませんでした。これはReadability自体の手柄ではなく、Readabilityが見る前に壊れたHTMLを修復してくれるjsdomのHTML5ツリービルダーのおかげです。どのフィクスチャでもパーサーがクラッシュすることはありませんでした。

メリットとデメリット

メリット

  • 記事のrecallが強力な軸です。22件の合成フィクスチャでラベル付きブロック74/74を回収し、混在セットでのtoken recallは1.000でした。
  • 正規表現でクラス判定されるページの装飾要素を確実に除去します。現実的なページではnav、広告バナー、サイドバー、コメント、フッターがすべて消えました(6個中5個を除去)。
  • 非本文コンテンツを完全に保持します。表、<pre>コード、図のキャプション、25文字未満の行がすべて生き残り(8/8)、trafilaturaはキャプションを1つ落としました。
  • セマンティックマークアップに依存しません。中立化した<div>だけの記事も、<article>/<main>版とまったく同じスコアでした。
  • 短い記事を誤って弾くことがありません。クリーンなコンテンツは120文字まで回収でき、charThresholdが200/500/1000のどれでも結果は同じでした。
  • 完全に決定的です。22件すべてで、3回の実行結果が同一のテキストでした。
  • インストール2分、Apache-2.0、npm上のバージョンがそのままテストしたバージョン(0.6.0)なので、この記事の内容が古くなる心配はありません。

デメリット

  • sibling追加のゲートは悪用されやすい面があります。長く、リンクが少なく、中立的なクラス名を持つプロモ文章は記事本文と見分けがつかず、linkDensity < 0.25であればそのまま一緒に取り込まれます。
  • コンテンツの乏しいページでは、nullではなく定型コンテンツを記事として返してしまいます。ほぼ空のフィクスチャでは、navバーが本文として返ってきました。
  • isProbablyReaderableは3種類のページ形状でfalse negativeを出し、そのうち1つはどう調整しても直せません。
  • 実行時にDOMが一式必要です。「依存関係なし」という説明の裏にjsdomのコストが隠れていて、それがループ全体を支配します。
  • parse()は入力のdocumentを破壊的に書き換えるため、ページごとにDOMを作り直す必要があります。
  • 取得も、JavaScriptレンダリングも、構造化出力もありません。これはパイプラインの一段階であって、パイプライン全体ではありません。
  • トラッカーに報告されている実ページでの欠落(冒頭段落や表の直前で落ちるケース)は今回のフィクスチャでは再現しなかったため、それが稀なのか、単に私のページがそこまで到達しなかっただけなのかは分かりません。

誰が使うべきで、誰は避けるべきか

すでにHTMLを手にしていて、Nodeサービスの中で純粋なJavaScriptとして記事を取り出したい場合、しかもPythonの依存関係を足すのが面倒な環境であれば、Readabilityは有力な候補です。(タイミングを正式な分布として計測していないため、速度について言えるのは「抽出そのものよりjsdomの構築がループを支配する」という程度にとどまります。) リーダーモード機能、オフラインでの記事保存、メールニュースレター、「クリーン表示」ボタン、ブラウザ拡張機能、コードブロックやキャプションを含むドキュメント処理パイプライン——これがReadabilityの得意な領域であり、recallの数値もそれを裏付けています。挙動がソースコードから素直に読み取れる点も、なぜ特定のブロックが残ったのかを同僚に説明する必要があるときに、見た目以上に価値があります。

一方で、定型コンテンツ除去のprecisionが評価対象になっている場合、特に紛れ込んだプロモ段落が検索可能なチャンクになってしまうLLMインデックスに流し込む用途であれば避けたほうがよいでしょう。ページがクライアントサイドでレンダリングされる場合も避けてください。渡されたDOMをそのまま読むだけで、JavaScriptは実行しません。必要なのが文章ではなく{title, price, sku}のような構造化データであれば避けてください。どう設定しても、コンテンツ抽出器がスキーマ駆動のツールに変わることはありません。そして「そもそもこのページに記事が存在したのか」が重要な判断材料になる処理では、非nullの戻り値をそのまま答えとして信用しないでください。

代替ツールと、Thunderbitスタックにおける位置づけ

ここまでの内容は、Mozillaが維持している無料のApache-2.0ライブラリを貶めるものではありません。Readabilityはインフラそのものであり、何年もFirefoxの中で動き続けてきましたし、リーダーモード抽出のリファレンス実装であり続けているのには理由があります。より広い選択肢を知りたい方には、オープンソーススクレイパーの総まとめと、さらに広く調査したおすすめのウェブスクレイピングツールで継続的に比較をまとめています。

同じフィクスチャを6つの抽出ライブラリすべてで比較した結果は、抽出ライブラリ6種の比較を参照してください。

著者注:Thunderbitは、URLを渡すだけでレンダリングと抽出をまとめて任せられる、私たちが提供しているマネージドの選択肢です。今回のフィクスチャでは実行していないため、同等の品質を主張しているわけではありません。ここで重要な分かれ道は、すでにDOMを持っていてローカルで記事抽出をしたいのか、それとも取得・レンダリング・構造化をサービスとして任せたいのか、という点です。セルフホストであればベンダー利用料はかかりませんが、インフラと保守のコストは残ります。

正直なトレードオフはこうです。Readabilityは無料で、透明性が高く、自分の手で動かせます。出力を決めている正確なゲートをソースコードで読める、というのはマネージドAPIではなかなか得られない利点です。マネージドスタックには費用がかかり、内部の仕組みは見えなくなりますが、その代わり自分で組み上げるはずだった取得・レンダリング・構造化の各段階を肩代わりしてくれます。そのスペクトラムのAI寄りの端を知りたい方には、AIであらゆるウェブサイトをスクレイピングする方法AIクローラーについて別の記事で書いています。実際にどの段階を自分で持ちたいかで選んでください。

Thunderbitでウェブデータを抽出してみる

総評

すでにDOMを持っていて、JavaScript/Node上で動かし、多少の余分なsiblingコンテンツを許容してでも過度な欠落を避けたいなら、Readabilityは有力な候補です。今回のフィクスチャパックでは、ラベル付けされた記事ブロック74個をすべて回収し、表・コード・キャプションも保持しました。ただしこの結果は単一カラムの合成ページという条件に限定されます。公開されている実ページではrecallが0.982で、冒頭段落や表の直前で落ちるという既知の欠落は再現せず、コンテンツの乏しいページでは定型コンテンツを記事として返してしまうことがあります。

弱点の大きさは正しく見積もる必要があります。今回のフィクスチャにおける主な失敗面はprecisionであり、それはソースコード上の特定の一行に起因しています。80文字を超えていて、リンク密度が0.25未満のsiblingは、それが記事の一部かどうかにかかわらず記事に追加されます。同一のテキストで0.143と0.278を比較したとき、その切り替わりを実際に観察できました。実ページのベンチマークではprecision 0.914、recall 0.982が報告されています。抽出したテキストを、後でモデルが引用するインデックスに投入するのであれば、保持された定型コンテンツと欠落した本文の両方を確認してください。どちらの誤りも存在しないと仮定してはいけません。

Thunderbitでウェブデータを抽出してみる Get Started Free

よくある質問

Mozilla Readabilityは定型コンテンツをすべて取り除いてくれますか? いいえ。しかもその数値は何を測定するかによって大きく変わります。公開されている実ページベンチマークでは、readability_js 0.6.0のprecisionは0.914で、返された内容の約8.6%は本文ではありませんでした。私が作った現実的なテストページでは、6個の外周ブロックのうち5個(nav、広告、サイドバー、コメント、フッター)を除去し、中立的なクラス名のプロモ段落1個だけを残しました。ヒューリスティックを意図的に突破するように作り込んだブロックを含むフィクスチャ集合では、17個中5個を保持しましたが、この最後の数値はストレステストであって、現実世界の比率ではありません。

Node上でreadability.jsを使うにはjsdomが必要ですか? はい。もしくは何らかの別のDOM実装が必要です。Readability自体は純粋なJavaScriptですが、ライブなdocumentオブジェクトを対象に動作するため、Node上ではDOMを自分で用意する必要があります。今回のセットアップではjsdom 29.1.1を使いました。「依存関係なし」という説明はアルゴリズムについてのものであり、実行環境についてのものではありません。またparse()は渡されたdocumentを書き換えるため、一つのDOMを使い回すのではなく、ページごとに新しく構築してください。

charThresholdオプションは実際には何をしているのですか? 多くの人が思っているものとは違います。短い記事をnullにするものではありません。120文字まで縮めたクリーンな記事も回収でき、charThresholdを200、500、1000に変えても抽出後の長さは同じでした。このしきい値が制御しているのは、クリーンアップのフラグを外して取得をやり直すかどうかです。クリーンなページには削るものがないので、どちらの経路でも結果は同じになります。本当のnullになるのは、抽出可能なテキストがまったく存在しないページの場合です。navだけのページでさえnullにはならず、navを記事として返してきました。

parse()の前にisProbablyReaderableを呼ぶべきですか? 関門としてではなく、ヒントとして使ってください。parse()が成功したにもかかわらずisProbablyReaderableがfalseを返したページ形状が3種類ありました。<li>要素の中にだけコンテンツがある場合、140文字未満の段落が10個ある場合、408文字の段落が1個だけの場合です。<li>のケースは調整では直せません。予測器がスコアを付けるのはpprearticleノードだけだからです。短い段落が多いケースはminContentLengthを下げる必要があり、長い段落1個のケースはminScoreを下げる必要があります。デフォルト設定では、単独の段落がしきい値を自力で超えるには540文字が必要です。そのページが重要なら、parseして結果を確認してください。

記事抽出にはReadabilityとtrafilaturaのどちらがよいですか? 同一のフィクスチャのバイト列で比較すると、trafilaturaは定型コンテンツの保持が少なく(17個中1個、Readabilityは5個)、一方でReadabilityは短いコンテンツをより多く回収し、trafilaturaが落とした<figcaption>も保持しました。どちらを選ぶかは、許容できる誤りの種類と実行環境次第です。公開されているベンチマークは別の文脈のものであり、このフィクスチャの結果を検証するものではありません。

Ke
Ke
Thunderbit の CTO | シニアデータサイエンティスト & ML エキスパート 機械学習とデータサイエンスで約10年の経験を持つ Ke Shen は、コロンビア大学の卒業生であり、Walmart Labs の元シニアデータサイエンティストです。Python、R、Java、統計学における深い専門性は同業者からも高く評価されており、理論段階の複雑な AI アルゴリズムを本番運用レベルのアーキテクチャへと落とし込むための、実践に裏打ちされた知見を共有しています。
Topics
Web Scraping ToolsAI Web Scraper
目次
Thunderbit · AIウェブデータエージェント

1クリックであらゆるページからデータを抽出

25万人以上のユーザーに支持されています
無料プランあり
Webページからスプレッドシートへ
欲しい内容を伝えるだけ — ThunderbitのAIエージェントが取得し、Excel、Google Sheets、Airtable、Notionへ出力します。すぐに無料で始められます。
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week