PyQuery は lxml の上に jQuery 風の API を載せたライブラリです。1 KB から 10 MB までの 5 つのページサイズを比べたところ、表示された中央値はすべて素の lxml を下回り、最大サイズでも差は 1.5% でした。ただし、このベンチマークから「ラッパーのほうが速い」とは断定できません。今回の selector と read の選択を変えるほど大きな差は見つからなかった、というのが結論です。
また、表示された中央値では 10 KB 以上の範囲で selectolax と比べても数パーセント以内に収まっていました。あらかじめ同等とみなす許容幅を決めていない以上、これは“ほぼ同じ”という印象に近く、統計的な引き分けとまでは言えません。
PyQuery とは
PyQuery は、lxml の文書ツリーに対して jQuery のようなセレクターとチェーン操作を使える Python ライブラリです。検証したバージョンは 2.1.0、BSD ライセンス、GitHub スター 2,380、公開 issue 59 件、最終 push は 2026-07-27 です。
公式リファレンス: PyQuery documentation

from pyquery import PyQuery as pq
d = pq(html)
titles = [e.text_content() for e in d("h3.title")]
hrefs = [e.get("href") for e in d("a")]
PyQuery が返す要素は lxml の要素なので、lxml でできることはそのまま使えます。ここが設計のポイントで、PyQuery はパーサーではなく操作性を高めるレイヤーです。pip install pyquery で入るのは 3 パッケージ — lxml、cssselect、そして PyQuery 本体 — で、サイズは 20.1 MiB。その大半は lxml のコンパイル済み拡張です。
Node で cheerio を使ったことがあるなら、Python 版でも同じ発想だと考えるとわかりやすいでしょう。ここで試した 2 種類のセレクターは両方で動きましたが、cssselect と cheerio の selector 言語が完全に同等だと証明したわけではありません。
測定方法
この検証のベースになっているパーサー比較には、多くのベンチマークにない特徴があります。それは 抽出結果をハッシュして照合する同値性ゲート があることです。つまり、タイトルをソートしたものと href をソートしたものを基準パーサーと照合するため、余計な処理を省いただけのパーサーが“速く見える”ことはありません。ページサイズは 5 種類、各 50 回、独立した 3 回の実行です。
ここに PyQuery を追加するには、2 つの作業が必要でした。
基準の再実行。 selectolax を同じプロセスで再度動かしました。内容ハッシュは 5/5 サイズ で再現され、p50 は公開済みの値の 0.989×〜1.079× に収まりました。つまり、同じマシン、同じテスト環境です。
lxml も同じプロセスで実行。 公開ベンチにはマシン情報と Python バージョンは記録されていますが、ライブラリのバージョンは記録されていません。そのため、公開された lxml の行が、PyQuery が内部で使っているものとは別の lxml リリースだった可能性があります。そこをまたいで比較すると、2 つの lxml バージョンを比べたのに“ラッパーの追加コスト”と見なしてしまう恐れがあります。今回は lxml も並べて実行したことで、その曖昧さを解消しました。どちらもこの仮想環境では lxml 6.1.1 です。
| ページサイズ | selectolax | PyQuery | lxml | PyQuery vs lxml |
|---|---|---|---|---|
| 1 KB | 0.0286 ms | 0.0456 ms | 0.0508 ms | 0.90× |
| 10 KB | 0.1725 ms | 0.1728 ms | 0.1802 ms | 0.96× |
| 100 KB | 1.4855 ms | 1.4093 ms | 1.4145 ms | 1.00× |
| 1 MB | 14.97 ms | 14.96 ms | 15.03 ms | 1.00× |
| 10 MB | 158.10 ms | 162.86 ms | 165.25 ms | 0.99× |
p50 ミリ秒、3 回の中央値、すべて同一プロセス内。parser-bench.json。全サイズで 3 回すべての内容ハッシュが基準と一致しました。
ないのは“ラッパー税”

