リークを定義できるフィクスチャに限れば、goose3 は コンテンツトークン精度 1.0000 を記録し、混入トークンは 0 でした。ナビゲーション、広告、サイドバー、コメント、プロモーションの語は一切出力に入りませんでした。6 ライブラリ比較の中で、これに並ぶ結果は他にありませんでした。
一方で、記事再現率は全体で最も低い ライブラリでもあります。22 個のフィクスチャ全体で 0.8243、Mozilla Readability の 1.0000 に対して低いのは、2 件で空文字を返したためです。
これらの指標はスコアリング規則上つながっています。空の結果は conditional precision に何も寄与せず、recall だけが取りこぼしとして記録されます。
goose3 とは
goose3 は、Scala の Gravity Labs Goose を起点とし、python-goose を経てつながる系譜の Python 3 版です。テキストをそのまま吐き出すだけのツールではなく、メタデータ付きの記事抽出器 です。Goose を生成して extract() を呼ぶと、クリーン化された本文、タイトル、著者、公開日、メイン画像、メタ説明、タグ、リンク、ツイートなど、約 28 項目の参照可能なフィールドを持つ Article オブジェクトが返ります。
公式リファレンス: goose3 の公式リポジトリ。

検証バージョン: 3.1.22、Apache ライセンス、GitHub スター 912、最終 push は 2026-07-23。テスト時点では継続的にメンテナンスされていました。Python 3.14.2。
API は 2 回の呼び出しと 1 つの注意点で使えます。
from goose3 import Goose
g = Goose()
try:
article = g.extract(raw_html=html)
text = article.cleaned_text
finally:
g.close() # 使用後は明示的に close する
この close() は見落としやすいので要注意です。何も警告は出ません。このレビューでは、閉じ忘れ時にセッション、接続、メモリがどれだけ残るかをループで計測していないため、「リソースリーク」と断定するには証拠が足りません。ここでは、API の使い方として示されているライフサイクル要件として、明示的な close が必要だと捉えるのが適切です。
トレードオフを数値で見る
6 つの抽出器、1 つの注釈付きフィクスチャセット、1 つのスコアラー。各フィクスチャの各ブロックには article か boilerplate が付与され、さらに固有のシグネチャトークンを持っています。そのため、「この単位を取り戻せたか」は類似度ではなく、完全な部分文字列の一致で判定できます。
| ライブラリ | 記事再現率(全 22 件) | ボイラープレート漏れ | コンテンツトークン精度 | 混入トークン | 出力あり |
|---|---|---|---|---|---|
| Readability | 1.0000 | 0.2353 | 0.9109 | 35 | 22/22 |
| trafilatura | 0.9865 | 0.0588 | 0.9411 | 4 | 22/22 |
| newspaper4k | 0.9865 | 0.0000 | 0.9452 | 0 | 22/22 |
| resiliparse | 0.9054 | 0.0588 | 0.9381 | 7 | 22/22 |
| jusText | 0.8378 | 0.4706 | 0.8760 | 74 | 19/22 |
| goose3 | 0.8243 | 0.0000 | 1.0000 | 0 | 20/22 |
sixway-scores.json。再現率は 22 件すべてを対象にし、漏れ率と精度は article と boilerplate の両方を含む 11 件だけを対象にしています。]
精度の列は、最後の列とセットで読む必要があります。ここでの precision は 出力がある場合に限った条件付き です。つまり、フィクスチャで空文字を返すライブラリは、分子にも分母にも何も足しません。この平均では、出力しないこと自体は損にならないのです。分子は、抽出された非ストップワードトークンとラベル付き article トークンの多重集合の重なり、分母は抽出された非ストップワードトークンの全量です。「混入トークン」はより狭く、ラベル付き boilerplate トークンとの重なりのみを数えます。追加で抽出されたトークンが、article にも boilerplate にも一致しない場合は、混入トークン数は増えずに精度だけが下がります。article の多重集合を超える重複トークンでも同じことが起きます。これが、newspaper4k が混入トークン 0 でも precision が 1.0000 未満になる理由です。goose3 の 1.0000 は 11 件中 10 件で採点された結果であり、Readability、trafilatura、newspaper4k、resiliparse は 11 件中 11 件で採点されています。
この synthetic セットの中では、1.0000 は依然として有用です。採点対象の 10 件では、goose3 はラベル付き boilerplate トークンを 1 つも出しませんでした。Readability は同じページで 35 個を出力しています。モデルがこの出力を使うなら、これら 10 件では boilerplate ラベルに対応するトークン消費がゼロだった、という意味になります。ただし、実ページでの無駄がゼロだと証明するものではありませんし、空結果はパイプラインの別の場所でフォールバックや再試行のコストを生むこともあります。
2 つの無応答、その意味
goose3 はちょうど 2 件のフィクスチャで何も返しませんでした。そのうち 1 件は許容可能、もう 1 件は本当の限界です。
ほぼ空のドキュメント。 記事単位が 32 文字しかないページです。goose3 は取り込みません。jusText も同様です。このフィクスチャセットでは、Readability は 22 ページすべてで出力したので、少なくともこの件に関しては goose3 の沈黙を支持する結果ではありませんでした。こうした極小ドキュメントを拒否するのが妥当かどうかは、呼び出し側が要求する最小コンテンツ条件次第です。

