BeautifulSoup は、PythonでWebページをスクレイピングするときに最初に手に取る人がほとんどのライブラリですが、実際のところ、主要なHTMLパーサーの中では最も遅い部類に入ります。この2つはどちらも事実で、どちらも欠点というわけではありません。おもしろいのは、この「遅い」が感覚ではなく、ちゃんと数値で見えるはっきりした差だという点です。
bs4(つまり beautifulsoup4、バージョン 4.15.0、2026年6月リリース、MITライセンス)について、最新の機能テストと、同じベンチマーク環境から再利用した計測データを組み合わせて検証したところ、結論は一貫していました。親しみやすいAPIと、業界トップクラスのエラー耐性を得る代わりに、おおよそ1桁台の速度ペナルティを払うことになります。この取引が賢いかどうかは、完全に用途次第です。なので本レビューでは、その両面をきちんと並べて扱います。
BeautifulSoupは実際には何をしているのか(していないのか)
多くの入門記事が一番大事な部分を飛ばしています。BeautifulSoup 自体はHTMLをパースしているわけではありません。あくまでラッパーです。内部では、Python標準の html.parser、lxml、html5lib の3つの本物のパーサーのいずれかに文書を渡し、そのツリーを、ひとつの使いやすいナビゲーション/検索APIで包みます。bs4の役割はパースそのものではなく、できあがった結果を気持ちよくたどれるようにすることです。
作者自身もこれを「screen-scraping library」と呼んでおり、昔から一貫した売り文句は同じです。ブラウザなら思わず顔をしかめるような壊れたHTMLを渡しても、必要なデータを拾い出してくれる。これは誇張ではなく、ちゃんと実力があります。ただし、1つだけ注意点があります。それは後ほど触れます。
まず押さえておきたい事実を整理しておきましょう。
| 項目 | 値 |
|---|---|
| パッケージ | beautifulsoup4(bs4 として import) |
| テストしたバージョン | 4.15.0(2026-06-07 アップロード) |
| Python要件 | >=3.7.0 |
| ライセンス | MIT |
| 公式サイト | crummy.com/software/BeautifulSoup |
| ソースコード + バグトラッカー | Launchpad — GitHubではない |
| 保守状況 | アクティブ(2026年6月に4.15.0、過去1年で6リリース) |
「GitHubではない」という点は、見た目以上に重要です。bs4は20年選手のライブラリで、crummy.com と Launchpad で運用されています。いつものGitHubスター数での雰囲気判断は通用しません。代わりに、リリース頻度で健全性を見るべきです。その意味では、今も十分に元気です。
ライセンス面でも、コンプライアンス担当者への説明が必要な人向けに一応触れておくと、ラッパー自体はMITですが、bs4を入れたときに実際に依存ツリーへ入ってくるものは、バックエンド次第です。html.parser はPython標準ライブラリなので、追加依存はありません(PSFライセンス)。lxml はBSDライセンスですが、裏で libxml2/libxslt という外部C依存を使います。これは自前でビルドするか、事前ビルド済みwheelを入れる形です。html5lib は純PythonでMITです。依存を最も軽くしたいなら、標準搭載の html.parser が一番です。もっとも、それが一番大きな落とし穴でもあります。詳しくは後で。
速度コストを数値で見る
まず数字から出します。ここを曖昧にするのは不誠実だからです。実際の「パースしてから抽出する」作業、つまり文字列をパースし、すべての <h3 class="title"> と <a href> を取り出す処理では、BeautifulSoup はこの比較の中で最も遅いパーサーです。しかも、かなり差があります。