PyQuery は、表示されたすべての中央値で raw lxml 以下 でした。だからといって、ラッパーがパースを高速化するとは言えません。3 回実行した中央値しかなく、同値とみなす閾値も事前に決めていないため、ここから言えるのは「この条件では、意思決定を左右するほどの selector オーバーヘッドは見られなかった」ということです。
10 MB では PyQuery の 3 回の実行が 162.86、163.17、161.13 ms、lxml は 169.46、165.25、164.18 ms でした。範囲は近いものの重なってはいません。1 MB では 2 つの中央値の差は 0.5% でした。これらの小さな差は、実務上の判断材料にはなっても、統計的な同等性の主張にはなりません。

仕組みは単純です。pq(html) が lxml のツリーを一度だけ構築し、d("h3.title") は cssselect を通して CSS セレクターを tree.cssselect() と同じようにコンパイルし、返ってくる要素は lxml の要素です。つまり、今回の測定対象のホットパスでは PyQuery が行う処理はごくわずかです。走査、要素操作、繰り返しクエリ、import、メモリ使用量は、セレクターの実行時間の主張には含まれていません。
10 KB 以上ではかなり近い
より重要なのは最初の列です。
10 KB 以上では、selectolax、lxml、PyQuery の最速から最遅までの中央値差は、10 KB で 4.5%、100 KB で 5.4%、1 MB で 0.5%、10 MB で 4.5% でした。今回は同値性テストではないため、実務的には、これらの差でこのワークロードにおけるパーサー選定が変わることは少ない、というのが妥当な見方です。
1 KB では selectolax が本当に速く、0.0286 ms に対して PyQuery は 0.0456 ms、lxml は 0.0508 ms でした。ただしこの行は判断材料としては使えません。同サイズ内の 3 パーサーの差は 77.6% にもなり、selectolax 自身の 3 回の実行でも 0.0267〜0.0404 ms と幅があります。28 マイクロ秒程度では、タイマーとスケジューラの影響が支配的です。ここで順位付けするつもりはありません。
この selector-and-read のワークロードでは、速度の先入観ではなく、API の好みと実測した依存関係で選ぶのがよいでしょう。PyQuery は lxml に対して意思決定を変えるほどの不利を示しませんでした。selectolax は別のパーサースタックを使いますが、この記事ではインストール済みサイズ、wheel の対応範囲、ビルド要件を同じ基準で測っていません。
参考までに、公開ベンチは同じ 10 MB の fixture でさらに 2 つの Python パターンも示しており、そちらのほうが差は明確です。
| パーサー (10 MB) | p50 |
|---|---|
| selectolax (lexbor) | 159.93 ms |
| lxml | 172.93 ms |
| parsel | 231.85 ms |
| selectolax (modest) | 247.95 ms |
| BeautifulSoup + lxml | 2,261.56 ms |
| BeautifulSoup + html.parser | 2,788.75 ms |
公開値は bench_parse.json より。
この過去ベンチの行を見ると、BeautifulSoup はこの fixture で高速パーサーの中央値より 1 桁以上遅いことがわかります。ただし、これらの行は現在のプロセスで PyQuery/lxml を並べた条件では再実行していないため、主結論に対する補足情報であって、厳密な倍率比較ではありません。
cheerio の保存済み結果は 2,927.89 ms でした(parser-bench.json 内の 2927.8857)。抽出内容のハッシュも一致しています。ただし、このクロスランタイム結果は Node、パッケージのバージョン、当時の実行条件に依存するため、単体のライブラリ速度倍率として読むべきではありません。
セットアップ上の現実
| ライブラリ | パッケージ数 | ディスク使用量 | ライセンス | スター数 | 最終 push |
|---|---|---|---|---|---|
| PyQuery | 3 | 20.1 MiB | BSD | 2,380 | 2026-07-27 |
| cheerio (Node) | 22 (npm) | 9.0 MiB | MIT | 30,449 | 2026-08-11 |
公式リファレンス: PyQuery on PyPI
3 パッケージという依存関係はすっきりしていますし、そのうち 2 つ — lxml と cssselect — は多くの Python スクレイピング案件ですでに使われているはずです。その意味では、PyQuery の追加コストは数十キロバイト程度です。
20.1 MiB は PyQuery ではなく lxml のコンパイル済み拡張です。lxml を直接使う場合と同じ約 20 MiB を支払うことになります。
ライセンスは BSD です。取得時点では未解決 issue が 59 件あり、テストの 3 週間前に push がありましたが、それだけで保守品質や将来の互換性を判断することはできません。
メモリと、壊れた HTML がどう影響するか
このシリーズで未検証だった 2 点を、今回は測定しました。
より広い負荷テストの文脈は、10 ライブラリのメモリと不正 HTML の比較 にあります。
ピーク常駐メモリ は /usr/bin/time -l で測定し、セルごとに新しいプロセスを起動しました。import 時の下限は“ライブラリを読み込んで待機しているだけ”のコスト、ピーク値は文書込みのコストです。
| ライブラリ | 実行環境 | import 下限 | 226 KB peak | 10 MB peak |
|---|---|---|---|---|
| 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 のベースラインは互いに直接比較できません。どちらにもインタプリタが含まれているためです。
PyQuery は 大きな文書では resiliparse より軽量 です。import 下限は高めですが、10 MB 文書でのピークは 172.5 MiB に対して resiliparse は 225.1 MiB でした。lxml のツリーはコンパクトで、PyQuery の 30.3 MiB の下限の大半は PyQuery 自体ではなく lxml の読み込みによるものです。
壊れた HTML については、12 個の文書を使いました。内容は、閉じていないタグ、入れ子が崩れたインライン要素、スペースを含む未引用属性、余計な閉じタグ、<html> がまったくないもの、重複属性、タグの途中で切れた文書、壊れた entity、閉じられていない <script>、嘘の charset 宣言、マークアップを含むコメント、600 階層の入れ子です。さらに、同じサイズの正常な文書を 2 つの対照群 として加えました。というのも、「何も返らなかった」だけでは、ライブラリが正常文書でも黙ってしまうなら、壊れ具合について何も言えないからです。
pyquery は 14 件中 0 件で例外を投げず、1 件で何も返さず、壊れた fixture 全体で 10/22 のシグナル を回収しました (malformed-results.json)。この数え上げには 1 件除外があります。HTML5 の仕様では、閉じられていない <script> の後ろはすべて script content なので、そこで失われるのは正しい挙動で、回収できるほうが逸脱です。ただし、他の候補と並べたセントネル結果がないため、10/22 は頑健性の観察であって、パーサー選定の順位付けではありません。
長所と短所
良い点。 jQuery 風の記法なので、フロントエンド JavaScript に慣れている人や cheerio を使ったことがある人には親しみやすいです。この条件では、素の lxml に対して意思決定を変えるほどの selector オーバーヘッドは見られませんでした。パッケージは 3 つだけで、そのうち 2 つは既にプロジェクト内にあることも多いでしょう。返り値が lxml 要素なので、lxml の技法をそのまま使えます。BSD ライセンスです。5 サイズすべてで内容ハッシュは基準と一致しました。
悪い点。 lxml があるぶん、20.1 MiB のサイズは避けられません。スター数 2,380 は cheerio の 30,449 よりかなり小さく、妙なケースで参照できる実例も少なめです。あくまで便利な薄いレイヤーなので、lxml にできないことは PyQuery でもできません。そして、jQuery API で性能向上を期待していたなら、それは違います。得られるのは使いやすさであり、実際に仕事をしているのは下にあるパーサーです。
どんな人に向くか、向かないか
PyQuery を使うべき人。 Python でも jQuery 風のセレクターを使いたい人、あるいはチームです。その測定では、構築・2 回の選択・読み取りの経路において lxml に対する不利は見られませんでした。なお、PyQuery の他の操作は計測していません。
lxml を直接使うべき人。 XPath を好む人、あるいは依存パッケージを 1 つ減らしたい人です。今回の結果からは、速度面だけでどちらかを選ぶ理由は見つかりませんでした。
selectolax を検討すべき人。 そのパーサー API と依存関係の構成がプロジェクトに合うなら候補です。1 KB の行は明確に順位付けしておらず、この記事は「依存が最小」という主張も支持しません。
Node では、 cheerio が似た API 形状にあたります。保存済みのクロスランタイム結果はここでは遅めでしたが、ランタイムと過去実行の条件差があるため、純粋なライブラリ比較の結論にはできません。
マネージド API が合う場面
PyQuery は、すでに手元にある HTML を解析するためのものです。取得、JavaScript のレンダリング、bot 対策レイヤーは扱いません。ここで比べたパーサーはどれもそこまではできず、実際の対象サイトではその部分のほうが難しいことも多いです。
著者注: Thunderbit は、URL からの取得・レンダリング・抽出をまとめて任せられるマネージド विकल्पです。今回は PyQuery と直接比較していません。重要なのは、HTML をすでに持っていてローカルのセレクターを使いたいのか、それともページ取得と抽出をサービスとして運用したいのか、という境界です。
率直に言えば、HTML を持っていてセレクターがわかっているなら、PyQuery は無料で快適です。逆に、セレクターが壊れ続ける、あるいは大規模に取得する必要があるなら、選ぶべき製品は別です。
より広い比較については、web scraping API の総覧 でホスト型の選択肢を、open-source scraper のまとめ でセルフホスト型の選択肢を紹介しています。抽出結果をモデルに渡すなら、Python で HTML を Markdown に変換する方法 でどこで fidelity が失われるかを確認してください。
PyQuery を使うべきか?
はい。Python で jQuery 風の記法を使いたくて、今回測った selector/read の経路があなたの用途に近いなら、PyQuery は有力です。
このベンチマークでは、内容ハッシュの一致を保ったまま、lxml に対して意思決定を変えるほどの selector オーバーヘッドは見つかりませんでした。ただし、ライブラリ全体のコストがゼロだと証明したわけではありません。
5 つのサイズを通して、3 つの Python パーサーの中央値は十分近く、今回の用途では API の相性が最終判断になりやすいでしょう。この判断をより広いパーサー順位にする前に、同値とみなす閾値を決めて、完全に同じ条件で再測定してください。
Thunderbit で Web データ抽出を試す Get Started Free
よくある質問
PyQuery は lxml を遅くしますか?
この実行では、意思決定を変えるほどの selector オーバーヘッドは見られませんでした。5 つのページサイズすべてで中央値は raw lxml と同等かそれ以下で、どちらも同じプロセス内で lxml 6.1.1 を使っています。10 MB では範囲が近く、重なりはありませんでした。PyQuery は 161.13〜163.17 ms、lxml は 164.18〜169.46 ms です。pq(html) は lxml ツリーを構築し、今回のセレクターは cssselect を通してコンパイルされます。
selectolax は PyQuery より速いですか? 1 KB の中央値は低かったのですが、マイクロ秒単位ではばらつきが支配的なので、その行は順位付けしていません。10 KB 以上では中央値差は 0.5%〜5.4% でした。このワークロードでは十分に近く、どのケースでも同値または範囲が重なることを示すものではありません。
なぜ公開済みの数値をそのまま使わず、lxml を再実行したのですか? 公開ベンチにはマシンと Python バージョンはありますが、ライブラリのバージョンがありません。公開されている lxml の行は、今 PyQuery が包んでいる lxml とは別バージョンだった可能性があり、その差は存在しない“ラッパー税”として見えてしまうおそれがありました。今回は両方を同一プロセスで lxml 6.1.1 として動かし、曖昧さをなくしています。
cheerio とはどう違いますか? API の発想は似ていますが、エコシステムは別です。ここで試した 2 種類のセレクターは両方で動き、5 サイズすべてで内容ハッシュも一致しましたが、それで完全な selector 互換性まで証明できるわけではありません。保存済みの cheerio のタイミングは遅めでしたが、ランタイム差と過去実行条件のため、単体ライブラリの倍率としては読めません。
ここで未検証だったものは何ですか? メモリは、import のみ、226 KB の文書、10 MB の文書でのピーク RSS として測りました。不正 HTML は、12 個の壊れた文書と 2 つの対照群で試しました。それでも未検証なのは、PyQuery の要素操作と走査性能、繰り返しクエリのキャッシュ、URL フェッチ、並列処理、そして実際のサイトに近いワークロードです。1 KB のタイミングも、なお順位付けしていません。


