Seleniumを安定稼働させるために行うべき3つの設定(Headlessモードにも対応)

この記事では、Seleniumでクローリング・スクレイピングを実践で安定稼働させるために行うべき設定を3つにまとめました。

  • 待機処理
  • エラー内容を通知する
  • 処理をリトライする

ちゃんと設定すれば、不安定なSeleniumでも、安定して定期実行できるようになるでしょう…!

厳しいことを言うと、苦労して作ったSeleniumテストが不安定だったら現場には無価値ですしね〜。

この記事にまとめた内容が活かされて、安定したクローリング・スクレイピングが行えるようになったら…と祈るばかりです。

待機処理

Seleniumを使用したクローリング・スクレイピングのプログラムを安定稼働させるために、まず設定して欲しい項目があります。

それは、Web Driverの待機処理を追加することです。

Seleniumのエラーの原因は、find_element_xxxメソッドで、指定した要素が見つからないことが95%以上を占めていることがほとんどだと思います。

何でこんなことが起こるのかと言うと、Seleniumがブラウザを操作するのと、人間がブラウザを操作するのでは、Seleniumの方が人間より圧倒的に速いからなんです!

MEMO
通常の人間が行うブラウザ操作であれば、ページの読み込みが完了していないのに、ボタンをクリックしたりテキストを取得したりなんて出来ませんよね。

つまり、Web Driverに正しく『待ち』の時間を設定してあげることでSeleniumのエラーは、大幅に減少します。

これには、『暗黙的な待機』と『明示的な待機』と言う2つのアプローチで解決できることが多いです。Selenium(Headless)の雛形プログラムを見てみましょう。

Python
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秒。

MEMO

暗黙的とは、『特に記述していないけど、実は、本当は裏で何らかの処理をしている』ということを強く示唆する場合に使用されます。

ま、設定してるんですけどね。

Python
driver.implicitly_wait(10) # 秒

一度設定すると、全てのfind_element等の処理時に、要素が見つかるまで指定した最大時間待機させるようにすることができます。

設定した時間内に要素が見つかった場合、残りの時間を無視して次の処理に移ります。

implicitly_waitメソッドを設置する場所は、Chrome Driverを起動させた直後で良いでしょう。

Python
# Chrome Driverを起動する
driver = webdriver.Chrome(executable_path=driver_path, chrome_options=options)

# 要素が見つかるまで、最大10秒間待機する
driver.implicitly_wait(10)

明示的な待機

暗黙的な待機(implicitly_wait)で対応しきれなかった処理に対して、WebDriverWait.untilメソッドで、任意のHTMLの要素が特定の状態になるまで待つ明示的な待機時間を設定することができます。

MEMO

明示的とは、暗黙的の対になる言葉で、『はっきりと記述する』ということを強く示唆する場合に使用されます。

Python
# 指定した要素が表示されるまで、明示的に30秒待機する
selector = '{{ CSSセレクタ }}'
element = WebDriverWait(driver, 30).until(
	EC.visibility_of_element_located((By.CSS_SELECTOR, selector))
)

先程、WebDriverWait.untilメソッドの説明を、任意のHTMLの要素が『特定の状態になる』と説明をしました。

例では、visibility_of_element_locatedメソッドが、ここで言う『特定の状態になる』部分を指しています。

expected_conditionsクラスの例
メソッド 説明
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 loadAjaxなどによるページ読み込み遅延対策』で使うことが多いです。

最終手段

暗黙的な待機(implicitly_waitメソッド)と明示的な待機(expected_conditionsクラス)でも、うまくいかなかった場合の最終手段を紹介します。

それは、Pythonの標準ライブラリのtimeクラスのsleepメソッドを使用して、要素が表示されていようがいまいが強制的に指定した時間だけプログラムをスリープさせる方法です。

Python
import time
time.sleep(30) # 秒

time.sleepメソッドは、シンプルにプログラムの終了時間が遅くなるので、あまり使いたくありません。

無駄に30秒待ったり、30秒経っても読み込めなかったり。あまりスマートな手法じゃないですよね。

time.sleepメソッドはこんなときに使う。

デメリットばかりを羅列しましたが、そんなtime.sleepメソッドでも、役に立つ場合ももちろんあります。

それは、ファイルをダウンロードする処理を書いたあとに、ファイルが完全にダウンロードされるまで待って次の操作に移りたい場合です。

Python
# ファイルをダウンロードする処理
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句を使用します。

Python
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句を使って、失敗しなかったときはループを抜けさせ、リトライが全部失敗したときの処理も行えるようにしました。

Python
for _ in range(3): # 最大3回実行
	try:
		# 失敗しそうな処理
	except Exception as e:
		# 失敗時の処理(不要ならpass)
	else:
		# 失敗しなかった場合は、ループを抜ける
		break
else:
	# リトライが全部失敗したときの処理

先ほど紹介した『エラー内容を通知する方法』と組み合わせることで、任意の回数リトライしたけど駄目だった場合に通知させることができるようになります。

for文の後にelse句を使うと、エラー内容がリトライが全部失敗したときに引き継げないので、任意の変数を用意し代入させます

Python
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、セレクタ、スクショなどを通知する。
	# そのまま処理を続けてもいいし、プログラムを強制終了しても良い

これらの項目をめんどくさがらずにちゃんと設定することが安定したクローリング・スクレイピングに繋がります。(自分への戒めとして…。)

次のように『リトライして失敗したらエラーメッセージを通知する』部分を関数として設定すれば楽ですね。

Python
# リトライして失敗したらエラーメッセージを通知する。成功したら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を設定するやり方はIDclassxpathnameなど色々ありますが、私はBeautiful Soupとの併用も考えて、CSSセレクタだけ使用している派なので…!

まとめ

Seleniumを使用したクローリング・スクレイピングのプログラムを安定稼働させるために、設定して欲しい項目をまとめました。

  • 待機処理
  • エラー内容を通知する
  • 処理をリトライする
Python
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でクローリング/スクレイピングを行うロードマップ

  1. クローリングとスクレイピングをする前の事前準備・知識など
  2. 静的なWebページを『Beautiful Soup』でスクレイピングする
  3. ログインが必要なWebサイトを『Selenium』でクローリングし、『Beautiful Soup』でスクレイピングする
  4. JavaScriptで書かれたページを『Selenium』でスクレイピングする
  5. Web APIやデータセットを使用してスクレイピングする
  6. スクレイピングで得たデータを様々な形式(pandas、BigQuery、スプレッドシート、DBなど)に変換する
  7. クローリング/スクレイピングをローカルマシンで定期実行する方法
  8. クローリング/スクレイピングをサーバーで定期実行する方法
  9. クローリング/スクレイピングを安定させるための3つの設定(待機処理・エラーの通知・処理のリトライ)
  10. クローリング/スクレイピングの次にやるべきこと

Selenium逆引きリファレンス

  1. Seleniumチートシート
  2. 【headlessモード】ブラウザを起ち上げずにSeleniumを実行する方法
  3. アラートダイアログを操作する
  4. セレクトボックスを選択する方法
  5. Basic認証を突破する方法
  6. 2段階認証(6桁のパスコード)を突破する方法
  7. reCAPTCHAを突破する方法
  8. 【target="_blank"対策】driverを別ウィンドウに切り替える方法
  9. 【display:none対策】JavaScriptを実行して隠された要素を表示させる方法
  10. 【表示読み込み対策】時間を遅延させる
  11. Beautiful Soupと組み合わせる
  12. herokuで定期実行させる手順