この計測値は selectolax のベンチマーク環境から再利用したものです(同一マシン、同じ3回計測方式、2026-07-13時点)。CPU競合や重複作業を避けるため、このレビュー自体ではタイミング計測を再実行していません。中央値 p50 のレイテンシをミリ秒で示すと次のとおりです。
| ページサイズ | bs4 (html.parser) | bs4 (lxml) | selectolax-Lexbor | lxml | bs4-hp の遅さ | bs4-lxml の遅さ |
|---|---|---|---|---|---|---|
| 1 KB | 0.323 | 0.281 | 0.027 | 0.036 | 12.0x | 10.5x |
| 10 KB | 2.049 | 1.705 | 0.160 | 0.166 | 12.8x | 10.7x |
| 100 KB | 20.599 | 16.540 | 1.464 | 1.423 | 14.1x | 11.3x |
| 1 MB | 232.558 | 181.855 | 14.901 | 14.177 | 15.6x | 12.2x |
| 10 MB | 2788.746 | 2261.557 | 159.935 | 172.933 | 17.4x | 14.1x |
つまり bs4(html.parser) は、selectolax-Lexbor のようなCパーサーより 12〜17倍 遅く、lxml バックエンドに変えても 10.5〜14倍 遅いままです。構造上の理由なので、バグではありません。どのバックエンドがパースを担当しても、bs4 は各ノードごとに完全なPythonオブジェクト(Tag または NavigableString)を作ります。このオブジェクト化の層は、Cパーサーが払わなくていいコストです。
ページが大きくなるほど倍率が上がっている点にも注目です。1 KBで12.0倍、10 MBで17.4倍です。これは固定の起動オーバーヘッドではなく、ノード数に比例して増えるコストだという意味です。
ここで見方を変えておきましょう。「10倍遅い」と聞くと、必要以上に怖く感じます。1 MBのページなら、232 ms と 15 ms の差です。もしあなたの仕事が「数百KB程度のページを数百〜数千件スクレイプする」ことなら、この差は体感しにくいですし、最適化しても大した得にはなりません。逆に、100万ページ級のパイプラインなら、この比率の差が「終わるジョブ」と「終わらないジョブ」を分けます。同じ数字でも、規模が違えば結論は真逆です。ベンチマークの数字ではなく、実際の処理量に対して判断すべきです。
いいえ、バックエンドを変えても根本解決にはなりません
bs4 に lxml バックエンドを指定すれば lxml 並みに速くなる、というよくある誤解があります。なりません。理由を理解しておく価値はあります。10万ノードの一括CSSクエリ(すべての <a> を選び、hrefを読む。ツリーは事前構築済み)では、スループットの差ははっきりしています。
| パーサー | クエリ p50 | ノード/秒 |
|---|---|---|
| lxml | 33.30 ms | 3,002,646 |
| selectolax-Modest | 34.19 ms | 2,924,550 |
| selectolax-Lexbor | 39.46 ms | 2,534,027 |
| bs4 (lxml) | 250.56 ms | 399,111 |
bs4(lxml) はおよそ 399,000 ノード/秒で、Cエンジン3種より 6.3〜7.5倍 遅いです。しかもバックエンド自体は lxml なのに、です。バックエンドが速くするのはツリー構築の部分だけです。クエリとトラバースは依然として soupsieve 経由で bs4 の Tag オブジェクトに流れ、マッチしたノードは毎回Pythonの箱に入れられます。つまり、「bs4にlxmlを渡せばlxml並みに速い」という理解は誤りです。速くなるのは一部の処理だけで、一番遅い部分はそこではありません。
メモリ使用量とコールドスタートもコストに入れておきましょう。10 MBの文書では、bs4 は selectolax や lxml より 1.5〜1.75倍 多くの常駐メモリを使います(218〜226 MB 対 129〜145 MB)。理由は同じで、ノードごとにPythonオブジェクトを作るからです。さらに、bs4 の import は lxml.html の 14.1 ms に対して約 33.4 ms かかるので、2.36倍 遅いです。長時間動くプロセスなら誤差に近いですが、CLIツールや、頻繁にコールドスタートするサーバーレス関数では、知っておく価値のある小さな実コストです。
なぜスレッドを増やしても救われないのか
CPUバウンドな遅い処理を見て「スレッドを増やせばいい」と思うなら、bs4 はその発想をしっかり痛めつけます。1 MBのページを48回パースしたときの、1スレッドと4スレッドの比較です。
| パーサー | 1スレッド | 4スレッド | 速度向上 |
|---|---|---|---|
| selectolax-Lexbor | 0.563 s | 0.159 s | 3.54x |
| lxml | 0.459 s | 0.378 s | 1.21x |
| bs4 (lxml) | 6.945 s | 26.842 s | 0.26x |
一番下の行は、もう一度見たほうがいいです。4スレッドにしたことで bs4 は速くなるどころか、約3.9倍遅く なりました。実測の示唆としては、「GILを抱えている可能性が高い」です。bs4 のツリー構築は純Pythonなので、Global Interpreter Lock の下で直列化されます。そこにスレッドを積み増しても、実際には並列実行できない処理にスケジューリングのオーバーヘッドが増えるだけです。selectolax は Cコアがロックを解放するため約3.5倍の速度向上が得られますが、bs4 にはその余地がありません。
フリースレッディング時代における実務上の結論はシンプルです。BeautifulSoup を並列化したいなら、スレッドではなく multiprocessing(ProcessPoolExecutor)を使ってください。selectolax と lxml はスレッドで伸ばせますが、bs4 は伸びません。なお、厳密さのための注記として、これは4スレッド・1 MBという単一条件での観測であり、「GILを保持している」という説明は壁時計時間から推定した仮説です。どのコードパスが実際にロックを握っているかを計測器で確認したわけではありません。方向性は明確ですが、機構そのものは現時点では推定です。
デフォルトバックエンドが罠です。まずここを読んでください。
このレビューからひとつだけ持ち帰るなら、これです。第二引数なしの BeautifulSoup(html) は html.parser を使います。そして html.parser は、HTML5の「省略可能な終了タグ」のルールを実装していません。学術的に聞こえるかもしれませんが、実データを黙って壊すので深刻です。