<li> 要素だけで構成された記事。 記事単位が 6 つあり、いずれも <p> タグではありません。goose3 は空文字を返します。
ここは少し意外でした。なぜなら goose3 のデフォルト設定には parse_lists=True があるからです。そこで、ライブラリそのものの結論を出すには 1 回の不振では足りないので、動作する対照ページと併せて 3 種類の設定を試しました。
| 設定 | リストだけのページ | <p> の対照ページ |
|---|---|---|
| デフォルト | 0 文字 | 937 文字 |
strict=False | 0 文字 | 937 文字 |
parse_lists=True(明示) | 0 文字 | 937 文字 |
3 通りすべてで 0 文字、対照ページは 3 通りすべてで 937 文字でした。つまり parse_lists=True が制御しているのは、すでに見つけた article の中にリストを残すかどうか であり、候補スコアラーがリストそのものを article として扱えるようにするものではありません。goose3 のノード採点は、本文を見つけるために paragraph 形状のブロックを必要とし、本文がリストだけのページにはそれがありません。
この結果として言えるのは、こうした synthetic フィクスチャのように、6 つの article 単位がすべて <li> で、段落候補が 1 つもない本文では、空文字が返るということです。変更履歴、API リファレンス、レシピ、FAQ、比較記事などは、リスト比率が高い可能性があるため実ページの再現テスト候補としては妥当ですが、この 1 フィクスチャだけでそれらのカテゴリ全体が一般に失敗するとまでは言えません。
空文字は、呼び出し側が非空出力を検証して初めて機械的に検出できます。記事が 1 文字も含まれないもっともらしいテキストよりは判定しやすいですが、例外だけを監視する運用では静かな失敗のままです。本番では、最低出力長のチェックと、フォールバックまたは失敗ページの明示的記録が必要です。
導入時の現実
pip install goose3 では 16 パッケージ と 44.3 MiB が入り、所要時間はおよそ 6〜9 秒でした。新しい subprocess でのコールドインポートは 2.181 秒。
公式リファレンス: PyPI の goose3。
| ライブラリ | パッケージ数 | 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 で測定しているため、他ライブラリの footprint は引き継いでいません。]
重さは中位、速度も中位でした。Python 3.14.2 で問題なくインストール・インポートできたのは、このジャンルでは必ずしも当たり前ではありません。
導入前に知っておきたい 3 つのデフォルト

