SeleniumやスクレイピングでWebページを自動操作するとき、最初の関門になるのが「狙った要素をどう指定するか」です、ボタンを押す・文字を入力する・値を読み取る、どれも先にその要素を特定することから始まります
そのときの「住所」にあたるのがCSSセレクタです、ページの中の「ここの要素」を文字列で言い当てるための書き方で、これが書けると自動化はぐっとラクになります
CSSセレクタというとWebデザイン(見た目をつける)の文脈で出てくることが多いんですが、この記事では自動化で要素を狙う視点に絞ってまとめます、同じセレクタでも、デザインで便利な書き方と自動化で効く書き方は少しズレるので、そのあたりも整理していきます
使用例はPython版のSeleniumで書きますが、セレクタの構文そのものは言語やツールに依存しません、VBA版のSeleniumでも、BeautifulSoupのようなスクレイピングでも、UiPathやPower Automate DesktopといったRPAツールでも、Web要素を指す手段としてCSSセレクタが使えます、Pythonを使っていない方も、セレクタの部分はそのまま読み替えられます
実際にブラウザを動かす操作コード(クリックや入力、待機、ループなど)は言語ごとの記事にまとめているので、この記事は「セレクタの書き方そのもの」に集中します、たとえばSeleniumVBAでの実装なら「SeleniumVBAの使い方 実践コード集」にあります
デザイン用のCSSと、自動化のセレクタは何が違う?
セレクタの具体的な書き方に入る前に、まずここを押さえておくと迷いません、同じCSSセレクタでも、見た目をつける(デザイン)目的と、要素を特定する(自動化)目的では、効くものと効かないものが違うんです
大きく分けると、違いは次の4つです
違い1 状態・装飾むけの擬似クラス・擬似要素は要素特定に効かない
デザインでよく使う ::before や ::after(擬似要素)は、自動化では使えません、これは見た目の上では存在しても、DOM(ページの構造)には実体のノードが無いからです
大事なのは、これがエラーになるのではなく常に「空」が返るという点です、MDNにも、擬似要素を含むセレクタは返り値が常に空になると明記されています、つまり「見つからない」のではなく「最初から取れない」ので、原因に気づきにくいハマりどころです
同じく :hover / :focus / :active のような操作状態の擬似クラスも、自動化での要素特定にはほぼ使えません、構文としては有効ですが、「その瞬間その状態の要素」しか返さないので、静的に要素を狙う用途には向かないんです、フォーカス中の要素が必要なら、セレクタではなく activeElement を使うのが定石になります
違い2 判定基準は「見た目に効くか」ではなく「querySelectorAllが解釈できるか」
ここが一番のキモです、自動化で使えるセレクタかどうかの判定基準は、そのセレクタをブラウザの querySelectorAll が解釈できるかどうかです
というのも、SeleniumでCSSセレクタ指定(By.CssSelector)を使うと、内部ではブラウザの querySelectorAll が呼ばれます、これはW3CのWebDriver仕様で定義されている動きです、要素1つを取る指定は先頭の1件、複数取る指定は当てはまる全件が返ります
つまり、querySelectorが解釈できるセレクタ=Seleniumで使えるセレクタと考えてOKです、ブラウザのDevToolsのコンソールで document.querySelectorAll(“セレクタ”) を試して、ちゃんと要素が返ればSeleniumでも同じく使えます、これは自分でセレクタを検証するときにも役立つ考え方です
違い3 「中の文字」では選べない
自動化だと「『送信』と書かれたボタンを押したい」のように、表示テキストで要素を狙いたい場面がよくあります、ところがCSSセレクタには、これがありません
:contains() という書き方を見かけることがありますが、これはjQuery独自の非標準セレクタです、ブラウザの querySelectorAll に渡すとエラー(SyntaxError)になるので、Seleniumでも使えません
テキストの中身で要素を選びたいときは、CSSではなくXPathの出番です、XPathなら text() や contains() が使えて、たとえば //button[contains(., ‘送信’)] のように「『送信』を含むボタン」を狙えます、このあたりの実装は実践コード集の方でも触れているので、後ほどリンクします
違い4 「壊れにくさ」という評価軸が増える
デザイン用のCSSは、自分のサイトのHTMLに対して書くので、構造が変われば自分でセレクタも直せます、ところが自動化は他人のサイト(自分で変えられないHTML)を相手にすることが多いです
すると、そのセレクタが「ページの作りが少し変わっても壊れにくいか」という、装飾CSSには無かった評価軸が出てきます、同じ要素を狙うのにも、壊れにくい書き方と壊れやすい書き方があって、自動化では前者を選ぶ意識が要ります、この観点は後半のTipsでも具体的に見ていきます
まとめると、自動化のセレクタは「querySelectorAllが解釈できる範囲」で、「状態・装飾むけのものは除く」、「テキストで選ぶならXPath」、そして「壊れにくさも意識する」、この4点を頭に置いておけば、あとは個別の書き方を覚えるだけです
基本のセレクタ(id・class・タグ)
まずは土台になる基本のセレクタからです、ここは自動化でもデザインでも共通で、一番よく使うところです
| 探したいもの | セレクタ | 例 |
|---|---|---|
| id で探す | #id名 | #search |
| class で探す | .class名 | .btn |
| タグ名で探す | タグ名 | input |
| 複数クラスのAND(両方持つ) | .classA.classB | .btn.primary |
| グループ化のOR(どちらか) | セレクタA, セレクタB | .btn, .link |
ポイントになるのが、下2つの組み合わせです、.btn.primary のようにクラスをくっつけて書くと「両方のクラスを持つ要素」を狙えます(AND)、間にスペースを入れないのがコツで、スペースを入れると意味が変わるので注意です
逆にカンマで区切ると「どちらかに当てはまる要素」をまとめて取れます(OR)、複数種類のボタンをいっぺんに集めたいときなどに便利です
Python版のSeleniumだと、こんな感じで指定します、find_element の第2引数に、上のセレクタ文字列をそのまま渡すだけです
from selenium.webdriver.common.by import By
# id が search の要素を探す
driver.find_element(By.CSS_SELECTOR, "#search")
# class が btn かつ primary の要素(両方のクラスを持つもの)
driver.find_element(By.CSS_SELECTOR, ".btn.primary")
# input タグを全部取る(find_elements は複数形・リストで返る)
driver.find_elements(By.CSS_SELECTOR, "input")セレクタの文字列が同じなら、VBA版のSeleniumでも考え方は同じです、VBAなら driver.FindElement(By.CssSelector, “#search”) のようにメソッド名や書き方は変わりますが、渡すセレクタ文字列(#search)はまったく同じものを使えます
属性セレクタ(動的id・class対策の主役)
ここからが、自動化でとくに頼りになるセレクタです、属性セレクタは、要素が持つ属性(name・type・href・id・classなど)を手がかりに要素を狙う書き方です
| 狙い方 | セレクタ | 意味・例 |
|---|---|---|
| 属性が存在する | [attr] | [disabled] … disabled属性を持つ |
| 完全一致 | [attr=’値’] | [name=’q’] … nameがちょうど q |
| 前方一致(で始まる) | [attr^=’値’] | [id^=’btn-‘] … idが btn- で始まる |
| 後方一致(で終わる) | [attr$=’値’] | [href$=’.pdf’] … hrefが .pdf で終わる |
| 部分一致(を含む) | [attr*=’値’] | [class*=’primary’] … classに primary を含む |
| 単語一致(空白区切り) | [attr~=’値’] | 空白区切りの単語のどれかに一致 |
| ハイフン始まり一致 | [attr|=’値’] | 値 か 値- で始まる(lang指定などで使う) |
このうち、自動化でとくに重宝するのが前方一致(^=)・後方一致($=)・部分一致(*=)の3つです、なぜかというと、最近のサイトは id=”btn-8f3a2″ のように、毎回ランダムに変わるid・classを持っていることが多いからです
こういう「変わる部分」を完全一致で書くと、ページを開き直すたびに壊れます、そこで、変わらない部分だけを前方一致や部分一致で拾うと安定します、たとえば btn- の部分が固定なら、後ろのランダム文字は無視して [id^=’btn-‘] で狙う、という具合です
# name 属性が q の要素(完全一致)
driver.find_element(By.CSS_SELECTOR, "[name='q']")
# id が btn- で始まる要素(後ろがランダムでもOK・前方一致)
driver.find_element(By.CSS_SELECTOR, "[id^='btn-']")
# class に primary を含む要素(部分一致)
driver.find_element(By.CSS_SELECTOR, "[class*='primary']")
# href が .pdf で終わるリンクを全部取る(後方一致)
driver.find_elements(By.CSS_SELECTOR, "[href$='.pdf']")ちなみに、値の大文字・小文字を無視したいときは、閉じカッコの直前に i を付けます、[type=’text’ i] のように書くと、TEXT でも Text でもマッチします、地味ですが、サイトによって表記がそろっていないときに効きます
結合子(子孫・直下・隣接・一般兄弟)
セレクタ同士をつないで「位置関係」で絞り込むのが結合子です、似た要素がたくさんあるページで、狙った1つに寄せるときに効いてきます
| 結合子 | 書き方 | 意味 |
|---|---|---|
| 子孫(スペース) | .親 .子 | 親の中にある(何段下でもよい) |
| 直下の子(>) | .親 > .子 | 親のすぐ下の子だけ |
| 隣接兄弟(+) | .A + .B | Aの直後にある兄弟B(1つだけ) |
| 一般兄弟(~) | .A ~ .B | Aより後ろにある兄弟Bすべて |
このうち、子孫(スペース)と直下の子(>)は基本でよく使うので、ここでは軽めにいきます、ざっくり言うと、スペースは「ずっと下まで含めて探す」、> は「すぐ下の階層だけに限定する」です
むしろ覚えておきたいのが、後半の2つ、隣接兄弟(+)と一般兄弟(~)です、これは「ある要素の横(同じ親の中の並び)」を狙う書き方で、フォームのラベルと入力欄のような「隣り合った要素」を取るときに便利です
+ は「直後の兄弟1つだけ」、~ は「後ろに続く兄弟すべて」を指します、たとえば「label のすぐ後ろの input」を狙うなら label + input、「ある見出しより後ろにある段落を全部」なら h2 ~ p という具合です
# .list の中にある .item をすべて(子孫・何段下でもよい)
driver.find_elements(By.CSS_SELECTOR, ".list .item")
# .list のすぐ下の .item だけ(直下の子)
driver.find_elements(By.CSS_SELECTOR, ".list > .item")
# label の直後にある input(隣接兄弟・1つだけ)
driver.find_element(By.CSS_SELECTOR, "label + input")
# .title より後ろに並ぶ .row をすべて(一般兄弟)
driver.find_elements(By.CSS_SELECTOR, ".title ~ .row")兄弟系の結合子はあくまで「後ろ方向」しか狙えません、「直前の兄弟」や「親」を遡る指定はCSSセレクタにはありません(親方向は後半で触れます)、横の並びは前から後ろへ、と覚えておくと混乱しにくいです
位置・除外で狙う(:nth-child・:not とFindElements+indexの使い分け)
「上から何番目」や「これは除く」という狙い方もあります、構造系の擬似クラスと呼ばれるもので、自動化でも普通に使えます、ただ、ここはSeleniumならではの選択肢もあるので、その使い分けまで含めて見ていきます
除外できる :not() はCSSの強み
まず、自動化でも素直に役立つのが :not() です、これは「カッコ内のセレクタに当てはまらないもの」を狙う書き方で、「○○以外」をセレクタ1本で表現できます
たとえば input:not([disabled]) と書くと、「無効化されていない入力欄だけ」を取れます、押せないボタンや入力できない欄を最初から除けるので、自動化のエラー回避にそのまま効きます、これはVBA側でループして弾く手間が省けるので、CSSセレクタならではの便利さです
# 無効化(disabled)されていない input だけを取る
driver.find_elements(By.CSS_SELECTOR, "input:not([disabled])"):nth-child は「使えるけど、集めてループでも同じ」
「上から○番目の要素」を狙うのが :nth-child(n) です、ほかにも :first-child(最初)、:last-child(最後)、:nth-of-type(n)(同じタグの中で○番目)があります
これらは確かに使えるんですが、自動化ではFindElements で全部集めてから、index(番号)やループで取り出しても同じことができます、どちらが正解というより、両方できると知っておくのが良いです
# 方法A セレクタで「2番目の li」を直接狙う
driver.find_element(By.CSS_SELECTOR, "li:nth-child(2)")
# 方法B 全部集めてから index で2番目を取る(リストは0始まりなので [1] が2番目)
els = driver.find_elements(By.CSS_SELECTOR, "li")
print(els[1].text) # [0]が1番目、[1]が2番目むしろ :nth-child には、知らないとハマるクセがあります、これは「すべての兄弟をひっくるめて数えた○番目」を見るので、たとえば li:nth-child(2) は「2番目の子が li ならそれ」という意味です、もし li の前に別のタグが混じっていると、番号がズレて狙いが外れます
「同じ種類のタグの中での○番目」が欲しいなら、:nth-of-type の方が直感に近いです、ただ、このあたりの数え方の混乱もあって、自動化ではFindElements で集めてindexやループで取り出す方が、結果が読めて安全な場面が多いと感じます、番号がズレる心配がなく、コードを見ても何番目を取っているかが分かりやすいからです
ついでに、CSSで「親に近いこと」をやりたいなら :has() という擬似クラスもあります、div:has(> img) で「imgを直下に持つdiv」のように、中身を条件にして親側を狙えます、ただし:has() は比較的新しい機能(Chrome・Edgeは105、Safariは15.4、Firefoxは121以降)なので、古いブラウザを自動操作する場合は注意してください
使い分けの目安はこんな感じです、「○○以外」を除くのは :not() が手早い、「上から○番目」は :nth-child でも書けるが、FindElements で集めてindex・ループの方が読めて安全なことが多い、と覚えておけば十分です、どちらにしても、位置で狙う指定はページの構造が変わると壊れやすいので、可能ならidやclass・属性で狙える方を先に探すのがおすすめです、実際のループ処理の書き方は「SeleniumVBAの使い方 実践コード集」の複数要素ループの章にまとめています
自動化での実践Tips
最後に、セレクタを実際に書くときに効いてくるコツを、いくつかまとめておきます、ここを意識すると、壊れにくくて読みやすいセレクタが書けるようになります
一意に絞るコツ … 属性やclassを組み合わせる
狙った1つに絞れないときは、条件を足して組み合わせるのが基本です、タグ+属性、class+属性、親の中の子…と重ねていくと、似た要素の中から目的のものを言い当てられます
# タグ + 属性 + 親で絞り込む(検索フォームの中の送信ボタンなど)
driver.find_element(By.CSS_SELECTOR, "form.search button[type='submit']")動的id・classは部分一致で逃がす
さっきの属性セレクタの章ともつながりますが、毎回変わるid・classを相手にするときは、完全一致を避けて前方一致・部分一致で「変わらない部分」だけを拾います、これは動的なページを自動操作するときの定番テクニックです
DevToolsの「Copy selector」は鵜呑みにしない
ブラウザのDevToolsには、要素を右クリックして「Copy selector」でセレクタをコピーできる機能があります、便利なんですが、出てくるのは body からの長い nth-child パスになりがちで、これがけっこう脆いんです
こういう「上から数えていく」パスは、ページの作りがちょっと変わるだけで簡単に壊れます、なので、コピーしたものをそのまま使うより、ユニークなidやclassを見つけて手で書き直す方が、ずっと壊れにくいセレクタになります、Copy selectorは「とっかかり」くらいに考えておくのがちょうどいいです
テキスト・親方向で狙うときはXPath
ここまで見てきたとおり、CSSセレクタには2つだけ苦手があります、表示テキストで選ぶことと、子から親へ遡ることです、この2つはXPathの担当だと割り切ると、迷わなくなります
| やりたいこと | 得意なほう |
|---|---|
| id・class・属性で特定 | CSS(簡潔) |
| 構造(子孫・兄弟)で特定 | CSS(簡潔) |
| 表示テキストで選ぶ | XPath(text() / contains()) |
| 子から親へ遡る | XPath(.. を使う) |
テキストで探す書き方(全要素を集めてループで文字一致を見る方法)や、親へ遡る “..” の使い方は、「SeleniumVBAの使い方 実践コード集」でVBA版の実際のコード付きで紹介しています、CSSで狙えるところはCSSで、苦手なところだけXPathで、と分担させるのが組みやすいです
まとめ
自動化で要素を狙うCSSセレクタの書き方を、ひととおり整理してみました、最後にポイントをまとめておきます
- 自動化で使えるセレクタかどうかは「querySelectorAllが解釈できるか」が判定基準、::before などの擬似要素は常に空が返る、:contains() は非標準で使えない
- 基本は #id / .class / タグ、複数クラスのANDは .a.b、ORは カンマ区切りで書ける
- 毎回変わるid・class対策には、属性の前方一致(^=)・部分一致(*=)が主役、結合子は子孫(スペース)・直下(>)・隣接兄弟(+)・一般兄弟(~)
- 「○○以外」は :not()、位置で狙う :nth-child は使えるが番号のズレに注意、FindElements で集めてindex・ループの方が安全な場面も多い
- DevToolsの Copy selector は脆いので手で書き直す、テキスト・親方向はXPathに任せる
セレクタの書き方そのものは、ここまでの内容でだいたいカバーできます、あとは実際にブラウザを動かすコード(クリック・入力・待機・ループなど)と組み合わせるだけです、SeleniumVBAでの実装は「SeleniumVBAの使い方 実践コード集」に、導入から知りたい方は「Excelでもブラウザ操作!SeleniumVBAの使い方」にまとめてあります
Python版で「結局どのByを使えばいいの?」というところは「Seleniumのfind_elementの使い方」が、要素が出てくるまで待つ書き方は「Selenium WebDriverWaitの使い方」が参考になります、Chromeの起動オプションまわりは「SeleniumのOptionsとServiceまとめ」も合わせてどうぞ
セレクタが書けるようになると、自動化でできることが一気に広がります、まずは身近なページで、DevToolsの document.querySelectorAll() を使ってセレクタを試してみるところから始めてみてください



コメント