3つのバックエンドすべてに対して、意図的に壊したHTMLサンプル15個を流し込みました。しかも各サンプルについては、事前にバックエンド非依存の構造アサーションを登録済みです(後出しで勝者を選べないようにするためです)。結果はこうなりました。
| バックエンド | 期待どおり / 15 |
|---|---|
| lxml | 15 |
| html5lib | 15 |
| html.parser | 12 |
失敗した3件は、すべて同じ根本原因を持っています。たとえば閉じていないテーブル <table><tr><td>a<td>b<tr><td>c<td>d</table> を見ると、html.parser では抽出結果が ['abcd','bcd','cd','d'] になります。各 <td> が後ろの内容を飲み込んでしまい、セルを閉じる代わりに入れ子にしてしまうからです。lxml と html5lib は正しく ['a','b','c','d'] を返します。<li>a<li>b<li>c のようなリスト項目も同じで、html.parser では ['abc','bc','c'] と入れ子になり、他の2つは ['a','b','c'] を返します。重複属性も同様で、<div id="first" id="second"> は html.parser では "second" を残し、lxml/html5lib では "first" を残します。HTML5仕様では最初の値を残すのが正解です。
これが単なる「ちょっと面倒」ではなく危険な理由は、エラーにならない からです。BeautifulSoup(html) のまま、閉じていないテーブルやリストに当たると、古いサイト、手書きHTML、閉じタグを忘れたテンプレートなどでよくある話ですが、隣接するセルの内容がひとつに混ざり、汚れたデータを返し、しかも一言も文句を言いません。対処法は引数を1つ足すだけです。BeautifulSoup(html, "lxml") か BeautifulSoup(html, "html5lib") を使ってください。
なお html.parser にも公平を期すと、残り12件の壊れたサンプルは3つのバックエンドすべてで同じ結果でした。たとえば <b><i></b></i> のような誤った入れ子、html/body の骨組みがないもの、引用符のない属性、孤立した閉じタグ、閉じていないコメント、入れ子の form、大文字小文字の混在などです。bs4 の耐性は全体として本当に強いです。差が出るのは、ほぼ省略可能な終了タグ系に集中しています。そしてこれは新発見でもありません。bs4 公式の「Differences between parsers」ドキュメントにも、html.parser は平易な言葉で「less lenient」と書かれています。この不完全HTMLのマトリクスが追加するのは、「less lenient」が実際にどのケースで誤出力になるか、という再現可能な具体例です。
失わないもの:APIとCSSはむしろ強み
bs4 は遅く、単一スレッド寄りで、デフォルトバックエンドに罠があります。それでも人が使うのは、「親しみやすさ」の側面が本物だからです。そして、それはテストでも裏付けられました。