ドキュメントではなく、同梱の Configuration オブジェクトを読むと、19 個の設定が見つかります。その中で、いくつかは驚くはずです。
自分の名前を名乗る。 browser_user_agent の既定値は Goose/3.1.22 です。goose3 に自前で取得させると、接続先サーバーにはライブラリ名と正確なバージョンが毎回記録されます。正直ではありますが、同時に指紋にもなります。意図的に設定するか、HTML を自前で取得して raw_html を渡してください。
MacPorts のバイナリを指している。 imagemagick_convert_path の既定値は /opt/local/bin/convert、imagemagick_identify_path は /opt/local/bin/identify です。私の環境にはどちらもありません。/opt/local は MacPorts で、多くの人は使っていません。Homebrew は /opt/homebrew にバイナリを置きます。この既定は、画像取得を有効にしない限りは無害です(enable_image_fetching の既定は False で、そこは妥当です)。ただし、トップ画像抽出を期待して有効化すると、静かに機能しません。
英語前提。 target_language は en が既定で、use_meta_language=True なので、ページ側に言語宣言があればそれを追従し、なければ英語に戻ります。英語コンテンツなら問題ありませんが、それ以外では明示設定したほうが安全です。
その他は概ね妥当です。parser_class は lxml、http_timeout は 30 秒、strict はオン、log_level は ERROR、parse_headers と keep_footnotes はオン、images_min_bytes は 4000。
メモリと、壊れた HTML がそれに与える影響
このレビュー系列で「未検証」とされていた 2 点を、今回測定しました。
より広いストレステストの文脈は、10 ライブラリのメモリと不正 HTML 比較 にあります。
ピーク常駐メモリ は /usr/bin/time -l で測定し、各セルごとに新しいプロセスを 1 つ起動しました。import floor は、ライブラリを読み込んで待機しているだけで必要なコスト、ピーク値にはドキュメント本体が含まれます。
| ライブラリ | 実行環境 | Import floor | 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 の基準値は相互比較できません。どちらにもインタプリタのコストが含まれています。]
goose3 の floor は 44.1 MiB、10 MB ドキュメントでは 398.5 MiB まで上がります。ここに示した Python ライブラリの中では import floor が 3 番目に高く、コールドスタート重視の導入では知っておくべき値です。
壊れた HTML。 12 文書はそれぞれ 1 箇所だけを壊しています。未閉じタグ、入れ子の崩れたインライン要素、スペースを含む引用符なし属性、余計な閉じタグ、<html> なし、重複属性、タグ途中で切れた文書、壊れたエンティティ、閉じていない <script>、嘘の charset 宣言、マークアップを含むコメント、そして 600 階層のネスト。さらに、サイズを合わせた 2 件の正しい対照文書 も含めています。というのも、「何も返さなかった」だけでは、その同じサイズのきれいな文書でも沈黙するライブラリなのか判断できないからです。
goose3 は 14 件中 0 件で例外を投げず、10 件で何も返さず、壊れたフィクスチャ全体で 33 個中 2 個 のシグネチャを回収しました (malformed-results.json)。1 件はこの集計から除外しています。HTML5 仕様では、閉じていない <script> の後ろはすべて script content なので、そこで失われるのが正しく、逆に回収できたら仕様からの逸脱だからです。
長所と短所
長所。 出力がある 10 件のコンテンツ忠実度フィクスチャで、ラベル付き boilerplate トークンは 0。約 28 の記事フィールドが利用可能で、ただし精度は採点ではなく棚卸しのみ。画像取得はデフォルトでオフで、設計として妥当。Python 3.14 でクリーンにインストール可能。継続メンテナンス中。Apache-2.0。呼び出し側が明示的にチェックすれば、空結果を簡単に失敗扱いにできます。
短所。 セット内で最も低い記事再現率 0.8243。これは誤った内容を返したからではなく、単に何も返さなかったことによるものです。記事がリストだけのページでは、設定にかかわらず空文字になります。44.3 MiB と 2.2 秒のコールドインポートは、resiliparse の 21.0 MiB / 15 ms と比べると重いです。close() が必要です。2 つのデフォルトは、多くのマシンでは正しくない場所を指しています。
どんな人に向くか、向かないか
goose3 を使うべきケース。 抽出テキストがモデルやデータベースに入り、ラベル付き boilerplate の混入コストが高い場合、かつページが段落中心の通常の記事である場合です。このフィクスチャセットでは、答えを返したときにラベル付き boilerplate トークンを 1 つも出しませんでした。メタデータの面は利用可能ですが、このレビューでは精度未検証です。タイトル、著者、日付、画像の正確さは、選定材料にする前に別途グラウンドトゥルース付きフィクスチャで確認が必要です。
避けるべきケース。 コーパスがリスト中心なら、空文字と理由なしの沈黙が返ってきます。コールドスタートコストが重要なら、resiliparse は 145 倍速く import します。全ページで必ず答えが必要なら、ここでは「答えがない」こと自体が現実的な結果です。22 件中 2 件、しかもどちらも空文字という静かな失敗です。
試す価値のある組み合わせ。 cleaned_text が空、または最小文字数ルールを下回るときに fallback を使う構成で、goose3 を一次候補にすることです。Readability はこの 22 件ですべての article 単位を回収し、goose3 の 2 つの空ケースも拾いました。この synthetic 結果が示しているのはアーキテクチャのパターンであって、本番ページで fallback が絶対に取り逃さないという約束ではありません。
マネージド API が適する場面
このベンチマークで検証したのは、goose3 の raw_html 取り込み経路です。つまり、goose3 に渡る前に HTML はすでに取得済みでした。goose3 には User-Agent 設定があるように、自前でネットワーク取得する経路もありますが、今回はそちらは未検証です。JavaScript レンダリングや anti-bot 挙動も対象外でした。
6 つの抽出器で同じフィクスチャを比較した結果は、6 ライブラリの抽出比較 をどうぞ。
取得・レンダリング・抽出をまとめて担うマネージドサービス、たとえば Thunderbit は、責務の境界が異なります。Thunderbit は今回のベンチマーク対象ではありません。違いは、渡された HTML から記事を抜き出すのか、URL を取得して処理するホスト型サービスなのか、という点です。この文章では同一指標での性能・品質比較はしていません。
公平に言えば、goose3 のフィールドセットは固定で記事向きです。ページが記事なら理にかなっていますが、商品一覧では不向きです。すでに HTML を持っていて、ページが記事なら、goose3 は無料でかなりクリーンです。
ホスト型の選択肢を広く見たいなら、web scraping API 一覧 が参考になります。セルフホストの代替案なら、オープンソーススクレイパー総覧。テキストをモデルに渡すなら、Python で HTML を Markdown に変換する方法 で、どこで忠実度が落ちるかも確認できます。
goose3 を使うべきか?
はい、段落型の記事がワークロードに合っていて、呼び出し側が空出力を成功ではなく失敗として扱うなら、使う価値があります。
このフィクスチャセットでは、goose3 は答えを返したときにラベル付き boilerplate トークンを 1 つも出さず、2 件の空文字を返しました。1 件はほぼ空のページ、もう 1 件はリストだけの synthetic 本文です。これは precision/coverage のトレードオフであり、製品全体の性格を示す証拠ではありません。
再現率をより重視するなら、明示的な最小出力ガード付きで fallback を試してください。Readability はこの 22 件の全 article 単位を回収しました。newspaper4k はラベル付き boilerplate 単位の漏れが 0 で、再現率 0.9865、しかも 22 件すべてで出力ありでした。これらの結果は、この synthetic セットに対する序列を示すもので、未知の本番ワークロードに対するものではありません。
goose3 は、誤った 1 語のコストが、ページ 1 枚を取り逃すコストより高いときに選ぶべきライブラリです。
Web データ抽出に Thunderbit を試す Get Started Free
FAQ
goose3 の完全な精度は本物ですか、それとも「出力しなかった」ことの副産物ですか? 両方です。しかも 2 つは切り分けられます。boilerplate を含む 11 件のうち 10 件で採点されたため、平均から 1 件抜けています。これは「出力しなかった」ことによる見かけです。しかし、その 10 件では、あえて難しく作った boilerplate に対して混入トークンが 0 でした。Readability は 35 個漏らしています。precision は答えを返したページでは本物で、出力しないことは recall に現れます。
なぜ goose3 は、記事がリストだけのページで何も返さないのですか?
候補スコアリングが記事本文を見つけるために paragraph 形状のブロックを必要とするからです。<li> だけで構成されたページには、それがありません。parse_lists=True のデフォルトではこの挙動は変わりません。明示的に試し、さらに strict=False も加えましたが、3 通りすべてで 0 文字でした。一方、<p> ベースの対照ページは 3 通りすべてで 937 文字でした。parse_lists は、すでに見つかった記事の中でリストを保持するかどうかを決めます。
close() は必ず呼ぶ必要がありますか?
はい。上で示したように try/finally で明示的に閉じてください。このレビューでは、閉じ忘れ時に何が蓄積するかは測っていないため、ループリークの定量的な断定はしていません。ただし、Goose に呼び出し側が管理すべきライフサイクルがあることは確認できています。
goose3 は User-Agent に何を送りますか?
既定では Goose/3.1.22 です。ライブラリ名と正確なバージョンが入ります。これは goose3 に取得まで任せる場合のみで、raw_html を渡せば完全に回避できます。自前で取得させるなら、User-Agent は意図的に設定してください。デフォルト値は、接続先すべてのサーバーに何が来たかを正確に知らせます。
このレビューで未検証だったことは何ですか?
実世界のページは一切です。ここで使ったのは制御されたフィクスチャだけです。target_language が重要設定であるにもかかわらず、多言語抽出は未検証です。メタデータのフィールド(タイトル、著者、日付、メイン画像)は棚卸ししただけで、正確さは採点していません。画像取得はデフォルトでオフで、ImageMagick のパスは多くのマシンにないパッケージマネージャ向けを指しています。並列・長時間負荷でのメモリ挙動やスループットも未測定です。メモリ表は、1 つの新しいプロセスが 1 文書を処理した場合の値です。


