LoginSignup
23
26

More than 1 year has passed since last update.

SeleniumでスクレイピングをするためのTips

Last updated at Posted at 2022-10-27

はじめに

本記事は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_elementfind_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はとても柔軟ですが、ブラウザベンダは性能テストを通常行っておらず、非常に動作が遅い傾向があります。

参考:ロケータをうまく扱うTips

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要素で構成されたコンテンツの場合は、ページ数を示す数字をクリックしないと次のデータを表示することができません。

スクリーンショット 2022-10-21 16.11.43.png

ソリューションとして上記で紹介した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同様に、実態を指定してアクセスすることで、スクレイピングできます。

参考:iFrame と Frame の操作

その他

プロファイルの使用方法

デフォルトでは、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サイトに過度な負荷を与える場合は、刑法の「第二百三十四条」(電子計算機損壊等業務妨害)に触れる可能性があります。

また取得したデータの取り扱いについて問題がある場合は、著作権法に触れる可能性もあります。

23
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
26