検索、CSS、ツリー移動、テキスト抽出、DOM変更をカバーする29個のAPIプローブを実行したところ、29個すべてが成功しました。しかも各プローブの結果は、目視ではなく期待値との比較で判定しています。Cパーサーではそのままでは提供されない、特に便利な機能が2つあります。
find/find_allに関数述語を使えること。soup.find(lambda t: t.name == "a" and "btn" in t.get("class", []))のように書けば、複雑な条件をPython1行で表現できます。「全部取ってからフィルタする」という2段階は不要です。- 名前付きで双方向に移動できるツリー操作。
.parent、.next_sibling、.find_parent、.stripped_strings、.descendantsなど、まるで英語のように読めて、上下どちらにも辿れます。selectolax では、これらの一部は複数ステップが必要だったり、そもそも用意されていなかったりします。
これは「開発者の時間を買う」という側面を、ちゃんと数で示したものです。マーケティングではありません。29個の緑チェックです。
公平なレビューとして、両面を挙げておくべきトラップも2つあります。まず、真偽値属性です。<input disabled> の disabled は bs4 では空文字列 "" になります(selectolax では None)。どちらも falsy なので、if node.get("disabled") と書くと、実際には存在している真偽値属性を両ライブラリで静かに取りこぼします。安全なのは "disabled" in tag.attrs です。次に、get_text(strip=True) は、strip 後に区切り文字なしでノードテキストを連結します。つまり "...with " + "link1" は "withlink1" になります。単語境界が必要なら separator=" " を指定してください。どちらも bs4 固有ではなく、ライブラリ横断の落とし穴です。
そして意外かもしれませんが、bs4 を選んでも CSS 対応で損をするわけではありません。CSSエンジンの soupsieve は、この比較の中で最も完成度の高い実装です。41ケースの基本マトリクス(selectolax のベンチから再利用)では、soupsieve は 41/41 を達成し、業界唯一の満点でした。selectolax-Lexbor の 39/41、cssselect(lxml/parsel)の 37/41 を上回っています。さらに、ドキュメントでサポートが明記されている拡張ケースを20件追加で試したところ、soupsieve は 20/20 でした。Lexbor が明確に弾く :lang(en)、soupsieve専用の :-soup-contains('featured')、:is()、:where()、:has(> a) まで通ります。本当の弱点は XPath に対応していないこと(soupsieve はCSS専用)と、parsel の ::text / ::attr() 擬似要素です。これは Scrapy の拡張です。XPath中心で生きているなら、その移行はかなりつらいでしょう。
このセクションの結論は明快です。BeautifulSoup を選ぶことで失うのは速度です。APIの使いやすさでも、CSS対応でもありません。
本番運用で予算を見ておくべき2つの落とし穴
デフォルトバックエンド以外にも、長時間動く処理や非UTF-8のワークロードで特に効いてくる挙動が2つあります。
参照サイクル: 長いループでは decompose() を呼ぶ
bs4 の Tag は、親への参照と子への参照の両方を持っています。つまり参照サイクルができます。CPython の参照カウント方式だけでは、サイクルは自力で回収できません。これは世代別GCの仕事です。これがどれくらい効くかを見るため、GCを切った状態でツリーを300回作って削除し、メモリ上に残った Tag オブジェクト数を数えました。

