WebスクレイピングやE2Eテスト、Seleniumを使ったブラウザ操作をしていると、避けて通れないのが XPath です。
しかし、いざXPathを書こうとすると、
-
//と/の違いが分からない - classを指定したら要素が取得できない
- 部分一致はどう書く?
- 親要素・兄弟要素を取得したい
- 「この文字を含むボタン」を指定したい
- 動的なWebページでも壊れにくいXPathを書きたい
といった問題に遭遇しがちです。
そこで本記事では、XPathの基本構文から、Selenium・Playwright・スクレイピングなどでそのまま使える実用的なXPathまで、サンプル付きでまとめます。
「XPathを毎回検索している」という方のチートシートとしても使える内容です。
XPathとは
XPath(XML Path Language)は、XMLやHTMLのツリー構造から特定の要素を指定するための言語です。
例えば、以下のHTMLがあるとします。
<div id="content">
<h1>商品一覧</h1>
<div class="product">
<span class="name">MacBook Pro</span>
<span class="price">298,800円</span>
</div>
<div class="product">
<span class="name">iPhone</span>
<span class="price">124,800円</span>
</div>
</div>
MacBook Pro の要素を取得したい場合、例えば次のXPathを書けます。
//span[text()='MacBook Pro']
classを使うなら、
//span[@class='name']
のように指定できます。
1. XPathの基本構文
まずは最低限覚えておきたい構文です。
要素名で取得する
//div
ページ内に存在するすべての div 要素を取得します。
//input
なら、すべての input 要素です。
/ と // の違い
XPathでは、この違いが非常に重要です。
/
直下の子要素を指定します。
/html/body/div
これは、
html
└── body
└── div
という直接の親子関係を表します。
//
子孫要素を検索します。
//div
HTML内のどこに存在していても div を検索します。
例えば、
//div/span
は、
「任意の場所にある div の直下に存在する span」
という意味です。
一方、
//div//span
なら、
「任意の場所にある div の子孫に存在する span」
となります。
2. 属性を指定する
XPathで最もよく使うパターンです。
idを指定する
HTML:
<input id="username">
XPath:
//input[@id='username']
要素名を省略して、
//*[@id='username']
とも書けます。
classを指定する
HTML:
<button class="submit">送信</button>
XPath:
//button[@class='submit']
name属性を指定する
HTML:
<input name="email">
XPath:
//input[@name='email']
type属性を指定する
//input[@type='text']
チェックボックスなら、
//input[@type='checkbox']
ボタンなら、
//button[@type='submit']
任意の属性を指定する
<a href="/users/123">ユーザー</a>
//a[@href='/users/123']
data-* 属性も指定できます。
<div data-testid="login-button">
//*[@data-testid='login-button']
E2Eテストでは data-testid のような専用属性を使うと、比較的壊れにくいセレクタになります。
3. 複数条件を指定する
AND条件
//input[@type='text' and @name='username']
例えば、
<input type="text" name="username">
にマッチします。
OR条件
//input[@name='username' or @name='email']
username または email のinputを取得します。
4. テキストで要素を指定する
完全一致
HTML:
<button>ログイン</button>
XPath:
//button[text()='ログイン']
非常によく使います。
. を使ったテキスト指定
次のHTMLを考えます。
<button>
<span>ログイン</span>
</button>
この場合、
//button[text()='ログイン']
では期待通りマッチしないことがあります。
その場合は、
//button[.='ログイン']
や、
//button[contains(., 'ログイン')]
を使うと便利です。
. はその要素配下を含めた文字列値を表します。
5. contains() で部分一致
実務では非常に頻繁に使います。
テキストの部分一致
HTML:
<button>ユーザー情報を保存する</button>
XPath:
//button[contains(text(), '保存')]
または、
//button[contains(., '保存')]
classの部分一致
HTML:
<div class="product-card active">
XPath:
//div[contains(@class, 'product-card')]
hrefの部分一致
<a href="/users/123/edit">
//a[contains(@href, '/users/')]
idの部分一致
//*[contains(@id, 'user')]
動的に生成されるIDを検索したい場合などに便利です。
6. starts-with() で前方一致
HTML:
<div id="user_123">
<div id="user_456">
XPath:
//div[starts-with(@id, 'user_')]
user_ から始まるidを持つ要素を取得できます。
7. class指定で注意すべきポイント
以下のHTMLがあります。
<div class="card active">
この要素に対して、
//div[@class='card']
はマッチしません。
class 属性の実際の値は、
card active
だからです。
簡単な方法として、
//div[contains(@class, 'card')]
と書けます。
ただし、これには注意点があります。
例えば、
<div class="product-card">
にも、
//*[contains(@class, 'card')]
がマッチします。
より厳密にclass名を判定したい場合は、次の書き方が使えます。
//*[contains(concat(' ', normalize-space(@class), ' '), ' card ')]
少し長いですが、「class属性内の card というクラス」に正確にマッチできます。
8. normalize-space() で空白を無視する
HTMLでは、改行や余分な空白が含まれることがあります。
<button>
ログイン
</button>
この場合、
//button[text()='ログイン']
では取得できない可能性があります。
そこで、
//button[normalize-space()='ログイン']
を使います。
部分一致なら、
//button[contains(normalize-space(.), 'ログイン')]
です。
UIテストではかなり便利です。
9. n番目の要素を取得する
HTML:
<ul>
<li>Apple</li>
<li>Banana</li>
<li>Orange</li>
</ul>
1番目
//li[1]
2番目
//li[2]
3番目
//li[3]
XPathのインデックスは 1始まり です。
JavaScriptなどの 0 始まりとは異なるので注意してください。
最後の要素
//li[last()]
最後から1つ前
//li[last()-1]
10. XPathの [1] でハマりやすいポイント
次の2つは意味が違います。
//div/span[1]
(//div/span)[1]
前者は、
各divについて、最初のspan
を取得します。
後者は、
XPath全体の検索結果の中から最初の1件
を取得します。
「ページ内で最初の1件だけ取得したい」場合は、
(//div/span)[1]
と括弧を付けます。
11. 親要素を取得する
.. を使う
HTML:
<div class="product">
<span class="name">MacBook Pro</span>
</div>
span から親の div を取得するなら、
//span[@class='name']/..
です。
parent軸
//span[@class='name']/parent::div
より明示的に書けます。
12. 祖先要素を取得する
HTML:
<div class="product">
<div>
<span>MacBook Pro</span>
</div>
</div>
span から祖先の .product を探す場合、
//span[text()='MacBook Pro']/ancestor::div[@class='product']
と書けます。
実務ではかなり便利です。
例えば、
「商品名がMacBook Proの商品カード内にある購入ボタン」
を取得したい場合、
//span[text()='MacBook Pro']
/ancestor::div[contains(@class, 'product')]
//button
のように書けます。
1行にすると、
//span[text()='MacBook Pro']/ancestor::div[contains(@class, 'product')]//button
です。
13. 子要素を取得する
//div[@id='content']/child::span
通常は、
//div[@id='content']/span
で十分です。
14. 子孫要素を取得する
//div[@id='content']//span
content 配下に存在するすべての span を取得します。
15. 兄弟要素を取得する
XPathの強みの一つです。
HTML:
<div class="form-group">
<label>メールアドレス</label>
<input type="email">
</div>
「メールアドレス」というlabelを基準にinputを取得したい場合があります。
後ろの兄弟要素
//label[text()='メールアドレス']/following-sibling::input
前の兄弟要素
//input[@type='email']/preceding-sibling::label
16. following を使う
following-sibling は同じ親を持つ兄弟要素だけが対象です。
一方、
following::
は、文書上で後ろに存在する要素を幅広く検索します。
例えば、
//h2[text()='ユーザー情報']/following::button[1]
とすると、「ユーザー情報」というh2より後ろにある最初のbuttonを取得できます。
ただし範囲が広いため、意図しない要素を取得しやすい点には注意が必要です。
17. preceding を使う
文書上で前に存在する要素を取得します。
//button[text()='保存']/preceding::input[1]
保存ボタンより前に存在するinputを探す、といった指定ができます。
18. 「特定の子要素を持つ親」を取得する
実務で非常に重要なパターンです。
HTML:
<div class="product">
<span class="name">MacBook Pro</span>
<button>購入</button>
</div>
<div class="product">
<span class="name">iPhone</span>
<button>購入</button>
</div>
MacBook Proの商品ブロックを取得するなら、
//div[@class='product'][.//span[text()='MacBook Pro']]
と書けます。
さらに購入ボタンを取得するなら、
//div[@class='product'][.//span[text()='MacBook Pro']]//button
これはスクレイピングやE2Eテストで非常に使いやすい書き方です。
19. 属性が存在する要素を取得する
例えば disabled 属性を持つbuttonなら、
//button[@disabled]
href を持つ要素なら、
//*[@href]
です。
20. 属性が存在しない要素を取得する
disabled ではないbuttonを取得したい場合、
//button[not(@disabled)]
と書けます。
21. 特定の文字列を含まない要素
//div[not(contains(@class, 'disabled'))]
例えば、disabled系のclassを持たないdivを取得できます。
テキストでも使えます。
//button[not(contains(., '削除'))]
22. 複数のclass条件
HTML:
<button class="btn primary large">
簡単に書くなら、
//button[
contains(@class, 'btn')
and contains(@class, 'primary')
]
です。
1行なら、
//button[contains(@class, 'btn') and contains(@class, 'primary')]
23. 複数候補から取得する
XPathには | を使ったUnionがあります。
例えば、
//button | //input[@type='submit']
とすると、
- button
- submitタイプのinput
の両方を取得できます。
24. inputのvalueで取得する
HTML:
<input type="submit" value="ログイン">
XPath:
//input[@value='ログイン']
input は表示文字列が text() ではなく value 属性に入っているケースがあるので注意してください。
25. placeholderで取得する
<input placeholder="メールアドレス">
//input[@placeholder='メールアドレス']
フォーム操作ではよく使います。
26. aria-labelで取得する
最近のWebアプリでは、アクセシビリティ属性を使えるケースも多いです。
<button aria-label="閉じる">
//button[@aria-label='閉じる']
意味のない動的classよりも、安定したセレクタになる場合があります。
27. labelからinputを取得する
HTML:
<label for="email">メールアドレス</label>
<input id="email" type="email">
この構造なら、単純に、
//input[@id='email']
で取得できます。
ただし、UI上のラベル文字列を基準にしたい場合は、HTML構造によってさまざまな書き方があります。
例えば、
<label>
メールアドレス
<input type="email">
</label>
なら、
//label[contains(., 'メールアドレス')]//input
です。
28. tableで使えるXPath
テーブル操作でもXPathは非常に便利です。
HTML:
<table>
<tr>
<td>田中</td>
<td>tanaka@example.com</td>
<td><button>編集</button></td>
</tr>
<tr>
<td>鈴木</td>
<td>suzuki@example.com</td>
<td><button>編集</button></td>
</tr>
</table>
「田中」の行を取得
//tr[.//td[text()='田中']]
「田中」の行にある編集ボタン
//tr[.//td[text()='田中']]//button[text()='編集']
このパターンは管理画面のE2Eテストで特に便利です。
29. nth列を取得する
2列目を取得するなら、
//tr/td[2]
特定の行の2列目なら、
//tr[.//td[text()='田中']]/td[2]
です。
30. SVG要素を取得する
SVGでは、環境によって通常のタグ指定がうまく動かないケースがあります。
例えば、
<svg>
<path d="...">
</svg>
次のように指定できます。
//*[local-name()='svg']
pathなら、
//*[local-name()='svg']//*[local-name()='path']
です。
31. XPathで使える比較演算子
例えば、
//input[@type='text']
は等価比較です。
代表的な比較演算子として、
=
!=
<
>
<=
>=
があります。
例えば、
//div[@data-count > 10]
のような条件式も記述できます。
ただし、HTML属性は文字列として扱われるケースがあるため、利用環境や値の形式には注意してください。
32. position() を使う
位置を明示的に指定できます。
//li[position()=1]
これは、
//li[1]
と似た意味です。
3番目まで取得
//li[position() <= 3]
2番目以降
//li[position() >= 2]
33. count() を使う
子要素の数で条件を付けることもできます。
例えば、
//ul[count(li) > 3]
なら、直下に3個より多くの li を持つ ul を取得します。
34. descendant を使う
//div[@id='content']/descendant::span
は、
//div[@id='content']//span
と近い意味です。
通常は // の方が簡潔なので、明示性が必要な場合を除けばこちらがよく使われます。
35. descendant-or-self
descendant-or-self::
という軸もあります。
例えば // は概念的には descendant-or-self を利用した記法として理解できます。
実務上は直接書くことは少ないですが、XPathの仕様を理解するときに知っておくと便利です。
36. よく使うXPath Axes一覧
XPathでは、要素同士の関係を「Axis」で表現できます。
| Axis | 意味 |
|---|---|
child:: |
子 |
parent:: |
親 |
ancestor:: |
祖先 |
ancestor-or-self:: |
自身または祖先 |
descendant:: |
子孫 |
descendant-or-self:: |
自身または子孫 |
following-sibling:: |
後ろの兄弟 |
preceding-sibling:: |
前の兄弟 |
following:: |
文書上で後ろ |
preceding:: |
文書上で前 |
self:: |
自分自身 |
attribute:: |
属性 |
特に実務では、
ancestor
following-sibling
preceding-sibling
を覚えておくと非常に役立ちます。
37. SeleniumでXPathを使う
Python + Seleniumでは次のように使えます。
from selenium.webdriver.common.by import By
element = driver.find_element(
By.XPATH,
"//button[text()='ログイン']"
)
複数要素なら、
elements = driver.find_elements(
By.XPATH,
"//div[@class='product']"
)
containsを使う
button = driver.find_element(
By.XPATH,
"//button[contains(., '保存')]"
)
商品名からボタンを取得
button = driver.find_element(
By.XPATH,
"//div[contains(@class, 'product')][.//span[text()='MacBook Pro']]//button"
)
38. JavaScriptからXPathを確認する
ブラウザのDevToolsでは、document.evaluate() を使ってXPathを実行できます。
最初の要素を取得する例です。
const result = document.evaluate(
"//button[text()='ログイン']",
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
);
console.log(result.singleNodeValue);
複数件取得する場合は、
const result = document.evaluate(
"//div",
document,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
for (let i = 0; i < result.snapshotLength; i++) {
console.log(result.snapshotItem(i));
}
とできます。
39. Chrome DevToolsでXPathを試す
Chrome DevToolsのConsoleでは $x() が利用できる環境があります。
例えば、
$x("//button")
や、
$x("//button[contains(., 'ログイン')]")
のように入力すれば、XPathにマッチする要素を確認できます。
ブラウザやDevToolsの環境によって利用可否が異なるため、使えない場合は document.evaluate() を利用してください。
40. 絶対XPathと相対XPath
絶対XPath
/html/body/div[1]/div[2]/main/div[3]/button
HTMLのルートから完全なパスを書く方法です。
一見確実そうに見えますが、実務では基本的におすすめしません。
HTML構造が少し変わっただけで壊れるからです。
相対XPath
//button[@data-testid='login']
または、
//button[normalize-space()='ログイン']
のように、意味のある属性やテキストを基準にします。
E2Eテストやスクレイピングでは、こちらを優先する方が保守しやすくなります。
41. 壊れにくいXPathを書くコツ
XPathは「取得できればOK」ではありません。
長期間運用する自動テストやスクレイパーでは、HTML変更に耐えられるか が重要です。
おすすめの優先順位は、概ね次のように考えられます。
テスト専用属性
↓
安定したid
↓
意味のあるname / aria-labelなど
↓
意味のあるテキスト
↓
安定したclass
↓
HTML構造
↓
位置指定
例えば、
/html/body/div[2]/div[3]/button[1]
より、
//button[@data-testid='save-button']
の方が圧倒的に保守しやすいでしょう。
42. 動的classに依存しすぎない
ReactやCSS Modules、CSS-in-JSなどを使ったサイトでは、class名が次のようになる場合があります。
<button class="Button_root__xA83K">
このclassを直接指定すると、ビルドやUI変更で壊れる可能性があります。
//button[@class='Button_root__xA83K']
よりも、例えば、
//button[@aria-label='保存']
や、
//button[normalize-space()='保存']
あるいはテスト用属性があるなら、
//button[@data-testid='save-button']
を優先した方が安定します。
43. 位置指定に依存しすぎない
例えば、
//div[3]/button[2]
のようなXPathは壊れやすいです。
要素が1つ追加されただけで対象が変わる可能性があります。
代わりに、
//div[.//span[normalize-space()='MacBook Pro']]//button[normalize-space()='購入']
のように、UI上の意味を利用したXPathを書くと保守しやすくなります。
44. XPathが長すぎる場合は設計を見直す
例えば、
//div[@id='app']/div[2]/div/div[3]/div[1]/form/div[4]/div/button
のようなXPathを書いている場合、HTML構造への依存度が高すぎる可能性があります。
可能であれば、
//button[@data-testid='submit']
のような安定した属性をフロントエンド側に追加することも検討しましょう。
テストコードだけで無理に解決するより、アプリケーション側を「テストしやすいHTML」にする方が良いケースもあります。
45. CSS SelectorとXPathはどちらを使うべき?
XPathとCSS Selectorにはそれぞれ得意分野があります。
CSS Selectorが得意
例えば、
#login
.product .name
input[type="email"]
のような単純な指定はCSS Selectorの方が読みやすいケースが多いです。
XPathが得意
XPathは要素の関係やテキストを基準にした検索が得意です。
例えば、
//button[text()='ログイン']
//tr[.//td[text()='田中']]//button
//label[text()='メールアドレス']/following-sibling::input
のような検索です。
特に、
「この文字列を持つ要素の親にあるボタン」
といった指定ではXPathが強力です。
46. よく使うXPathチートシート
最後に、実務で特によく使うXPathをまとめます。
id
//*[@id='login']
class
//*[contains(@class, 'product')]
name
//*[@name='email']
完全一致テキスト
//button[text()='ログイン']
空白を無視した完全一致
//button[normalize-space()='ログイン']
部分一致テキスト
//button[contains(., 'ログイン')]
属性の部分一致
//a[contains(@href, '/users/')]
属性の前方一致
//*[starts-with(@id, 'user_')]
AND
//input[@type='text' and @name='username']
OR
//input[@name='username' or @name='email']
親
//span[text()='商品名']/..
祖先
//span[text()='商品名']/ancestor::div
後ろの兄弟
//label[text()='メール']/following-sibling::input
前の兄弟
//input/preceding-sibling::label
特定の子要素を持つ親
//div[.//span[text()='MacBook Pro']]
最初の要素
(//button)[1]
最後の要素
(//button)[last()]
属性が存在する
//*[@disabled]
属性が存在しない
//*[not(@disabled)]
class名を厳密に判定
//*[contains(concat(' ', normalize-space(@class), ' '), ' active ')]
47. 実務で使えるサンプル集
「ログイン」ボタン
//button[normalize-space()='ログイン']
メールアドレス入力欄
//input[@type='email']
placeholderが「検索」のinput
//input[@placeholder='検索']
「削除」を含むボタン
//button[contains(normalize-space(.), '削除')]
ユーザー「田中」の編集ボタン
//tr[.//td[normalize-space()='田中']]//button[normalize-space()='編集']
商品「MacBook Pro」の価格
//div[.//*[normalize-space()='MacBook Pro']]//*[contains(@class, 'price')]
「メールアドレス」の横にあるinput
//label[normalize-space()='メールアドレス']/following-sibling::input
activeではない要素
//*[not(contains(concat(' ', normalize-space(@class), ' '), ' active '))]
/users/ を含むリンク
//a[contains(@href, '/users/')]
disabledではないsubmitボタン
//button[@type='submit' and not(@disabled)]
48. XPathが取得できないときのチェックリスト
XPathが正しそうなのに要素を取得できない場合は、構文以外が原因のこともあります。
確認したいポイントは次の通りです。
1. iframe内ではないか
対象要素が iframe 内にある場合、Seleniumなどでは先にiframeへ切り替える必要があります。
2. 要素がまだ描画されていないのではないか
SPAではJavaScriptによって後から要素が生成されることがあります。
その場合はXPathの修正ではなく、Wait処理が必要です。
3. Shadow DOM内ではないか
Shadow DOM内の要素は、通常のXPathだけでは取得できないケースがあります。
4. テキストに余計な空白がないか
//button[text()='ログイン']
で取れないなら、
//button[normalize-space()='ログイン']
を試してみましょう。
5. 子要素にテキストが入っていないか
<button>
<span>ログイン</span>
</button>
の場合、
//button[text()='ログイン']
ではなく、
//button[contains(., 'ログイン')]
の方が適しています。
6. 同じ要素が複数ないか
XPath自体は正しくても、意図しない1件目を取得していることがあります。
DevToolsなどで検索結果の件数も確認すると原因を特定しやすくなります。
まとめ
XPathを実務で使ううえでは、すべての構文を暗記する必要はありません。
まずは次のパターンを押さえておけば、多くのケースに対応できます。
//*[@id='xxx']
//*[@name='xxx']
//button[normalize-space()='保存']
//*[contains(@class, 'xxx')]
//*[contains(., 'xxx')]
//div[.//span[normalize-space()='xxx']]
//span[normalize-space()='xxx']/ancestor::div
//label[normalize-space()='xxx']/following-sibling::input
特に重要なのは、単に「現在取得できるXPath」を書くのではなく、
HTMLが多少変わっても壊れにくいXPathを書く
ことです。
E2Eテストやスクレイピングでは、
- 意味のある属性を使う
- 動的classへの依存を減らす
- position指定に頼りすぎない
-
normalize-space()を活用する - 必要に応じて
ancestorやfollowing-siblingを使う - テスト可能な
data-testid等をアプリ側に用意する
といった設計を意識すると、メンテナンスコストを大きく下げられます。
XPathで困ったときに、この記事をチートシートとして活用してみてください。