よく忘れるのでxpathの書き方をまとめた
pythonでスクレイピングするとき、私は lxml ライブラリを使用して xpath で要素を取得することが多い。Beautiful Soup ライブラリを使えばシンプルに目的の要素を取得できるっぽいが私は使ったことがない。
それはさておき、xpath の書き方を忘れて調べることが多いので自分の見やすいように主要な書き方をまとめます。
書き方
XPathの主要な書き方を表にまとめました。
取得したい要素 | XPath |
---|---|
htmlの子であるbody直下のspan要素 | /html/body/span |
span要素(どこでも) | //span |
bodyの子のspan要素 | //body/span |
bodyの子孫のspan要素 | //body//span |
bodyの子であるすべての要素 | //body/* |
上からn番目のli要素 | //ul/li[n] |
class属性が"hoge"であるp要素 | //p[@class="hoge"] |
class属性に"hoge"が含まれるているp要素 | //p[contains(@class, "hoge")] |
src属性が"https://"で始まるimg要素 | //img[starts-with(@src, "https://")] |
src属性が".png"で終わるimg要素 (XPath2.0以上) |
//img[ends-with(@src, ".png")] |
直下の文字列が"ほげ"であるp要素 | //p[text()="ほげ"] |
直下の文字列に"ほげ"が含まれているp要素 | //p[contains(text(), "ほげ")] |
pythonで動作確認
ざっくり動作確認したいと思います。
※python2で書いてます。
準備
Web上のものを使いたいとろこですが、迷惑がかかると嫌なので適当に作った以下のHTMLファイルを使用します。ホームディレクトリ内に配置したと仮定する。
<!DOCTYPE html> <html> <head> <style type="text/css"> <!-- * { border: solid 1px #000000; padding: 0.5em 1em; margin: 1em 0; } --> </style> <meta charset='utf-8'> <title>タートルちゃんのテストHTML</title> </head> <body> <h1>タートルちゃんのスクレイピング</h1> <p>これはパラグラフです。</p> <span>これは html/body 直下のspanです。</span> <p>タートルちゃんがスクレイピングテスト用に書いたHTMLです。</p> <h2>タートルちゃんの見出し</h2> <img src='https://cdn-ak.f.st-hatena.com/images/fotolife/t/turtlechan/20190903/20190903214546.png' alt='タートルちゃんの画像' width=300> <p>↑タートルちゃん</p> <a href='https://turtlechan.hatenablog.com/'>タートルちゃんのブログ</a> <a href='//localhost'>ローカルホスト</a> <p class='hoge'>ほげ</p> <p class='hogehoge'>ほげほげ</p> <ul> <li>リスト1</li> <li>リスト2</li> <li>リスト3</li> </ul> <div class='turtlechan'> <span>これは html/body/div 直下のspanです。</span> <p>div要素内のパラグラフ</p> <div class='kame'> <span>これは html/body/div/div 直下のspanです。</span> <p>カメ</p> </div> </body> </html>
Webブラウザに読み込ませると以下のような感じになる。要素の親子関係が分かりやすいようにボーダーで囲った(汚い)。
次にpythonで上記のHTMLを読み込む。以下のスクリプトをホームディレクトリに配置して動作を確認する。
#! /usr/bin/env python # coding: utf-8 import lxml.html ''' HTMLファイルの読み込み ''' htmlText = None with open('./test.html', 'r') as f: htmlText = f.read() ''' XPathでパースできるようにHtmlElementを生成 ''' html = lxml.html.fromstring(htmlText) ''' 以下に動作確認するコードを書く ''' elements = html.xpath('ここに動作確認するXPathを記述する') ''' 以上に動作確認するコードを書く ''' ''' printで標準出力に出力 ''' for e in elements: print e.text
HtmlElement.xpath() の戻り値は一つだったとしてもリストで返ってくるので for ループで該当した要素のテキストを出力する形にしています。
動作確認
書き方の項目で書いた順番で確認していきます。
htmlの子であるbody直下のspan要素
elements = html.xpath('/html/body/span')
これは html/body 直下のspanです。
span要素(どこでも)
elements = html.xpath('//span')
これは html/body 直下のspanです。 これは html/body/div 直下のspanです。 これは html/body/div/div 直下のspanです。
先程含まれていなかったspan要素もちゃんと拾われていますね。HTMLファイル内すべてのspan要素が表示されるはずです。
bodyの子のspan要素
elements = html.xpath('//body/span')
これは html/body 直下のspanです。
bodyの子孫のspan要素
elements = html.xpath('//body//span')
これは html/body 直下のspanです。 これは html/body/div 直下のspanです。 これは html/body/div/div 直下のspanです。
bodyの子であるすべての要素
elements = html.xpath('//body/*')
タートルちゃんのスクレイピング これはパラグラフです。 これは html/body 直下のspanです。 タートルちゃんがスクレイピングテスト用に書いたHTMLです。 タートルちゃんの見出し None ↑タートルちゃん タートルちゃんのブログ ローカルホスト ほげ ほげほげ
ちょっと結果が分かりにくいですね。「print e.text」を「print e.tag」にして実行してみるます。
h1 p span p h2 img p a a p p ul div
body要素の子のみが表示されているのが確認できますね。
上からn番目のli要素
ここでは「上から2番目」のli要素にします。
elements = html.xpath('//ul/li[2]')
リスト2
ある要素の子に同じ要素名が複数ある場合に使います。
class属性が"hoge"であるp要素
elements = html.xpath('//p[@class="hoge"]')
ほげ
要素名[@属性名="値"] の形で使います。
class属性に"hoge"が含まれるているp要素
elements = html.xpath('//p[contains(@class, "hoge")]')
ほげ ほげほげ
hoge が含まれていれば該当するので先程はなかった「ほげほげ」もヒットします。
src属性が"http://"で始まるimg要素
elements = html.xpath('//img[starts-with(@src, "https://")]')
None
img要素にテキストが存在しないので結果は None になってます。ちゃんとimg要素は拾われているので text を tag に変えて確認してみてください。
src属性が".png"で終わるimg要素(XPath2.0以上)
elements = html.xpath('//img[ends-with(@src, ".png")]')
Traceback (most recent call last): File "scrape_test.py", line 15, inelements = html.xpath('//img[ends-with(@src, ".png")]') File "src/lxml/etree.pyx", line 1581, in lxml.etree._Element.xpath File "src/lxml/xpath.pxi", line 305, in lxml.etree.XPathElementEvaluator.__call__ File "src/lxml/xpath.pxi", line 225, in lxml.etree._XPathEvaluatorBase._handle_result lxml.etree.XPathEvalError: Unregistered function シェルが値を返しました 1
私の環境では対応していなかったので確認できませんでした。ごめんなさい。
直下の文字列が"ほげ"であるp要素
elements = html.xpath('//p[text()="ほげ"]')
Traceback (most recent call last): File "scrape_test.py", line 15, inelements = html.xpath('//p[text()="ほげ"]') File "src/lxml/etree.pyx", line 1581, in lxml.etree._Element.xpath File "src/lxml/xpath.pxi", line 293, in lxml.etree.XPathElementEvaluator.__call__ File "src/lxml/apihelpers.pxi", line 1527, in lxml.etree._utf8 ValueError: All strings must be XML compatible: Unicode or ASCII, no NULL bytes or control characters シェルが値を返しました 1
怒られる。多分日本語を指定することはできない。
直下の文字列に"ほげ"が含まれているp要素
elements = html.xpath('//p[contains(text(), "ほげ")]')
Traceback (most recent call last): File "scrape_test.py", line 15, inelements = html.xpath('//p[contains(text(), "ほげ")]') File "src/lxml/etree.pyx", line 1581, in lxml.etree._Element.xpath File "src/lxml/xpath.pxi", line 293, in lxml.etree.XPathElementEvaluator.__call__ File "src/lxml/apihelpers.pxi", line 1527, in lxml.etree._utf8 ValueError: All strings must be XML compatible: Unicode or ASCII, no NULL bytes or control characters シェルが値を返しました 1
これも上と同様ですね。
ちなみに日本語でなければ問題なく動作します。
elements = html.xpath('//p[contains(text(), "HTML")]')
タートルちゃんがスクレイピングテスト用に書いたHTMLです。
おわりに
かなりざっくりですがxpathの書き方をまとめました。他にも記述方法があるみたいですが、今の所私はこれで十分です。これだけでも大体のページでは必要な要素のみ抜き出せると思います。
皆様の参考になれば幸いです。