この記事では、Seleniumでクローリング・スクレイピングを実践で安定稼働させるために行うべき設定を3つにまとめました。
- 待機処理
- エラー内容を通知する
- 処理をリトライする
ちゃんと設定すれば、不安定なSeleniumでも、安定して定期実行できるようになるでしょう…!
厳しいことを言うと、苦労して作ったSeleniumテストが不安定だったら現場には無価値ですしね〜。
この記事にまとめた内容が活かされて、安定したクローリング・スクレイピングが行えるようになったら…と祈るばかりです。
目次
待機処理
Seleniumを使用したクローリング・スクレイピングのプログラムを安定稼働させるために、まず設定して欲しい項目があります。
それは、Web Driverの待機処理を追加することです。
Seleniumのエラーの原因は、find_element_xxx
メソッドで、指定した要素が見つからないことが95%以上を占めていることがほとんどだと思います。
何でこんなことが起こるのかと言うと、Seleniumがブラウザを操作するのと、人間がブラウザを操作するのでは、Seleniumの方が人間より圧倒的に速いからなんです!
つまり、Web Driverに正しく『待ち』の時間を設定してあげることでSeleniumのエラーは、大幅に減少します。
これには、『暗黙的な待機』と『明示的な待機』と言う2つのアプローチで解決できることが多いです。Selenium(Headless)の雛形プログラムを見てみましょう。
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
# Chrome Optionsの設定
options = Options()
options.add_argument('--headless') # headlessモードを使用する
options.add_argument('--disable-gpu') # headlessモードで暫定的に必要なフラグ(そのうち不要になる)
options.add_argument('--disable-extensions') # すべての拡張機能を無効にする。ユーザースクリプトも無効にする
options.add_argument('--proxy-server="direct://"') # Proxy経由ではなく直接接続する
options.add_argument('--proxy-bypass-list=*') # すべてのホスト名
options.add_argument('--start-maximized') # 起動時にウィンドウを最大化する
# Chrome DriverのPath
driver_path = '{{ Chrome DriverのPath }}'
# Chrome Driverを起動する
driver = webdriver.Chrome(executable_path=driver_path, chrome_options=options)
#
# (1) 暗黙的な待機
#
# Webページを開く
driver.get('{{ クローリングするURL }}')
#
# (2) 明示的な待機
#
# Chrome Driverを終了する
driver.quit()
暗黙的な待機
implicitly_wait
メソッドを使用すると、暗黙的な待機時間を設定することができます。デフォルト:0秒。
暗黙的とは、『特に記述していないけど、実は、本当は裏で何らかの処理をしている』ということを強く示唆する場合に使用されます。
ま、設定してるんですけどね。
driver.implicitly_wait(10) # 秒
一度設定すると、全てのfind_element
等の処理時に、要素が見つかるまで指定した最大時間待機させるようにすることができます。
設定した時間内に要素が見つかった場合、残りの時間を無視して次の処理に移ります。
implicitly_wait
メソッドを設置する場所は、Chrome Driverを起動させた直後で良いでしょう。
# Chrome Driverを起動する
driver = webdriver.Chrome(executable_path=driver_path, chrome_options=options)
# 要素が見つかるまで、最大10秒間待機する
driver.implicitly_wait(10)
明示的な待機
暗黙的な待機(implicitly_wait
)で対応しきれなかった処理に対して、WebDriverWait.until
メソッドで、任意のHTMLの要素が特定の状態になるまで待つ明示的な待機時間を設定することができます。
明示的とは、暗黙的の対になる言葉で、『はっきりと記述する』ということを強く示唆する場合に使用されます。
# 指定した要素が表示されるまで、明示的に30秒待機する
selector = '{{ CSSセレクタ }}'
element = WebDriverWait(driver, 30).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, selector))
)
先程、WebDriverWait.until
メソッドの説明を、任意のHTMLの要素が『特定の状態になる』と説明をしました。
例では、visibility_of_element_located
メソッドが、ここで言う『特定の状態になる』部分を指しています。
メソッド | 説明 |
---|---|
alert_is_present |
Alertが表示されるまで待機する |
element_to_be_clickable |
要素がクリック出来る状態になるまで待機する |
visibility_of_element_located |
指定した要素が表示されるまで待機する |
invisibility_of_element_located |
指定した要素が非表示になるまで待機する |
text_to_be_present_in_element |
指定したテキストが表示されるまで待機する |
presence_of_element_located |
指定した要素がDOM上に現れるまで待機する |
他にも気になる人はこのページで確認して下さい。
実際のSeleniumのプログラムを組むときは、visibility_of_element_located
を『ページ遷移した直後の操作』や『ファイルのダウンロードボタンのクリック』、『Lazy load
やAjax
などによるページ読み込み遅延対策』で使うことが多いです。
最終手段
暗黙的な待機(implicitly_wait
メソッド)と明示的な待機(expected_conditions
クラス)でも、うまくいかなかった場合の最終手段を紹介します。
それは、Pythonの標準ライブラリのtime
クラスのsleep
メソッドを使用して、要素が表示されていようがいまいが強制的に指定した時間だけプログラムをスリープさせる方法です。
import time
time.sleep(30) # 秒
time.sleep
メソッドは、シンプルにプログラムの終了時間が遅くなるので、あまり使いたくありません。
無駄に30秒待ったり、30秒経っても読み込めなかったり。あまりスマートな手法じゃないですよね。
time.sleep
メソッドはこんなときに使う。
デメリットばかりを羅列しましたが、そんなtime.sleep
メソッドでも、役に立つ場合ももちろんあります。
それは、ファイルをダウンロードする処理を書いたあとに、ファイルが完全にダウンロードされるまで待って次の操作に移りたい場合です。
# ファイルをダウンロードする処理
selector = '{{ CSSセレクタ }}'
element = WebDriverWait(driver, 30).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, selector))
)
element.click()
# ファイルがダウンロードされるまで、とりあえず10秒スリープする
time.sleep(10)
#
# 次の処理
#
まだ試していないので何とも言えませんが、オリジナルのカスタム待機条件を実装してファイルのダウンロードが完了するまでWeb Driverを待機させている強者を見つけました。
参考 ファイルのダウンロードが完了するまでWebDriverを待機させるにはQiitaエラー内容を通知する
Seleniumを使用したクローリング・スクレイピングのプログラムを安定稼働させるために、設定して欲しい2つ目の項目は、『エラー内容をちゃんと通知して、プログラムをどんどん改良していくこと』です。
不安安なSeleniumだからこそ、「1回作って終わり」ではなく、エラーが起きるたびに2度と同じエラーが起きないように努めましょう。
エラー内容を通知させる方法として、Pythonのtry-except
句を使用します。
from selenium.common.exceptions import TimeoutException
try:
# 失敗しそうな処理
selector = '{{ CSSセレクタ }}'
element = WebDriverWait(driver, 30).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, selector))
)
except TimeoutException as e:
# 失敗時の処理
# エラー内容(e)や実行時間、操作中のURL、セレクタ、スクショなどを通知する。
# そのまま処理を続けてもいいし、プログラムを強制終了しても良い
エラーの通知先は、プロジェクトに応じてSlackなりチャットワークなりLINEなりメールなりと、好きにすれば良いと思います。
処理をリトライする
Seleniumを使用したクローリング・スクレイピングのプログラムを安定稼働させるために、設定して欲しい最後の項目は、『エラーが起きても、任意の回数リトライさせること』です。
『成功し続けるまでループする』だと永遠に失敗し続ける場合が怖すぎるので、リトライする回数に制限を設けます。
for
文の後にelse
句を使って、失敗しなかったときはループを抜けさせ、リトライが全部失敗したときの処理も行えるようにしました。
for _ in range(3): # 最大3回実行
try:
# 失敗しそうな処理
except Exception as e:
# 失敗時の処理(不要ならpass)
else:
# 失敗しなかった場合は、ループを抜ける
break
else:
# リトライが全部失敗したときの処理
先ほど紹介した『エラー内容を通知する方法』と組み合わせることで、任意の回数リトライしたけど駄目だった場合に通知させることができるようになります。
for
文の後にelse
句を使うと、エラー内容がリトライが全部失敗したときに引き継げないので、任意の変数を用意し代入させます
from selenium.common.exceptions import TimeoutException
error = ''
for _ in range(3): # 最大3回実行
try:
# 失敗しそうな処理
selector = '{{ CSSセレクタ }}'
element = WebDriverWait(driver, 30).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, selector))
)
except TimeoutException as e:
# エラーメッセージを格納する
error = e
else:
# 失敗しなかった場合は、ループを抜ける
break
else:
# リトライが全部失敗したときの処理
# エラー内容(error)や実行時間、操作中のURL、セレクタ、スクショなどを通知する。
# そのまま処理を続けてもいいし、プログラムを強制終了しても良い
これらの項目をめんどくさがらずにちゃんと設定することが安定したクローリング・スクレイピングに繋がります。(自分への戒めとして…。)
次のように『リトライして失敗したらエラーメッセージを通知する』部分を関数として設定すれば楽ですね。
# リトライして失敗したらエラーメッセージを通知する。成功したらelementを返す
def getElement(css_selector, wait_second, retries_count):
error = ''
for _ in range(retries_count):
try:
# 失敗しそうな処理
selector = css_selector
element = WebDriverWait(driver, wait_second).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, selector))
)
except TimeoutException as e:
# エラーメッセージを格納する
error = e
else:
# 失敗しなかった場合は、ループを抜ける
break
else:
# リトライが全部失敗したときの処理
# エラー内容(error)や実行時間、操作中のURL、セレクタ、スクショなどを通知する。
do_something() # 好きな処理
# プログラムを強制終了する
sys.exit(1)
return element
element = getElement('{{ CSSセレクタ }}', {{ 待機時間 }}, {{ リトライする回数 }})
# element.click()
# element.text
# element.get_attribute('属性名')
# element.send_keys('入力する文字列')
# element.clear()
elementを設定するやり方はID
やclass
、xpath
、name
など色々ありますが、私はBeautiful Soup
との併用も考えて、CSSセレクタだけ使用している派なので…!
まとめ
Seleniumを使用したクローリング・スクレイピングのプログラムを安定稼働させるために、設定して欲しい項目をまとめました。
- 待機処理
- エラー内容を通知する
- 処理をリトライする
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException
# Chrome Optionsの設定
options = Options()
options.add_argument('--headless') # headlessモードを使用する
options.add_argument('--disable-gpu') # headlessモードで暫定的に必要なフラグ(そのうち不要になる)
options.add_argument('--disable-extensions') # すべての拡張機能を無効にする。ユーザースクリプトも無効にする
options.add_argument('--proxy-server="direct://"') # Proxy経由ではなく直接接続する
options.add_argument('--proxy-bypass-list=*') # すべてのホスト名
options.add_argument('--start-maximized') # 起動時にウィンドウを最大化する
# Chrome DriverのPath
driver_path = '{{ Chrome DriverのPath }}'
# Chrome Driverを起動する
driver = webdriver.Chrome(executable_path=driver_path, chrome_options=options)
# 要素が見つかるまで、最大10秒間待機する
driver.implicitly_wait(10)
# Webページを開く
driver.get('{{ クローリングするURL }}')
# リトライして失敗したらエラーメッセージを通知する。成功したらelementを返す
def getElement(css_selector, wait_second, retries_count):
error = ''
for _ in range(retries_count):
try:
# 失敗しそうな処理
selector = css_selector
element = WebDriverWait(driver, wait_second).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, selector))
)
except TimeoutException as e:
# エラーメッセージを格納する
error = e
else:
# 失敗しなかった場合は、ループを抜ける
break
else:
# リトライが全部失敗したときの処理
# エラー内容(error)や実行時間、操作中のURL、セレクタ、スクショなどを通知する。
do_something() # 好きな処理
# プログラムを強制終了する
sys.exit(1)
return element
# 処理1
element = getElement('{{ CSSセレクタ }}', {{ 待機時間 }}, {{ リトライする回数 }})
element.click()
# 処理2
element = getElement('{{ CSSセレクタ }}', {{ 待機時間 }}, {{ リトライする回数 }})
element.text
# 処理3
element = getElement('{{ CSSセレクタ }}', {{ 待機時間 }}, {{ リトライする回数 }})
element.click()
#
# 処理…
#
# Chrome Driverを終了する
driver.quit()
連載:Pythonでクローリング/スクレイピング
《超実践的》Pythonでクローリング/スクレイピングを行うロードマップ
クローリングとスクレイピングをする前の事前準備・知識など- 静的なWebページを『Beautiful Soup』でスクレイピングする
ログインが必要なWebサイトを『Selenium』でクローリングし、『Beautiful Soup』でスクレイピングするJavaScriptで書かれたページを『Selenium』でスクレイピングするWeb APIやデータセットを使用してスクレイピングするスクレイピングで得たデータを様々な形式(pandas、BigQuery、スプレッドシート、DBなど)に変換するクローリング/スクレイピングをローカルマシンで定期実行する方法- クローリング/スクレイピングをサーバーで定期実行する方法
- クローリング/スクレイピングを安定させるための3つの設定(待機処理・エラーの通知・処理のリトライ)
クローリング/スクレイピングの次にやるべきこと
Selenium逆引きリファレンス
- Seleniumチートシート
【headlessモード】ブラウザを起ち上げずにSeleniumを実行する方法アラートダイアログを操作するセレクトボックスを選択する方法Basic認証を突破する方法- 2段階認証(6桁のパスコード)を突破する方法
- reCAPTCHAを突破する方法
- 【target="_blank"対策】driverを別ウィンドウに切り替える方法
【display:none対策】JavaScriptを実行して隠された要素を表示させる方法【表示読み込み対策】時間を遅延させるBeautiful Soupと組み合わせるherokuで定期実行させる手順