この記事の執筆者について、そしてそれがここではほとんど重要ではない理由について一言:私たちは Geonode であり、データ抽出作業を行う方々にプロキシを販売しています。 セレクタの選択は、当社が販売する製品には一切影響しません。言及する価値がある唯一の共通点は、診断上の点だけです。つまり、何にも一致しないセレクタと、ブロックされたリクエストは、外部から見れば同じように見えますが、これらは全く無関係な問題です。 ページが正常に表示され、セレクタが何も見つからなかった場合、このリファレンスが適切な情報源であり、ページの取得方法について変更すべき点は一切ありません。
基本的なセレクタ
| セレクタ | 一致する要素 |
|---|---|
* | すべての要素 |
div | div 要素 |
.card | クラス「card」を持つ要素 |
#main | ID「main」を持つ要素 |
.card.featured | 両方のクラスを持つ要素 |
div.card | クラス「card」を持つdiv要素 |
div, p | クラス「div」を持つ要素 および クラス「p」を持つ要素 |
このうち2つについて詳しく見ておく価値があります。
** スペースを含まない「.card.featured」は、1つの要素に両方のクラスが付与されていることを意味します。** スペースを含む「.card .featured」は、クラス「featured」を持つ要素が、クラス「card」を持つ要素の中に含まれていることを意味します。たった1文字の違いで意味が完全に変わってしまいますが、これはこの分野で最もよくあるタイプミスです。
div, p はセレクタリストであり、関係性を表すものではありません。これはどちらのグループ内の要素にも一致し、各部分は独立して評価されます。したがって、div, p.note は、クラス note を持つ div ではなく、クラス note を持つすべての div および段落に一致します。
コンビネータ
これらは要素間の関係を表現します。
| セレクタ | 一致条件 |
|---|---|
div p | div 内の任意の位置にある p(子孫) |
div > p | div の直系の子である p |
h2 + p | h2 の直後に位置する p(隣接する兄弟要素) |
h2 ~ p | 同じ親要素を持つh2の直後に続くすべてのp(一般の兄弟要素) |
子孫コンバイナはスペースであるため、誤って挿入されやすく、読み飛ばされやすい。
> の方がより正確で、通常は堅牢です。div > p は、来月誰かが <section> で囲んだ段落には一致しません。これは欠点のように聞こえますが、実際には逆の場合が多いのです。なぜなら、意図したよりも多くの要素に黙って一致し始めるセレクタは、一致を停止してそのことを知らせてくれるセレクタよりも悪いからです。
+ と ~ は兄弟要素コンバイナであり、前方のみを参照します。 CSSには「前の兄弟要素」コンバイナーは存在しません。:has() を使用することで間接的に同様の効果を得ることができますが、これについては後述します。
属性セレクタ
属性は通常、意図的に設定されるため、多くの場合、最も安定した選択肢となります。
| セレクタ | 一致条件 |
|---|---|
[href] | href 属性を持つ要素 |
[href="/about"] | 完全一致する値 |
[href^="/docs"] | 値が で始まる |
[href$=".pdf"] | 値が で終わる |
[href*="download"] | 値に が含まれる |
[class~="card"] | 値が card という単語を含むスペース区切りのリストである |
[lang|="en"] | 値が en であるか、または en- で始まる |
[data-state="open" i] | 大文字小文字を区別しない一致 |
このうち2つはあまり活用されていません。
[class~="card"] は .card と完全に同等です — ~= 演算子は、空白で区切られた単語のマッチングを行い、これは class 属性に必要な動作です。この存在を知っておくことは、主に .card が実際に何を行うのかを理解するため、および変数に属性名が入っている場合にプログラムでセレクタを構築するために役立ちます。
iフラグを指定すると、大文字小文字を区別せずに一致させることができます。これは、サイト全体で大文字小文字が混在する属性値がある場合に、非常に便利です。
特にデータ抽出に関しては、data-*属性はclass属性よりも強く推奨されます。[data-testid="product-card"]は開発者が安定したフックとして選択したものです。一方、クラス名はデプロイのたびにビルドツールによって再生成される可能性があります。
疑似クラス:構造
| セレクタ | 一致条件 |
|---|---|
:first-child | 親要素の最初の子要素である要素 |
:last-child | 最後の子要素 |
:only-child | 兄弟要素を持たない要素 |
:nth-child(3) | 3番目の子要素 |
:nth-child(2n) | 偶数番目の子要素 |
:nth-child(2n+1) | 奇数番目の子要素 |
:nth-child(-n+3) | 最初の3つ |
:nth-last-child(2) | 最後から2番目 |
:first-of-type | 兄弟要素の中でそのタイプに属する最初の要素 |
:nth-of-type(2) | そのタイプに属する2番目の要素 |
:empty | テキストを含め、子要素を持たない要素 |
:root | ドキュメントのルート(通常は html) |
-of-type のバリエーションは、多くの人が思っている以上に重要です。 p:first-child は、その段落が親要素の最初の子要素である場合にのみ一致します。つまり、見出しが先に来ている場合は、何も一致しません。p:first-of-type は、その前に何があるかに関係なく、最初の段落に一致します。データを抽出する際、ほとんどの場合、意図しているのは後者の方です。
「最初の3つ」を表す:nth-child(-n+3) は、An+B の構文の中で覚えておく価値のある慣用表現であり、「4つ目以降」を表す :nth-child(n+4) も同様です。これらを組み合わせると範囲指定が可能になります。:nth-child(n+2):nth-child(-n+5) は、2番目から5番目までの子要素を選択します。
疑似クラス:論理
ここ数年で最も変化の大きかったグループです。
| セレクタ | 一致条件 |
|---|---|
:not(.hidden) | クラス「hidden」を持たない要素 |
:is(h1, h2, h3) | リストされたセレクタのいずれか |
:where(h1, h2, h3) | :is() と同じだが、特異度はゼロ |
:has(> img) | 一致する子要素または兄弟要素を含む要素 |
:is() および :where() は、繰り返しの多いリストを短縮します。:is(article, section) > h2 は article > h2, section > h2 に置き換わり、リストが長くなるほど短縮効果は急速に大きくなります。 これら間の唯一の違いは特異度です。:is()は最も特異度の高い引数の特異度を採用しますが、:where()は常に特異度ゼロとなります。
:not()は、最新のブラウザではセレクタリストを受け付けます。そのため、:not(.a, .b)は機能し、「いずれも該当しない」という意味になります。以前の単一引数の動作では、:not(.a):not(.b)をチェーンする必要がありました。
:has() が重要なものです。 MDNのドキュメント では、これを「引数として渡された相対セレクタのいずれかが、この要素を基準として少なくとも1つの要素と一致する場合、その要素を表す」ものと説明しており、「参照要素に対する親要素または前の兄弟要素を選択する方法」を提供するとされています。 そのステータスは「Baseline widely available」であり、2023年12月以降、各ブラウザでサポートされています。
div.card:has(span.sold-out) /* a card containing a sold-out marker */
h1:has(+ p) /* an h1 immediately followed by a p */
li:has(~ li.active) /* an li with a later active sibling */
label:has(input:checked) /* a label wrapping a checked input */
文書化されている制限は3つあります。すなわち、別の:has()内に「ネストできない」、擬似要素をその内部やアンカーとして使用することは無効である、そしてその特異度は:is()や:not()と同様に、最も特異度の高い引数に従うということです。
疑似クラス:state と form
| セレクタ | 一致条件 |
|---|---|
:hover, :focus, :active | インタラクションの状態 |
:focus-visible | 目に見えるリングを表示すべきフォーカス |
:focus-within | フォーカスされた子要素を含む要素 |
:checked | チェックされたチェックボックス、ラジオボタン、または選択されたオプション |
:disabled, :enabled | フォームコントロールの状態 |
:required, :optional | フォームの検証属性 |
:valid, :invalid | 制約の検証状態 |
:placeholder-shown | プレースホルダーが表示されている入力フィールド |
:target | URLフラグメントに一致する要素 |
:visited, :link | リンクの状態 |
これらのおおむねは、データ抽出というよりはスタイルに関する問題ですが、2つの例外があります。:checkedは、テスト自動化においてフォームの状態を読み取るのに非常に有用であり、:disabledは、コントロールがインタラクティブであるかどうかを示します。これは、クリックしても何も起こらなかった理由を説明するケースが頻繁にあります。
なお、:hover および :focus は、スクレイピングの文脈において querySelectorAll を使用して有効なクエリを行うことはできません。なぜなら、単に解析しただけのドキュメントには、これらの状態は存在しないからです。
特異性について(概要)
セレクタ間でスタイルの適用が競合する場合に重要ですが、クエリを行う場合には関係ありません。この2つの文脈を混同することが多いため、知っておく価値があります。
特異性は3つの数値で算出されます:
| 構成要素 | 算出の根拠 |
|---|---|
| ID | #main |
| クラス、属性、疑似クラス | .card, [href], :hover |
| 要素、疑似要素 | div, ::before |
左から右へと比較すると、いくら多くのクラスがあっても、単一のIDには決して勝てません。インラインスタイルはこれらすべてに勝り、!importantはさらにそれを上回ります。そのため、これら2つはツールというよりは、あくまで最後の手段となります。
2つの現代的な注意点。:where()は特異性を一切持たないため、利用者が難なく上書きできるライブラリのデフォルト設定には最適な選択肢となります。また、:is()、:not()、:has()はすべて、最も特異性の高い引数の特異性を継承するため、:is(#main, div)は#mainと同じ特異性を持っています。
querySelectorAllやスクレイピングに関しては、特異性は全く適用されません。 セレクタは要素に一致するか、一致しないかのいずれかであり、競合する要素は存在しません。
抽出のためのパターン
スクレイパーにおいて実際に処理を行うセレクタ。
グリッド内の商品カード:
div.product-grid > article.product-card
PDFへのリンク:
a[href$=".pdf"]
外部リンク:
a[href^="http"]:not([href*="example.com"])
クラスではなく安定したテストフック:
[data-testid="price"]
何かが入っているコンテナ:
tr:has(td.error)
article:has(img)
form:has(input:invalid)
記事の最初の段落(その前に何があるかに関係なく):
article p:first-of-type
ヘッダー行をスキップ:
tbody tr:not(:first-child)
2行目から11行目までを取得:
tbody tr:nth-child(n+2):nth-child(-n+11)
プログラムで生成された、特定のクラストークンを持つ要素:
[class~="card"]
除外したい空のセル:
td:not(:empty)
壊れないセレクタの書き方
リファレンスには、利用可能なものが記載されています。選択肢の中から適切なものを選ぶことこそが耐久性の源であり、リデザイン後も生き残るセレクタと、来週には機能しなくなるセレクタとの違いは、そのセレクタが何に依存しているかという点に尽きます。
アンカーを、どれほど意図的に選ばれたかという基準でランク付けしましょう。 data-testid が存在するのは、開発者が自動化のためにそこに配置したからであり、したがってページ上で最も安定した要素です。次に id があります。通常は意図的に設定されますが、時折自動生成されることもあります。セマンティック要素や ARIA ロールは、意味を帯びているため安定しています。 手動で記述されたクラス名は、ある程度安定しています。css-1x9dj2kのような生成されたクラス名は、ビルドのたびに変化するため、全く価値がありません。位置指定セレクタは、ある瞬間のページの正確な形状をコード化しているため、すべての中で最も安定性が低いです。
意味が要求する範囲で、それ以上には特定しないこと。 body > div > div > div.content > p は正確だが脆弱だ。そのチェーンのどこかにラッパーが追加されると、選択できなくなる。一方、.content p は実際の要件を表現しており、構造の再編成にも耐える。安全のために特異性を高めようとする本能は、かえって安全性を損なう結果をもたらす。
失敗が明確にわかるセレクタを優先する。 要素を一つもマッチしないセレクタは、明らかな問題を引き起こします。逆に、要素を多くマッチしすぎた場合、セレクタは静かに最初の要素を返すだけであり、これは一見妥当に見えても実際には間違っている可能性があります。選択肢があるなら、徐々にずれていくバージョンよりも、すぐにエラーになるバージョンを選びましょう。つまり、関係が本当に直接的な場合は、div > p を div p よりも優先すべきです。
セレクタは一箇所にまとめてください。 コードベース全体に散らばった40個のセレクタ文字列は、40個の独立したリスク要因となります。説明的な名前を付けて単一のモジュールにまとめれば、それらは依存関係のマップとなり、サイトの変更後の更新作業は、1日かかる「考古学的な作業」ではなく、1時間の作業で済みます。
抽出するデータの形状を明確に定義しましょう。 これは、気づかないうちに生じる変化を捕捉する習慣です。価格が特定の通貨パターンに一致すべき場合は、それを確認しましょう。リストの項目数が20件を下回ったことがない場合は、20件未満をエラーとして扱います。これを怠ると、誤った要素に一致し始めたセレクタが、形式的には正しいが内容的に誤ったデータを際限なく生成し続け、どの時点でも例外が発生することはありません。
解析したHTMLを保存しておく。 セレクタが機能しなくなった場合、古いマークアップと新しいマークアップを比較すれば、何が変更されたかを数秒で正確に把握できます。調査のために再取得を行うと、処理が遅くなり、帯域幅を消費する上、実際にエラーが発生したページとは異なるページが表示されてしまう可能性があります。
CSSではまだできないこと
簡単なリストですが、各項目はXPathを利用する正当な理由となります。
テキストコンテンツによるマッチング。 標準的な :contains() は存在しません。かつて提案されましたが取り下げられ、どのブラウザも実装していません。これが最大のギャップであり、「ラベルから値を検索する」というパターンがXPathの領域となっている理由です。つまり、//dt[normalize-space()='Price']/following-sibling::dd[1] に対応するCSSの機能はありません。
任意の祖先要素への移動。 :has() は条件付き親要素の選択(「このspanを含むdiv」)を可能にしますが、一般的な祖先軸は存在しません。特定のコンテナまで数レベル遡るには、XPathが必要です。
文字列関数。 トリミングも、部分文字列の抽出も、大文字小文字の変換も、連結もありません。マッチしたものは何でも、その後ホスト言語で処理する必要があります。
名前空間を含むXMLのクエリ。 CSSには、実質的な名前空間のサポートがありません。サイトマップ、RSS、またはSOAPについては、XPathを使用してください。
直前の兄弟要素を直接選択する。 :has() を使用すれば同様の効果が得られます(例:h2:has(+ p) で見出しを選択)。しかし、+ に対応する - コンバイナーは存在しません。
実用上の観点からは、これが完全なリストとなります。それ以外でXPathに頼っていた処理は、現在すべてCSSで実現可能であり、通常はより読みやすいコードで記述できます。
確認すべき環境の違い
セレクタに関して最もよくある予期せぬ問題は、実行している場所とは異なる場所で機能が実装されていることです。
ブラウザは、:has()、:is()、:where()、および多引数形式の:not()を含め、ここに挙げたすべての機能をサポートしています。
サーバーサイドのパーサーは、その仕様が大きく異なります。 lxml や Scrapy で使用されている Python の cssselect は、CSS を XPath に変換します。これは、従来のセレクタではうまく機能しますが、新しいセレクタではそれほどうまく機能しません。特に :has() のサポートは一様ではなく、ブラウザのコンソールで動作するセレクタでも、パーサーではエラーを発生させたり、何も返さずに静かに処理を終了したりすることがあります。
BeautifulSoupは、select()メソッドにsoupsieveを使用しています。このメソッドの対応範囲は良好ですが完全には一致しておらず、XPathのサポートは一切ありません。
PlaywrightとSeleniumはブラウザのエンジンを利用するため、サポート範囲はブラウザに準じます。さらにPlaywrightは、CSSでは表現できない部分の多くをカバーするテキストロケーターやロールロケーターも提供しています。
実用的な習慣として、セレクタは実際に実行される環境でテストするようにしましょう。ブラウザのコンソールは便利ですが、JavaScript処理後のDOMに対して異なる実装をテストすることになるため、この2つの点で誤解を招く可能性があります。
よくある質問
「.card.featured」と「.card .featured」の違いは何ですか?
前者はスペースがなく、両方のクラスを持ち合わせる要素に一致します。後者はスペースがあり、クラス「card」を持つ要素の中に、クラス「featured」を持つ要素が存在する場合に一致します。たった1つのスペースで意味が完全に変わってしまうため、これはCSSセレクタで最もよくある間違いです。
CSSで親要素を選択するにはどうすればよいですか?
:has()を使用します。例えば、div:has(> span.price)と記述すると、spanではなくdivが選択されます。この機能は2023年12月以降、主要なブラウザで広く利用可能になっています。一般的な祖先セレクタは存在しないため、数レベル上の特定のコンテナにアクセスするには、依然としてXPathが必要です。
CSSセレクタでテキストをマッチさせることができますか?
いいえ。標準的な:contains()疑似クラスは存在しません。これは提案されましたが、採用されることはありませんでした。テキストコンテンツによるマッチングには、XPath、またはPlaywrightのgetByText()のようなライブラリ固有のロケーターが必要です。
:nth-child と :nth-of-type の違いは何ですか?`
:nth-child`` は親要素のすべての子要素をカウントします。一方、``:nth-of-type`` は同じタイプの要素のみをカウントします。したがって、見出しが先にある場合、p:first-child は何も一致しませんが、p:first-of-type` は見出しの有無にかかわらず最初の段落に一致します。
:is() はどのような働きをし、:where() とどう違うのですか?
どちらもセレクタのリストを受け取り、そのいずれかに一致する要素を選択することで、繰り返しの多いセレクタを簡潔にします。唯一の違いは特異性です。:is()は最も特異性の高い引数の特異性を採用するのに対し、:where()は常に特異性をゼロとします。このため、:where()は、上書きしやすいデフォルト設定に最適です。
CSSの特異性はどのように機能しますか?
要素ごとに比較すると、IDはクラス、属性、疑似クラスよりも優先度が高く、それらは要素および疑似要素セレクタよりも優先度が高くなります。インラインスタイルはセレクタよりも優先度が高く、!importantはそれらよりも優先度が高くなります。ただし、スタイル設定ではなくクエリを行うためにセレクタを使用する場合は、これらの優先順位は適用されません。
スクレイピングにおいて、CSSセレクタはXPathよりも優れているか?
クラス、ID、属性、および構造的な選択に関しては、はい — より短く、より明確です。テキストコンテンツのマッチングや、任意の祖先要素への移動については、XPathが必要です。ほとんどのスクレイピングライブラリは両方をサポートしており、状況に応じてそれぞれを使用するのが一般的です。
なぜセレクタはブラウザでは動作するのに、スクレイパーでは動作しないのですか?
主な原因は2つあります。ブラウザのDOMはJavaScript処理後の状態であるのに対し、パーサーは生のHTMLを処理するため、取得したデータにその要素が存在しない可能性があります。また、サーバーサイドのCSS実装では、すべての最新セレクタがサポートされているわけではありません。特に:has()は、ブラウザ以外ではサポート状況にばらつきがあります。
まとめ
CSSセレクタの中核となる部分はごくわずかです。識別子としての「type」「class」「id」、関係性を表す4つのコンバイナ、意図的に選択するための属性セレクタ、そして位置や状態を表す少数の疑似クラスです。
追加の構文を知るよりも、次の3つの習慣を身につけるほうが、実際のセレクタをより効果的に改善できます。 関係が真に直接的な場合は、単にスペースを空けるよりも > を使用するようにしましょう。マッチしなくなるセレクタは、マッチしすぎてしまうセレクタよりも有用だからです。クラス名よりも data-* 属性を使用するようにしましょう。クラスはビルドツールによって再生成されますが、属性は人が選択するものです。また、「2番目の子要素」ではなく「2番目の段落」を意味する場合は、:nth-child ではなく :nth-of-type を使用するようにしましょう。
:has() これは、意図的に採用する価値のある追加機能です。プレーンなCSSにおける親要素の選択や兄弟要素の条件指定は、以前ユーザーがXPathを使わざるを得なかった状況の大部分を解消しており、ブラウザ上で信頼して利用できるほど十分に長い間広く普及しています。ブラウザ以外では、まずパーサーを確認してください。残りの予期せぬ挙動は、そこに潜んでいるからです。