| 状況 | del 後に残った Tag 数 |
|---|---|
| GCオフ | 120,900(300回分、何も回収されず) |
| GCオン | 26,598(ループ中に世代別GCが発火) |
強制 gc.collect() 後 | 0(すべて回収) |
| サイクルなしの対照群(文字列リスト、GCオフ) | 差分 0 |
GCオフでは del soup しても何も回収されず、120,900個すべてがメモリ上に残りました。参照サイクルが参照カウントを打ち負かしたからです。gc.collect() を1回呼ぶと、すべて消えました。サイクルを持たない対照群(単なる文字列リスト)は差分ゼロでした。つまり、増加分はbs4のサイクルが原因で、計測ノイズではありません。bs4 自身のドキュメントにも、オブジェクトが「densely interconnected ... exactly the sort a garbage collector would have trouble with」とあるので、これは文書化された挙動です。このテストが追加するのは、残留オブジェクト数と、collect() でゼロになることの実証です。
実務上のルールはこうです。大量のページを短いループで処理するパイプラインでは、コードや高スループット設定によってGCが無効化されていたり、十分な頻度で動かなかったりすると、bs4 のツリーが残り続け、メモリが増えていきます。各ページの後で soup.decompose() を呼んでください。bs4 がそれを用意しているのは、サイクルを断ち切って早期解放するためです。selectolax や lxml のCツリーには、この問題はありません。
エンコーディング: UnicodeDammit はbs4の静かな強み
bs4 には、高速パーサーにはないコンポーネントがあります。UnicodeDammit です。文書の文字コードを推定して、自動でUnicodeに変換してくれます。これについて、8ケースの「宣言された文字コード vs 実際の文字コード」マトリクスを試しました。

