XPath対CSSセレクタ: ウェブスクレイピングにどちらを使用するか
Advanced Data Extraction Specialist
TL;DR:
- CSSとXPathは通常の抽出に対して同一の結果を返します:同じページで
article.product_pod h3 a::attr(title)と//article[@class='product_pod']/h3/a/@titleから返されたのは同じ20冊の本のタイトルです。 - XPathは2つの中で唯一、ツリーを上向きに歩くことができます。
parent::、ancestor::、preceding-sibling::、following-sibling::はそれぞれそのページで20ノードと一致しました;CSSにはそれらに相当するものはありません。 - 「CSSは速い」というのはブラウザの事実であり、Pythonの事実ではありません。parselでは同じ抽出がCSSで487.1 ms、XPathで309.1 msかかりました。これはcssselectがすべてのCSSクエリをXPathにコンパイルしてから実行するためです。
a:contains('Sharp')はparselで1つのマッチを返し、SyntaxErrorは実際のChromeページ内でスローされます。これが、セレクタがスクレイパーで動作し、DevToolsでは失敗する理由です。- マークアップが決して到着しない場合、どちらの言語も重要ではありません;セレクタは実際に構築されたDOMをクエリすることしかできません。
- 両方の言語は、あなたのフェッチ層がデコードしたバイトを読み取ります。したがって、文字セットなしで提供されたページは、ブラウザが
£47.82を表示する場所で£47.82を返すことがあります。 - Scrapeless無料プラン上の完全にレンダリングされたページに対して、いずれかの構文を実行してください。
任意の製品リストのインスペクタを開き、Chromeが提供するセレクタをコピーしてPythonのスクレイパーに貼り付けると、通常は機能します。興味深いケースは機能しないケースであり、セレクタが1つのエンジンでは有効で、別のエンジンでは構文エラーとなる場合や、あなたが書いた整然としたCSSが避けたXPathよりも遅くなる場合です。
2つの言語は重複していますので、有用な比較は端にあります:それぞれが表現できること、それぞれのコスト、そして実際にクエリを実行しているエンジンはどれかです。
以下のすべての測定は、スクレイピング練習のために構築された1つの公開ページからのものです — books.toscrape.com上の20冊のカテゴリリスト — lxml 6.0.2、cssselect 1.3.0、parsel 1.10.0で解析されました。
両方の言語での同じクエリ
一般的なターゲットについては、2つの言語は互いの直訳です。以下の表は、テストページ上の同一ノードセットを選択するクエリをペアにしています。
| ターゲット | CSS | XPath |
|---|---|---|
| 任意のタグ | article |
//article |
| クラス | .product_pod |
//*[contains(concat(' ',normalize-space(@class),' '),' product_pod ')] |
| ID | #messages |
//*[@id='messages'] |
| 子孫 | article h3 a |
//article//h3//a |
| 直下の子 | div > span |
//div/span |
| 属性値 | a[title] |
//a[@title] |
| N番目の子 | li:nth-child(2) |
//li[2] |
| 属性テキスト | a::attr(href) |
//a/@href |
そのテーブルからの3つのペアが、ライブページに対して実行され、マッチングノードセットを返しました:
python
from parsel import Selector
import requests
url = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
response = requests.get(url, timeout=30)
response.raise_for_status()
sel = Selector(text=response.content.decode("utf-8"))
pairs = [
("article.product_pod h3 a::attr(href)", "//article[@class='product_pod']/h3/a/@href"),
("p.star-rating::attr(class)", "//p[contains(@class,'star-rating')]/@class"),
("div.image_container img::attr(alt)", "//div[@class='image_container']//img/@alt"),
]
for css_query, xpath_query in pairs:
css_hits = sel.css(css_query).getall()
xpath_hits = sel.xpath(xpath_query).getall()
print(len(css_hits), len(xpath_hits), css_hits == xpath_hits)
各ペアは20 20 Trueを表示しました。両方の言語がターゲットを表現できる場合、選択は可読性であり、能力ではありません。
XPathだけが表現できること
CSSは下向きに選択します。祖先から子孫に移動し、決して上には戻らないため、ルールは「このカードの中の価格」と言うことはできますが、「この価格が含まれるカード」とは言えません。
XPathは軸を持ち、そのうちの4つにはCSSに相当するものがまったくありません。それぞれがテストページで20ノードと一致しました:
python
axes = [
("parent::", "//p[@class='price_color']/parent::div/@class"),
("ancestor::", "//h3/ancestor::article/@class"),
("preceding-sibling::", "//div[@class='product_price']/preceding-sibling::h3/a/@title"),
("following-sibling::", "//h3/following-sibling::div[@class='product_price']//p[@class='price_color']/text()"),
]
for label, query in axes:
hits = sel.xpath(query).getall()
print(f"{label:20s} {len(hits):2d} hits {hits[0]!r}")
text
parent:: 20 hits 'product_price'
ancestor:: 20 hits 'product_pod'
preceding-sibling:: 20 hits 'Sharp Objects'
following-sibling:: 20 hits '£47.82'
最後のものが保持する価値のあるパターンです。「見出しを見つけ、それに続く価格を取る」というのは兄弟関係であり、これをCSSで表現することは、価格を別々に選択し、インデックスで2つのリストを再結合することを意味します — これにより、1つのカードに価格がないときに間違ったペアが静かに生成されます。
W3Cセレクタレベル4仕様はCSS文法を定義しており、上向きのトラバーサルは設計上欠如しています。これは、言語がスタイリングのために構築され、レンダラーがルールを上から下に解決するためです。XPath 1.0勧告は、ドキュメントツリー内の任意のノードをアドレス指定するために構築されたため、13の軸を定義しています。
:containsの分岐
テキストマッチングは、2つの言語が分岐する場所であり、混乱を招くバグレポートを生成します。
XPathは常にcontains()を持っていました。CSSは初期のドラフトで:contains()疑似クラスを持っていましたが、仕様が安定する前に削除されました。注目すべきは、Pythonのcssselectはまだそれを実装しているということです。
python
print(len(sel.css("a:contains('Sharp')").getall())) # 1
print(len(sel.xpath("//a[contains(., 'Sharp')]").getall())) # 1
両方は1を印刷します。さて、実際のブラウザページ内の同じ2つのクエリが、DOMの自分自身のエンジンを介して評価されます:
python
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
JS = """() => {
const out = {};
try { out.css_class = document.querySelectorAll('article.product_pod h3 a').length; }
catch (e) { out.css_class = 'ERR ' + e.name; }
try { out.css_contains = document.querySelectorAll("a:contains('Sharp')").length; }
catch (e) { out.css_contains = 'ERR ' + e.name + ': ' + e.message.slice(0, 60); }
const xp = (q) => document.evaluate(q, document, null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null).snapshotLength;
out.xpath_class = xp("//article[@class='product_pod']/h3/a");
out.xpath_contains = xp("//a[contains(., 'Sharp')]");
out.xpath_parent = xp("//p[@class='price_color']/parent::div");
out.xpath_ancestor = xp("//h3/ancestor::article");
return out;
}"""
url = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
endpoint = "wss://browser.scrapeless.com/api/v2/browser?" + urlencode({
"token": os.environ["SCRAPELESS_API_KEY"], "sessionTTL": 300, "proxyCountry": "US",
})
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(endpoint)
page = browser.new_page()
page.goto(url, wait_until="domcontentloaded")
for key, value in page.evaluate(JS).items():
print(f"{key:16s} {value}")
browser.close()
text
css_class 20
css_contains ERR SyntaxError: Failed to execute 'querySelectorAll' on 'Document': 'a:conta
xpath_class 20
xpath_contains 1
xpath_parent 20
xpath_ancestor 20
CSSのテキストマッチはスローされます。XPathのテキストマッチは1つのノードを返します。したがって、Pythonのスクレイパーでテストを通過するセレクタが、DevToolsに貼り付けた瞬間に構文エラーを引き起こします — そして、コンソールでセレクタを検証してから出荷する人にとって逆のことが起こります。
二つのことがその実行から注目すべきこととして浮かび上がります。ブラウザのXPathは劣化していません:parent:: と ancestor:: はそれぞれ DOMのdocument.evaluateインターフェイスを通じて20ノードを解決しました。そして :contains は言語機能ではなくライブラリ拡張であるため、ライブラリが行くところまでしか移動しません。
セレクタが両方の場所で機能しなければならない場合、//a[contains(., 'Sharp')] がポータブルな形式です。
実際に速いのはどちらか
一般的な答えはCSSが速いということです。それはブラウザにおいてquerySelectorAllがネイティブな高速パスであり、Pythonでは逆です。
lxmlにはCSSエンジンがありません。すべてのCSSクエリはcssselectによってXPathに変換され、XPathエンジンがそれを実行します。これはScrapyセレクタのドキュメントが直接述べています。CSSを書くことは翻訳ステップを買うことです。
python
import time
N = 2000
start = time.perf_counter()
for _ in range(N):
sel.css("article.product_pod h3 a::attr(title)").getall()
css_ms = (time.perf_counter() - start) * 1000
start = time.perf_counter()
for _ in range(N):
sel.xpath("//article[@class='product_pod']/h3/a/@title").getall()
xpath_ms = (time.perf_counter() - start) * 1000
print(f"CSS {css_ms:8.1f} ms ({css_ms / N * 1000:6.1f} us/call)")
print(f"XPath {xpath_ms:8.1f} ms ({xpath_ms / N * 1000:6.1f} us/call)")
text
CSS 487.1 ms ( 243.5 us/call)
XPath 309.1 ms ( 154.5 us/call)
CSSコストは同じ出力に対してXPath時間の1.58倍です。翻訳を印刷すると、どこに行くかが示されます:
python
from cssselect import GenericTranslator
print(GenericTranslator().css_to_xpath("article.product_pod h3 a"))
text
descendant-or-self::article[@class and contains(concat(' ', normalize-space(@class), ' '), ' product_pod ')]/descendant-or-self::*/h3/descendant-or-self::*/a
手書きの//article[@class='product_pod']/h3/aは一つの属性を比較します。生成された形式は空白を正規化し、すべての候補ノードでクラスリストをパディングします。なぜなら、複数のクラスを持つ要素に対して正しい必要があるからです。その防御的な述語が1.58倍です。
それを最適化する前にその比率を考慮してください:ギャップは50 KBページでの呼び出しあたり約89マイクロ秒です。単一のHTTPリクエストは何千倍も高価です。セレクタ言語はスクレイパーの時間が使われる場所ではなく、可読性が通常はより良いトレードです — 数字はキャッシュされたHTMLのホットパースループでのみ重要です。
両言語が共有する罠
セレクタはフェッチ層がデコードしたものを返します。サーバーがtext/htmlをcharsetなしで送信すると、リクエストはHTTPセマンティクス仕様のメディアタイプルールの下でISO-8859-1にフォールバックし、ワイヤ上のバイトはUTF-8です:
python
import requests
from parsel import Selector
url = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
response = requests.get(url, timeout=30)
response.raise_for_status()
print(response.headers.get("content-type"))
print(response.encoding)
print(response.apparent_encoding)
print(Selector(text=response.text).css("p.price_color::text").get())
print(Selector(text=response.content.decode("utf-8")).css("p.price_color::text").get())
text
text/html
ISO-8859-1
utf-8
'£47.82'
'£47.82'
セレクタは両方の回で正しかったです。response.contentを明示的にデコードすることは、response.textを信頼するのではなく、抽出された価格を利用可能にするものです — そしてセレクタ言語の変更ではそれを修正しません。
クライアントサイドでレンダリングされるページに対してセレクタをテストするには、実際のブラウザが必要です。Scrapelessの無料プランは、ライブDOMに対して両方の構文を試すのに十分なセッションをカバーしています。
どちらの言語も助けないところ
両方の言語はDOMをクエリします。どちらも一つを構築しません。
リスティングがクライアントサイドでレンダリングされると、平凡なHTTPクライアントが受け取るHTMLにはシェルが含まれており、レコードが含まれていないため、正しいセレクタはゼロノードを返し、セレクタバグのように見えます。修正はセレクタの上流にあります:最初にページをレンダリングし、次にそれをクエリします。Scrapeless Scraping BrowserはCDPを介したクラウドブラウザを公開しますので、ブラウザが組み立てたのと同じページがあなたのセレクタが実行されるページです — これはまさに上記のブラウザ内数字がキャプチャされた方法です。
python
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright
url = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
cdp_endpoint = "wss://browser.scrapeless.com/api/v2/browser?" + urlencode({
"token": os.environ["SCRAPELESS_API_KEY"],
"sessionTTL": 300,
"proxyCountry": "US",
})
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(cdp_endpoint)
page = browser.new_page()
page.goto(url, wait_until="domcontentloaded")
titles = page.eval_on_selector_all(
"article.product_pod h3 a", "els => els.map(e => e.title)"
)
print(len(titles), titles[0])
browser.close()
text
20 Sharp Objects
DOMが存在すると、セレクタの質問はスタイルの質問に戻ります。選択層自体のウォークスルーについては、私たちのparselガイドが一つのlxmlバックツリー上で両方の方言をカバーし、価格設定はレンダリングセッションのコストを列挙します。
CSSを選ぶべき時、XPathを選ぶべき時
| 状況 | 選ぶべきもの |
|---|---|
| クラス、ID、属性、または子孫の一致 | CSS |
| セレクタがフロントエンドのチームメイトと共有されている | CSS |
ルールがDevToolsまたはquerySelectorAllでも実行されなければならない |
CSS、またはポータブルXPath |
| 含まれているものでコンテナを選択 | XPath |
| ラベルをその後の値とペアリング | XPath |
| 表示されるテキストで一致 | XPath |
| 祖先に向かって遡る | XPath |
| XML、RSS、またはサイトマップを解析 | XPath |
| lxmlでのキャッシュされたHTMLに対するホットループ | XPath |
CSSを選ぶべき時 は、ターゲットが下向きにアドレス指定可能で、クエリがスタイルシートを書く人々によって読まれる場合です。短く、テストページでは、すべての普通のフィールドを損失なく選択しました。
XPathを選ぶべき時 は、必要な関係が階層的ではなく構造的である場合 — 木を上って、兄弟の間で、またはテキストに基づいてです。これはまたlxmlとparsel内での正直なデフォルトでもあり、実際に実行されるものです。
ほとんどの作業用スクレイパーは、プロジェクトごとに選択肢を決定するのではなく、フィールドごとに両方を混ぜます。そして、parselは同じツリーに対して両方を受け入れるため、選択肢ごとに決定が行われます。
結論
CSSとXPathは通常のケースに対して同じ質問に答え、20のタイトルは両方から同一に戻ってきました。重要な違いは、通常のフレーミングが示唆するよりも狭いです:XPathは上に移動してテキストを一致させることができますが、CSSはできません。CSSはブラウザでは速く、lxmlでは遅く、そこで最初にXPathにコンパイルされます;そして、:containsは一方のエンジンでは機能しますが、もう一方ではエラーを投げるため、「私のスクレイパーでは動くが、コンソールでは動かない」という混乱の主な原因です。
選択肢ごとに選んで、クエリが両方の場所で実行される必要があるときはポータブルな形式を維持し、文字が壊れたことでどちらの言語を非難する前にレスポンスをデコードします。
クエリを実行する前にページに対してセレクターをテストする準備はできましたか? Scrapelessの無料プランから始める し、いずれかの構文をライブDOMにポイントさせてください。
FAQ
Q: XPathはCSSセレクターより速いですか?
エンジンによります。lxmlとparselでは、XPathが速く、CSSは実行前にXPathにコンパイルされるため、ここで測定されたギャップは309.1 ms対487.1 msで、同じ抽出の2000回の繰り返しの間でのことです。ブラウザでは、querySelectorAllはネイティブな高速パスであり、CSSが勝ちます。いずれにせよ、違いは呼び出しごとにマイクロ秒で、HTTPリクエストのコストからははるかに低いです。
Q: CSSセレクターは要素のテキストを一致させることができますか?
ブラウザではできません。document.querySelectorAll("a:contains('x')")はSyntaxErrorを投げます、なぜなら:contains()はセレクター仕様が安定化する前にドロップされたからです。Pythonのcssselectはまだそれを実装しているので、同じクエリがparselで動作します。両方で同一の動作をするセレクターを使用するには、//a[contains(., 'x')]を使用してください。
Q: CSSセレクターは親要素を選択できますか?
いいえ。CSSは下方のみを選択するので、parent::やancestor::に相当するものはありません。XPathは両方を処理し、テストページでは//h3/ancestor::articleがすべての20枚のカードに一致しました。:has()擬似クラスを使用すると、要素の子孫に基づいてフィルタできますが、内側の要素から上へ遡るのではなく、外側の要素を返します。
Q: なぜ私のセレクターはDevToolsで機能するのに、Pythonで何も返さないのですか?
2つの一般的な原因があります。ページはJavaScriptでコンテンツをレンダリングするため、HTTPクライアントが受け取ったHTMLには、ブラウザが後に構築するノードは含まれていません — セレクターは正しいですが、DOMが存在しません。または、セレクターがブラウザ専用またはライブラリ専用の拡張を使用しています。セレクターを変更する前に、原始的なレスポンスボディを検査ツールが表示する内容と比較してください。
Q: ScrapyでCSSまたはXPathを使用すべきですか?
フィールドごとに両方。Scrapyはresponse.css()とresponse.xpath()を同じparselセレクターの上に公開しており、チェーンにすることができます。ストレートフォワードなクラスと属性の一致にはCSSを使用し、テキストマッチングや上方移動にはXPathに切り替えて、CSSルールを適合させるのではなく使いましょう。
Q: XPathセレクターはすべてのブラウザで機能しますか?
はい、document.evaluateを経由してquerySelectorAllではなく機能します。これは別のDOMインターフェースであり、軸が完全にサポートされています — parent::とancestor::はそれぞれ上の実行で20ノードを返しました。Chrome DevToolsの$x()ヘルパーは、コンソール使用のために同じインターフェースをラッピングしています。
Scrapelessでは、適用される法律、規制、およびWebサイトのプライバシーポリシーを厳密に遵守しながら、公開されているデータのみにアクセスします。 このブログのコンテンツは、デモンストレーションのみを目的としており、違法または侵害の活動は含まれません。 このブログまたはサードパーティのリンクからの情報の使用に対するすべての責任を保証せず、放棄します。 スクレイピング活動に従事する前に、法律顧問に相談し、ターゲットウェブサイトの利用規約を確認するか、必要な許可を取得してください。



