PyQueryはjQuery風の記法を追加するが、このベンチマークでは選択処理のオーバーヘッドは意思決定に影響しなかった

最終更新日 August 18, 2026
PyQueryはjQuery風の記法を追加するが、このベンチマークでは選択処理のオーバーヘッドは意思決定に影響しなかった
AI要約
PyQuery は lxml の上に jQuery 風の API を載せたライブラリです。1 KB から 10 MB までの 5 つのページサイズで、表示された中央値はすべて素の lxml を下回り、最大サイズでも差は 1.5% でした。ただし、このベンチマークからラッパーのほうが速いとは断定できません。selector と read の判断を変えるほど大きな差は見つかっていないからです。また、表示された中央値では 10 KB 以上で selectolax とも数パーセント以内でした。あらかじめ同等とみなす許容幅を決めていない以上、これは統計的な引き分けではなく、かなり近い結果です。

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

System diagram: jQuery Syntax Over lxml

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 です。

ページサイズselectolaxPyQuerylxmlPyQuery vs lxml
1 KB0.0286 ms0.0456 ms0.0508 ms0.90×
10 KB0.1725 ms0.1728 ms0.1802 ms0.96×
100 KB1.4855 ms1.4093 ms1.4145 ms1.00×
1 MB14.97 ms14.96 ms15.03 ms1.00×
10 MB158.10 ms162.86 ms165.25 ms0.99×

p50 ミリ秒、3 回の中央値、すべて同一プロセス内。parser-bench.json。全サイズで 3 回すべての内容ハッシュが基準と一致しました。

ないのは“ラッパー税”

Measured results chart: PyQuery and lxml on the same fixture

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% でした。これらの小さな差は、実務上の判断材料にはなっても、統計的な同等性の主張にはなりません。

System diagram: Wrapper and Parser Boundaries

仕組みは単純です。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
lxml172.93 ms
parsel231.85 ms
selectolax (modest)247.95 ms
BeautifulSoup + lxml2,261.56 ms
BeautifulSoup + html.parser2,788.75 ms

公開値は bench_parse.json より。

この過去ベンチの行を見ると、BeautifulSoup はこの fixture で高速パーサーの中央値より 1 桁以上遅いことがわかります。ただし、これらの行は現在のプロセスで PyQuery/lxml を並べた条件では再実行していないため、主結論に対する補足情報であって、厳密な倍率比較ではありません。

cheerio の保存済み結果は 2,927.89 ms でした(parser-bench.json 内の 2927.8857)。抽出内容のハッシュも一致しています。ただし、このクロスランタイム結果は Node、パッケージのバージョン、当時の実行条件に依存するため、単体のライブラリ速度倍率として読むべきではありません。

セットアップ上の現実

ライブラリパッケージ数ディスク使用量ライセンススター数最終 push
PyQuery320.1 MiBBSD2,3802026-07-27
cheerio (Node)22 (npm)9.0 MiBMIT30,4492026-08-11

公式リファレンス: PyQuery on PyPI

metadata-snapshot.json

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 peak10 MB peak
html2textpython3.1418.719.971.2
pyquerypython3.1430.333.9172.5
resiliparsepython3.1420.525.1225.1
markdownifypython3.1423.928.9278.5
goose3python3.1444.152.4398.5
cheerionode2266.876.5398.5
justextpython3.1430.336.6431.2
newspaper4kpython3.1452.661.8668.5
trafilaturapython3.1452.564.8927.1
turndownnode2247.868.42947.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 が失われるかを確認してください。

Thunderbit で Web データ抽出を試す

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 のタイミングも、なお順位付けしていません。

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

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

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