| ケース | 実際の文字コード | UnicodeDammit の推定 | 復元できたか |
|---|---|---|---|
| utf8_no_decl | utf-8 | utf-8 | はい |
| utf16_bom | utf-16 | utf-16le | はい |
| gbk_chinese | gbk | gb18030 | はい(上位互換) |
| shiftjis | shift_jis | cp932 | はい(上位互換) |
| latin1_declared_utf8 | latin-1(宣言は utf-8) | iso-8859-1 | はい(嘘を無視) |
| latin1_no_decl | latin-1 | cp720 | いいえ |
| cp1252_no_decl | cp1252 | cp862 | いいえ |
| utf8_declared_latin1 | utf-8(宣言は latin-1) | iso-8859-1 | いいえ(宣言に従った) |
8件中5件で復元できました。UTF-8、BOM付きUTF-16、GBK、Shift-JIS、そして誤記されたlatin-1まで正しく戻せました。さらに、GBK→gb18030、Shift-JIS→cp932 のような上位互換推定でも問題なくデコードできます。失敗パターンも知っておく価値があります。短い latin-1 / cp1252 のバイト列は DOS系コードページと誤認されがちです。短い入力では統計的検出が当てになりにくく、DOSの罫線文字とlatin-1のコードポイントが重なるためです。また、<meta charset> の宣言自体が間違っている場合、UnicodeDammit はその宣言を信じます。bs4 のドキュメントでも、サンプルが「so short that Unicode, Dammit can't get a lock on it」となり得ることや、データ量が多いほど推定が良くなることが示されています。
selectolax は非UTF-8バイトを黙って壊し、ユーザー側でデコードする前提ですが、それと比べるとこれは本物の利点です。bs4 は少なくとも推定を試み、多くの場合うまくいきます。ただし保証ではありません。既知の文字コードなら、推定に頼らず明示してください。BeautifulSoup(bytes, from_encoding="...") を使いましょう。
実ページではバックエンドが本当に食い違うのか?
壊れたHTMLではバックエンド差が出ました。次に気になるのは、それが実世界でも意味を持つかどうかです。そこで BBC、Wikipedia、Craigslist、MDN、old.reddit、Python docs、Hacker News、Books to Scrape、webscraper.io、whitehouse.gov、そしてJSレンダリングされたquotesページの11実ページを、3つのバックエンドすべてで通し、リンク数・見出し数・画像数を比較しました。
結果は全11ページで一致。差分はゼロでした。つまり、前述の罠セクションで見たバックエンド差は、意図的に壊れたHTMLでしか現れません。現代の本番サイトが、たとえ多少「雑」でも、十分に構造化されているなら、どのバックエンドでも抽出結果は変わりません。実務的には、一般的で整ったサイトなら html.parser で十分で、依存も増えません。バックエンド選択が結果に効いてくるのは、見た目にも非標準、手書き、あるいは古いHTMLをスクレイプするときです。そのとき初めて lxml か html5lib に切り替える価値が出ます。
このテストで見つかった、もうひとつの端的な例もあります。MDNのページには <template> 要素が含まれており、bs4 の全バックエンドは508個のリンクを返しました。つまり bs4 は <template> の中身をメインツリーに平坦化します。これは lxml と同じ側の挙動で、HTML5仕様に厳密な selectolax-Lexbor とは逆です。Lexbor は <template> を inert な DocumentFragment として扱い、内部の11個のリンクを静かに落として497個しか返しません。つまり bs4 は <template> 内のデータを拾えます。便利ですが、ブラウザでは表示されない「幻のコンテンツ」を拾う可能性もある、ということです。どちらが正しいというより、仕様解釈が違うので、どちらを取っているかを知っておくべきです。
BeautifulSoup の居場所 — そして居場所ではないところ
これらを0〜100の単一スコアに押し込むと、重要なトレードオフが隠れてしまいます。なので、各次元ごとのスコアカードとして整理します。各行に注意点も添えました。
| 次元 | テストで分かったこと | 読者への注意 |
|---|---|---|
| インストール / 初回起動 | 純粋なラッパーでブラウザ設定不要。html.parser は追加依存なし。事前ビルドwheelあり | lxml バックエンドはC依存が必要 |
| Cパーサー比の速度 | 12〜17倍遅い(html.parser) / 10.5〜14倍(lxmlバックエンド)、全サイズで同様 | 単一環境、selectolaxデータを再利用 |
| CSSクエリスループット | 10万ノードで約6〜7.5倍遅い。lxmlバックエンドでも救えない | 再利用データ。Python Tagコストを払う |
| メモリ | selectolax/lxmlの1.5〜1.75倍。最も重い | 再利用。RSSで測定 |
| import時のコールドスタート | 2.36倍遅い(33.4 ms 対 14.1 ms) | 再利用。小さな項目 |
| スレッドスケーリング | bs4-lxml は4スレッドで約3.9倍 遅い(GIL保持) | 単一観測。並列化するなら multiprocessing |
| APIの使いやすさ | 29/29プローブ成功。関数述語find + 双方向ナビゲーション | 空文字の真偽値属性と strip の単語境界に注意 |
| CSS対応 | soupsieve が最強。基本41/41 + 拡張20/20。:lang に対応 | XPath なし、::text なし |
| 3バックエンドの耐性 | lxml/html5lib は15/15、html.parser は12/15 | 差が出るのは壊れたHTMLのみ |
| 実ページでの一致 | 3バックエンドは11/11で一致。<template> は全て平坦化(508) | 整ったサイトではバックエンド差なし |
| 参照サイクルGC | ツリーはサイクル。300ループで120,900個残留、collect でゼロ化 | 長いループでは decompose() が必要 |
| エンコーディング | UnicodeDammit は8件中5件復元。短いサンプルを誤認し、誤った宣言には従う | 単一観測 |
| 保守状況 | アクティブ(4.15.0、2026年6月)、MIT | crummy/Launchpad 上で運用、GitHubではない |
では、BeautifulSoup は誰のためのものか。読みやすいAPIと、寛容なパースを、純粋なスループットよりも重視する人です。中規模の用途、試作、単発スクレイピング、社内ツール、実行時間より開発者工数のほうが高いチームに向いています。逆に、どこを見るべきでないか。速度コストが積み上がって本当に金額になる100万ページ級のパイプライン、スレッドレベル並列が必要な仕事、XPathに強く依存している人です。
実際のスクレイピングスタックの中でどこに位置するか、そして私たちのツールがどこに入るかについても一言。BeautifulSoup は、すでにHTMLを持っていることが前提です。ページ取得も、JavaScriptレンダリングも、ボット対策も、CAPTCHAも扱いません。そこは別レイヤーで、しかも現代のWebでは本当に難しい仕事です。ここにAIスクレイピングAPIは別の層として入ります。Thunderbit の開発者向けスタック — REST API、MCPサーバー、CLI — は、取得、JSレンダリング、アンチボット問題を処理し、あなたがセレクタを書くことなく、クリーンなMarkdown(POST /distill)またはスキーマ一致の構造化JSON(POST /extract)を返します。両者は競合ではなく補完関係です。bs4 はすでに持っているHTMLを解析し、Thunderbit の API、MCP、CLI は、そもそも簡単には届かないHTMLを取りに行きます。ボトルネックがパースなら bs4 は良い答えです。ボトルネックが取得なら、話は別レイヤーです。
結論
BeautifulSoup は、この比較の中で最も親しみやすいAPI、最も強い壊れHTML耐性、そして最も完全なCSSエンジンを提供します。その代わりとして、ほぼ1桁台の速度コストと、最も重いメモリ使用量を支払います。それがトレードオフの全体像です。はっきり言えば、最大の罠はデフォルトの html.parser です。閉じていないテーブルやリストを黙って壊すので、入力が汚れている可能性があるなら必ず "lxml" か "html5lib" を指定してください。スレッドでは速くなりません。並列化するなら multiprocessing です。長いループでは各ページで decompose() し、参照サイクルを溜めないようにしてください。
最後に2つだけ制約を挙げます。ここで使った数値はすべて単一プラットフォーム(macOS arm64、Python 3.14、事前ビルドwheel)上で測定したもので、速度倍率は selectolax のベンチマーク環境から再利用したものです(同一ベンチ、2026-07-13時点)。そのため、Linux x86_64 やソースビルド環境では、正確な数値が変わる可能性があります。そして、ここで出てきた挙動は何ひとつ新発見ではありません。bs4 は20年選手のライブラリなので、テストした挙動はすべて文書化済みか公開記録のあるものです。価値はスクープではなく、ドキュメントが定性的にしか語らないトレードオフに、実数を与えたことにあります。
よくある質問
BeautifulSoup は遅いですか?
はい、測定可能なレベルで遅いです。パース+抽出のタスクでは、デフォルトの html.parser バックエンドだと selectolax-Lexbor のようなCパーサーより約12〜17倍遅く、lxml バックエンドでも約10.5〜14倍遅いです。理由は、各ノードごとにPythonオブジェクトを作るからです。これが問題になるかは規模次第です。1 MBページでは232 ms 対 15 ms で、数千ページならほぼ気になりませんが、100万ページ級のパイプラインでは決定的です。
BeautifulSoup では html.parser、lxml、html5lib のどれを使うべきですか?
整った一般的なサイトなら、デフォルトの html.parser で十分で、依存も増えません。ただしHTML5の省略可能な終了タグには対応していないので、閉じていないテーブルやリストではエラーを出さずに隣接テキストを混ぜてしまいます。入力が壊れている可能性、手書きHTML、古いHTMLがあるなら、"lxml" か "html5lib" を明示してください。壊れたHTMLの15ケースでは、両者とも15/15、html.parser は12/15でした。
BeautifulSoup はスレッドで並列処理できますか?
いいえ。bs4 のツリー構築は純PythonでGILを保持するため、スレッドを増やすと速くなるどころか遅くなります。実測では、1 MBのパースが4スレッドで約3.9倍遅くなりました。bs4 を並列化したいなら multiprocessing(ProcessPoolExecutor)を使ってください。selectolax や lxml のようなCコアを持つライブラリは、スレッド並列の恩恵を受けられます。
BeautifulSoup は壊れたHTMLに強いですか?
概ね強いです。誤った入れ子、骨組み不足、引用符なし属性など、幅広い壊れHTMLに対して、3つのバックエンドすべてがきれいに復元しました。弱点はデフォルトの html.parser と、省略可能な終了タグです。閉じていない <td> / <li> は閉じずに入れ子になり、抽出テキストを壊します。lxml か html5lib に切り替えれば、その種の問題は解消します。
BeautifulSoup と lxml はどちらが優れていますか?
用途が違います。lxml はツリー構築もクエリもずっと高速で、XPathにも対応しています。BeautifulSoup は lxml を含むパーサー群の上に、はるかに親しみやすいAPIを載せており、soupsieve によってCSS対応も広いです。ただし、lxml バックエンドを使っても bs4 が lxml 並みに速くなるわけではありません。バックエンドが速くするのはパース部分だけで、クエリやトラバースは依然として bs4 のノードごとのPythonオブジェクトコストを払うため、大量選択ではなお約6〜7.5倍遅いままです。
Webデータ抽出に Thunderbit を試す Get Started Free


