はじめに
本記事はSeleniumでスクレイピングをするための要素の取得方法や、デバッグの考え方などについて記載しています。
本記事の環境についてSeleniumは4.4で、ブラウザはchromeを使用しています。
Seleniumの環境構築の方法は、以前書いたルーチンワークはPythonにやらせよう Seleniumで勤怠処理を自動化するをご参照ください。
前提を確認する
前提が正しくない場合は、デバッグに無駄な時間が発生します。
スクレイピングを実行していて、NoSuchElementException
の例外が出力された場合は、前提を確認しましょう。
URLが正しいこと
NoSuchElementException
の例外が出力された場合は、要素を取得しようとしているページのURLが正しいことを確認しましょう。
HTTPのリダイレクトなど、期待したレスポンスが得れないことは大いに考えられます。
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"[id="app"]"}
Seleniumではcurrent_url
メソッドが用意されています。
参考:現在のURLを取得
current_url
メソッドを使用して、URLの判定を行うことができます。
以下はcurrent_url
の結果と、定義済みであるbase_url
のURLを比較して正しくない場合に、例外を出力する例です。
try:
if browser.current_url == f'{base_url}':
<URLが正しい場合の処理>
else:
raise BatchError('failure')
finally:
pass
なお、名前解決できない場合は以下のエラーメッセージが出力されます。
URIスキームのホスト名が解決できる状況で、NoSuchElementException
のメッセージが出力される場合はパスに誤りがある可能性があります。
selenium.common.exceptions.WebDriverException: Message: unknown error: net::ERR_NAME_NOT_RESOLVED
取得しようとしている要素名が正しいこと
タイポなど取得しようとしている要素名が間違っている場合は、NoSuchElementException
の例外が出力されます。
例えば、CSSセレクタに'app'を指定したつもりで、間違って1個pを多く入力してしまいました。
element = browser.find_element(By.ID, 'appp')
存在しない要素名になるため、NoSuchElementException
の例外が出力されます。
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"[id="appp"]"}
Seleniumではis_enabled()
メソッドが用意されています。
参考:Web要素に関する情報
is_enabled()
メソッドを使用して、要素の判定を行うことができます。
以下の例では要素が有効な場合に、element: True
と出力されます。
element = browser.find_element(By.ID, 'app').is_enabled()
print(f'element: {element}')
要素を取得するために知っておくこと
スクレイピングで要素を取得するためには、ロケーターの位置について理解することが重要です。
ロケーターの動きを理解する
要素を検索するために使用する基本的なメソッドとして、find_element
とfind_elements
が用意されています。
ロケーターの動きや、メソッドの戻り値のデータ型はそれぞれ異なるため、仕様を理解した上で使い分けましょう。
find_element
メソッドは、ロケーターと一致するDOMの最初の要素を取得します。
element = browser.find_element(By.ID, 'wrapper')
print(f'element: {element}')
print(type(element))
上記の出力結果は以下の通りです。
element: <selenium.webdriver.remote.webelement.WebElement (session="83637704440c02c40230f7f3c432a776", element="3962b35a-d2ae-4517-92bf-9d4d15733494")>
<class 'selenium.webdriver.remote.webelement.WebElement'>
find_elements
メソッドは、複数の要素を検索し、リストで返します。
element = browser.find_elements(By.ID, 'wrapper')
print(f'element: {element}')
print(type(element))
以下の通りにリストで返ってきているのが確認できます。
element: [<selenium.webdriver.remote.webelement.WebElement (session="b6272bed38cd78b07fdcba2f7fafbb4c", element="14ba230f-78b6-4e8f-bc08-ccd001d950f4")>]
<class 'list'>
参考:Web要素の検索
要約すると、find_element
メソッドで取得できる要素は最初に検索で一致した1個のみです。複数の要素を取得するためには、find_elements
メソッドを使用します。
XPathとCSSセレクタ
XML Path Language(以下、XPath)は、取得したい要素に適切なIDが存在しない場合などには有効です。
XPathの仕組みを踏まえて、ルートから絶対パスで参照しているため、Webサイトに変更が加えられた場合は取得できなくなる可能性があります。
ユニークなidが使えない場合、きれいに書かれたCSSセレクタが要素を探す方法として適しています。 XPathはCSSセレクタと同様に動作しますが、シンタックスは複雑で大抵の場合デバッグが困難です。 XPathはとても柔軟ですが、ブラウザベンダは性能テストを通常行っておらず、非常に動作が遅い傾向があります。
table
Seleniumは、table要素も取得することができます。
BeautifulSoupと同じような感覚で、取得したtable要素を含むオブジェクトからtr要素及びtd要素を抽出します。
オブジェクトはリスト型で格納されているため、for
文などを実行することでtr要素が取得できます。
以下は、スコープをapp
のIDで絞り込み、table要素のデータを取得する例です。
# id
element = browser.find_element(By.ID, 'app')
# table
table = element.find_element(By.TAG_NAME, 'table')
# tr
tr_list = table.find_elements(By.TAG_NAME, 'tr')
取得した行の集合となるtr_list
のオブジェクトから、セルデータとなるtd_list
をインデックスで指定して抽出します。
テーブルのセルデータが2個の場合は、以下のようにインデックスを指定します。
table_list = []
for index, row in enumerate(tr_list):
# ヘッダはスキップ
if index == 0:
continue
# td
td_list = row.find_elements(By.TAG_NAME, 'td')
td_dict = {'name': td_list[0].text,
'price': td_list[1].text}
table_list.append(td_dict)
button
Webサイトの構成によって、スクレイピングで連続的にデータを取得する難易度は異なります。
以下のようにリクエストヘッダに、クエリパラメータを指定してHTTP通信を行うサイトの場合は、page=
の値をインクリメントするだけなので容易いと思います。
https://<WebサイトのURL>/list/?page=2
SPAなどで構成されたWebサイトで、button要素などクライアントサイドの動きに応じて、コンテンツがロードされて表示が変わるようなサイトの場合は、コンテンツを読み込むための命令が必要です。
従って以下のようにbutton要素で構成されたコンテンツの場合は、ページ数を示す数字をクリックしないと次のデータを表示することができません。
ソリューションとして上記で紹介したis_enabled()
メソッドを使用して、ボタンが存在する場合は次のコンテンツを読み込み、存在しない場合はそのページから抜けるなどのロジックを組めます。
以下の例ではtable要素で構成されたコンテンツに対して、CSSセレクタのnth-childをインクリメントすることで、次に読み込むボタンを判定しています。
button.is_enabled()
の結果がTrueの場合は、ボタンをクリックして次のコンテンツを読み込みます。
flag = True
num = 2
while flag is True:
# id
element = browser.find_element(By.ID, 'app')
# table
table = element.find_element(By.TAG_NAME, 'table')
# tr
tr_list = table.find_elements(By.TAG_NAME, 'tr')
try:
if flag is True:
num += 1
button = element.find_element(By.CSS_SELECTOR, "CSSセレクタのパス > li:nth-child("+(str(num))+") > button")
if button.is_enabled() is True:
button.click()
else:
flag = False
except common.exceptions.NoSuchElementException as e:
continue
frameとiframe
HTML5では、frame要素及びframeset要素は非推奨とされています。
レガシーなシステムなどで見かけることがあると思いますが、iframe同様に、実態を指定してアクセスすることで、スクレイピングできます。
その他
プロファイルの使用方法
デフォルトでは、ChromeDriverはセッションごとに新しい一時プロファイルを作成します。
セッションなどのログイン情報を引き継ぎたい場合は、Chromeのユーザープロファイルを使用することで解決できます。
以下はドライバのオプションに、Chromeのユーザープロファイルを使用する例です。
profile_path
はChromeを起動して、chrome://version/
にアクセスすることで確認できます。
from selenium import webdriver
options = webdriver.chrome.options.Options()
profile_path = '/Users/<Your Name>/Library/Application Support/Google/Chrome/Default'
options.add_argument('--user-data-dir=' + profile_path)
参考:Options
ドライバのオプションに関する詳細は、Seleniumのドキュメントではなく、ChromeDriverのCommon use casesより確認できます。
try/except
要素が見つからないため、NoSuchElementException
の例外が出力されても処理を中断することなく続けて実行したい場合は、common.exceptions.NoSuchElementException
を使用して例外をキャッチできます。
from selenium import common
try:
<何らかの処理>
except common.exceptions.NoSuchElementException as e:
continue
スリープ処理
要素が存在するのに関わらず、レンダリングに時間がかかるときはNoSuchElementException
が発生して、要素が取得できない場合があります。
対策として、標準モジュールのtime.sleep
関数を使用すれば手軽にスリープ処理を実装できますが、Seleniumには明示的な待機と、暗黙的な待機と呼ばれる2つの手段が用意されています。
参考:待機
ゾンビプロセス
Selenium実行時に正しくquit()
で終了しないと、バッググラウンドで実行されているドライバのゾンビプロセスが残ります。
サーバで、cronなどを用いて定期的に実行している場合は、メモリが開放されないため、無駄なメモリを要する原因になります。
hoge 68212 0.0 0.0 37946300 6680 s002 S 4:16PM 0:05.48 /Users/hoge/.wdm/drivers/chromedriver/mac64/106.0.5249/chromedriver --port=61942
hoge 67990 0.0 0.0 37946300 6668 s002 S 4:04PM 0:02.43 /Users/hoge/.wdm/drivers/chromedriver/mac64/106.0.5249/chromedriver --port=58852
hoge 67539 0.0 0.0 37957564 6684 s002 S 3:32PM 0:01.95 /Users/hoge/.wdm/drivers/chromedriver/mac64/106.0.5249/chromedriver --port=50768
hoge 66708 0.0 0.0 37946300 6660 s002 S 2:48PM 0:00.45 /Users/hoge/.wdm/drivers/chromedriver/mac64/106.0.5249/chromedriver --port=55595
MacやLinuxなどシェルが使用できる環境の場合に、ワンライナーで処理する例を以下に記載します。
ps=`ps aux | grep 'chromedriver' | grep -v grep | awk '{print $2 }'`;for i in $ps; do kill $i; done
参考:Quitting the browser at the end of a session
Seleniumはquit()
を使用して終了しましょう。
おわりに
スクレイピングを行う際は、法律についても理解しましょう。
過去には偽計業務妨害容疑で立件された事例や、Webサイトに過度な負荷を与える場合は、刑法の「第二百三十四条」(電子計算機損壊等業務妨害)に触れる可能性があります。
また取得したデータの取り扱いについて問題がある場合は、著作権法に触れる可能性もあります。