Seleniumでブラウザ操作をする際、「要素が表示されるまで待ちたい」「クリック可能になるまで待ちたい」などのタイミング調整は非常に重要です
単に time.sleep()
を使うと、処理が無駄に遅くなったり、逆に待ちが足りずにエラーになることが頻繁にあります
今回はPython + Selenium における待機処理のベストプラクティスとして、WebDriverWait
と expected_conditions
を使った「より良い待機」について詳しく解説します
WebDriverWaitについて
指定した条件を満たすまで待機する便利なモジュールです
Seleniumでブラウザ操作をする上で特定の条件を満たすまで待機するシーンは多く、”time.sleepでxx秒待つ“にすると時間が長過ぎると無駄が多く、短過ぎるとエラーになりがちな雑なプログラムになってしまうのではこのWebDriverWaitは必須レベル
ただし、経験上WebDriverWaitだけではうまくいかない場合があり、最後に0.5秒ほどsleepしてあげると処理が安定する気がします
基本的なパターンはコチラ
WebDriverWait(webdriver, タイムアウトまでの上限秒数).until(expected_conditionsで設定する条件)
実際にサンプルソースにするとこんな感じです
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
driver = webdriver.Chrome()
driver.get('https://javeo.jp/practice_scraping/')
try:
WebDriverWait(driver, 10).until(EC.presence_of_element_located((By.CSS_SELECTOR, '#hoge')))
print('条件を満たしました')
except TimeoutException:
print('条件を満たせませんでした')
driver.quit()
expected_conditionsについて
WebDriverWaitとセットで使うのがこのexpected_conditions
結局のところexpected_conditionsが肝心の条件部分で、WebDriverWaitはこの条件が正常終了 or Trueになるまで待つだけの仕様
ただこのexpected_conditionsは良くも悪くも指定できる条件が多く、使いこなすには事前把握が望ましいってことでまとめてみました
expected_conditionsのオススメ関数と全関数
↓の表で”個人的によく使う“タブはその名の通り私が個人的によく使う・使えそうだと思うものだけを、”分類別の全関数“ではある程度グループ分けした関数を全て目次のようにしています
調べて気づきましたが思ったより多くの関数があって利用シーンが謎なものもチラホラ・・
また、expected_conditionsの返り値はいろいろありますが、WebDriverWaitとセットで使うとWebDriverWaitの仕様上、返り値は正常終了時の値 or エラー(”TimeoutException“)になるのでtry exceptするときは注意
- 個人的によく使う
- 分類別の全関数
関数 | 条件 |
---|---|
presence_of_element_located | DOM上に存在しているか |
visibility_of_element_located | 画面上に表示されているか |
invisibility_of_element_located | 画面上で非表示になっているか |
element_to_be_clickable | 画面上に表示されてかつ利用できる状態か |
分類 | 返値の型 |
---|---|
DOMで判定する ※画面上に見えているかは加味しない | |
presence_of_element_located | WebElement |
presence_of_all_elements_located | WebElement |
画面上の表示・非表示で判定する | |
visibility_of | WebElement |
visibility_of_element_located | WebElement |
visibility_of_all_elements_located | List |
visibility_of_any_elements_located | List |
invisibility_of_element | bool |
invisibility_of_element_located | bool |
elementの状態で判定する | |
element_attribute_to_include | bool |
element_located_selection_state_to_be | bool |
element_located_to_be_selected | bool |
element_selection_state_to_be | bool |
element_to_be_clickable | WebElement |
element_to_be_selected | bool |
文字や要素の値で判定する | |
text_to_be_present_in_element | bool |
text_to_be_present_in_element_attribute | bool |
text_to_be_present_in_element_value | bool |
ページタイトルで判定する | |
title_contains | bool |
title_is | bool |
ページURLで判定する | |
url_changes | bool |
url_contains | bool |
url_matches | bool |
url_to_be | bool |
ブラウザのウィンドウで判定する | |
number_of_windows_to_be | bool |
new_window_is_opened | bool |
frame要素で判定する | |
frame_to_be_available_and_switch_to_it | bool |
staleness_of | bool |
アラートポップアップで判定する | |
alert_is_present | Alert |
複合条件にする | |
all_of | List |
any_of | List |
none_of | bool |
presence_of_element_located(locator: Tuple[str, str])
引数のlocatorがDOMに存在すしていれば返り値は該当したWebElementを返す(DOMなので画面上に見えるかどうかは問わない)
内部的にはfind_elementでlocatorの有無確認しているだけ
def presence_of_element_located(locator: tuple[str, str]) -> Callable[[WebDriverOrWebElement], WebElement]:
def _predicate(driver: WebDriverOrWebElement):
return driver.find_element(*locator)
return _predicate
presence_of_all_elements_located(locator: Tuple[str, str])
引数のlocatorがDOMに存在すしていれば返り値は該当した全てのWebElementを返す(DOMなので画面上に見えるかどうかは問わない)
待機するだけならpresence_of_element_locatedと同じ
def presence_of_all_elements_located(locator: tuple[str, str]) -> Callable[[WebDriverOrWebElement], list[WebElement]]:
def _predicate(driver: WebDriverOrWebElement):
return driver.find_elements(*locator)
return _predicate
visibility_of_element_located(locator: Tuple[str, str])
引数のlocatorが画面上に見えていれば返り値は該当したWebElementを返す
実態はlocatorの有無確認と”is_displayed()“をしているだけ
def visibility_of_element_located(
locator: tuple[str, str],
) -> Callable[[WebDriverOrWebElement], Union[Literal[False], WebElement]]:
def _predicate(driver: WebDriverOrWebElement):
try:
return _element_if_visible(driver.find_element(*locator))
except StaleElementReferenceException:
return False
return _predicate
def _element_if_visible(element: WebElement, visibility: bool = True) -> Union[Literal[False], WebElement]:
return element if element.is_displayed() == visibility else False
visibility_of(element: WebElement)
引数のelementが画面上に見えていれば返り値は該当したWebElementを返す
引数にしたWebElementがそのまま返り値になるので単純な待機処理としてしか使うことはなさそう
def visibility_of(element: WebElement) -> Callable[[Any], Union[Literal[False], WebElement]]:
def _predicate(_):
return _element_if_visible(element)
return _predicate
visibility_of_all_elements_located(locator: Tuple[str, str])
引数のlocatorが全て画面上に見えていれば返り値は該当した全てのWebElementを返す
ただし、locatorが1つでもDOM上に存在しないか非表示(hidden)になっていればエラーになる絶妙仕様なので使うことはあまりなさそう
def visibility_of_all_elements_located(
locator: tuple[str, str],
) -> Callable[[WebDriverOrWebElement], Union[list[WebElement], Literal[False]]]:
def _predicate(driver: WebDriverOrWebElement):
try:
elements = driver.find_elements(*locator)
for element in elements:
if _element_if_visible(element, visibility=False):
return False
return elements
except StaleElementReferenceException:
return False
return _predicate
visibility_of_any_elements_located(locator: Tuple[str, str])
引数のlocatorが画面上に見えていれば返り値は該当した全てのWebElementを返す
こちらはlocatorがDOM上に存在しない場合のみエラーになり、一つでも表示されていれば表示されているWebElementを返してくれる
def visibility_of_any_elements_located(locator: tuple[str, str]) -> Callable[[WebDriverOrWebElement], list[WebElement]]:
def _predicate(driver: WebDriverOrWebElement):
return [element for element in driver.find_elements(*locator) if _element_if_visible(element)]
return _predicate
invisibility_of_element_located(locator: Union[WebElement, Tuple[str, str]])
visibility_of_element_locatedの逆でDOM上に見えてないことを検知してくれる
DOM上になければTrue、DOM上にあるけど非表示なら対象のWebElementを返してくれる
def invisibility_of_element_located(
locator: Union[WebElement, tuple[str, str]],
) -> Callable[[WebDriverOrWebElement], Union[WebElement, bool]]:
def _predicate(driver: WebDriverOrWebElement):
try:
target = locator
if not isinstance(target, WebElement):
target = driver.find_element(*target)
return _element_if_visible(target, visibility=False)
except (NoSuchElementException, StaleElementReferenceException):
# In the case of NoSuchElement, returns true because the element is
# not present in DOM. The try block checks if the element is present
# but is invisible.
# In the case of StaleElementReference, returns true because stale
# element reference implies that element is no longer visible.
return True
return _predicate
invisibility_of_element(element: Union[WebElement, Tuple[str, str]])
実はinvisibility_of_element_locatedのシノニム
見ての通り実はlocatorでも受け付けできて、invisibility_of_element_locatedをそのまま返してるだけ
def invisibility_of_element(
element: Union[WebElement, tuple[str, str]],
) -> Callable[[WebDriverOrWebElement], Union[WebElement, bool]]:
return invisibility_of_element_located(element)
element_attribute_to_include(locator: Tuple[str, str], attribute_: str)
引数のlocatorがDOM上に存在し、さらにそのタグの中に引数のattribute_属性有無で照合(それ以外はエラー)
使い道としてはjsで要素が追加される時ぐらいですかね
def element_attribute_to_include(locator: tuple[str, str], attribute_: str) -> Callable[[WebDriverOrWebElement], bool]:
def _predicate(driver: WebDriverOrWebElement):
try:
element_attribute = driver.find_element(*locator).get_attribute(attribute_)
return element_attribute is not None
except StaleElementReferenceException:
return False
return _predicate
element_located_selection_state_to_be(locator: Tuple[str, str], is_selected: bool)
引数のlocatorがDOM上に存在し、さらにその要素の選択状況と引数のis_selected()が判定を返してくれる
locatorがDOM上に存在しない時はエラーになります
チェックボックスやラジオボタンではない場合、そもそも選択の概念がないのでis_selected()はFalseになる
def element_located_selection_state_to_be(
locator: tuple[str, str], is_selected: bool
) -> Callable[[WebDriverOrWebElement], bool]:
def _predicate(driver: WebDriverOrWebElement):
try:
element = driver.find_element(*locator)
return element.is_selected() == is_selected
except StaleElementReferenceException:
return False
return _predicate
element_located_to_be_selected(locator: Tuple[str, str])
引数のlocatorが選択されている状態であればで照合
is_selected()はあまり使う機会ないと思うんですよね
def element_located_to_be_selected(locator: tuple[str, str]) -> Callable[[WebDriverOrWebElement], bool]:
def _predicate(driver: WebDriverOrWebElement):
return driver.find_element(*locator).is_selected()
return _predicate
element_to_be_selected(element: WebElement)
element_located_to_be_selectedの引数がlocatorからWebElementになった版
def element_to_be_selected(element: WebElement) -> Callable[[Any], bool]:
def _predicate(_):
return element.is_selected()
return _predicate
element_selection_state_to_be(element: WebElement, is_selected: bool)
element_to_be_selectedのis_selected() == Falseが選べるようになっただけ
def element_selection_state_to_be(element: WebElement, is_selected: bool) -> Callable[[Any], bool]:
def _predicate(_):
return element.is_selected() == is_selected
return _predicate
element_to_be_clickable(mark: Union[WebElement, Tuple[str, str]])
まず引数がmark(WebElementとlocatorのどちらでもOK)になっているのに好感
※全部そうしてほしい
内部的にはvisibility_ofかつis_enabled()なのでclickableの名前からcheckbox向けのような印象を受けますが、inputタグなどにも有効(むしろこっちの方が利用頻度高い)
def element_to_be_clickable(
mark: Union[WebElement, tuple[str, str]],
) -> Callable[[WebDriverOrWebElement], Union[Literal[False], WebElement]]:
# renamed argument to 'mark', to indicate that both locator
# and WebElement args are valid
def _predicate(driver: WebDriverOrWebElement):
target = mark
if not isinstance(target, WebElement): # if given locator instead of WebElement
target = driver.find_element(*target) # grab element at locator
element = visibility_of(target)(driver)
if element and element.is_enabled():
return element
return False
return _predicate
text_to_be_present_in_element(locator: Tuple[str, str], text_: str)
XPATHの[text()=’hoge’]を事前チェックできる感じ
需要がありそうでなさそう、、、でやっぱりありそう
def text_to_be_present_in_element(locator: tuple[str, str], text_: str) -> Callable[[WebDriverOrWebElement], bool]:
def _predicate(driver: WebDriverOrWebElement):
try:
element_text = driver.find_element(*locator).text
return text_ in element_text
except StaleElementReferenceException:
return False
return _predicate
text_to_be_present_in_element_value(locator: Tuple[str, str], text_: str)
text_to_be_present_in_elementをtextからvalue属性に変えてみました
もともと微妙な需要だったのにさらに下がってる気がする
def text_to_be_present_in_element_value(
locator: tuple[str, str], text_: str
) -> Callable[[WebDriverOrWebElement], bool]:
def _predicate(driver: WebDriverOrWebElement):
try:
element_text = driver.find_element(*locator).get_attribute("value")
if element_text is None:
return False
return text_ in element_text
except StaleElementReferenceException:
return False
return _predicate
text_to_be_present_in_element_attribute(locator: Tuple[str, str], attribute_: str, text_: str)
text_to_be_present_in_element_valueをvalue以外の属性を選べるようにしてみました
じゃあtext_to_be_present_in_element_valueいらない・・・
def text_to_be_present_in_element_attribute(
locator: tuple[str, str], attribute_: str, text_: str
) -> Callable[[WebDriverOrWebElement], bool]:
def _predicate(driver: WebDriverOrWebElement):
try:
element_text = driver.find_element(*locator).get_attribute(attribute_)
if element_text is None:
return False
return text_ in element_text
except StaleElementReferenceException:
return False
return _predicate
title_is(title: str)
titleとの完全一致判定
title自体ちょっと使いにくいのに完全一致はなかなか難しい印象
def title_is(title: str) -> Callable[[WebDriver], bool]:
def _predicate(driver: WebDriver):
return driver.title == title
return _predicate
title_contains(title: str)
こちらはtitleの部分一致判定
完全一致よりは現実的だけどやっぱりtitleで照合はしないと思う
def title_contains(title: str) -> Callable[[WebDriver], bool]:
def _predicate(driver: WebDriver):
return title in driver.title
return _predicate
url_contains(url: str)
urlの部分一致で判定
待機処理の条件にurlは使わないと思う
def url_contains(url: str) -> Callable[[WebDriver], bool]:
def _predicate(driver: WebDriver):
return url in driver.current_url
return _predicate
url_matches(pattern: str)
こちらはurlを正規表現で判定
だから待機処理の条件にurlは・・・
def url_matches(pattern: str) -> Callable[[WebDriver], bool]:
def _predicate(driver: WebDriver):
return re.search(pattern, driver.current_url) is not None
return _predicate
url_to_be(url: str)
urlの完全一致で判定
だから待機処理の・・・
def url_to_be(url: str) -> Callable[[WebDriver], bool]:
def _predicate(driver: WebDriver):
return url == driver.current_url
return _predicate
url_changes(url: str)
urlの不一致で判定
だから・・・
def url_changes(url: str) -> Callable[[WebDriver], bool]:
def _predicate(driver: WebDriver):
return url != driver.current_url
return _predicate
number_of_windows_to_be(num_windows: int)
ウィンドウ(=タブ)の数で照合
いつ使うんですか?
def number_of_windows_to_be(num_windows: int) -> Callable[[WebDriver], bool]:
def _predicate(driver: WebDriver):
return len(driver.window_handles) == num_windows
return _predicate
new_window_is_opened(current_handles: List[str])
新しいウィンドウができるまで待つらしい
いつ使うんですか?(2回目)
def new_window_is_opened(current_handles: list[str]) -> Callable[[WebDriver], bool]:
def _predicate(driver: WebDriver):
return len(driver.window_handles) > len(current_handles)
return _predicate
frame_to_be_available_and_switch_to_it(locator: Union[Tuple[str, str], str])
指定のframeに移動できるまで待つ
待機処理でじゃなくてswitch_to.frameを安全にする使い方なら需要ありそう
def frame_to_be_available_and_switch_to_it(
locator: Union[tuple[str, str], str, WebElement],
) -> Callable[[WebDriver], bool]:
def _predicate(driver: WebDriver):
try:
if isinstance(locator, Iterable) and not isinstance(locator, str):
driver.switch_to.frame(driver.find_element(*locator))
else:
driver.switch_to.frame(locator)
return True
except NoSuchFrameException:
return False
return _predicate
staleness_of(element: WebElement)
なんでこんな名前になったのか、、実態はis_enabledだけ
def staleness_of(element: WebElement) -> Callable[[Any], bool]:
def _predicate(_):
try:
# Calling any method forces a staleness check
element.is_enabled()
return False
except StaleElementReferenceException:
return True
alert_is_present()
アラートに遷移できるか
frame_to_be_available_and_switch_to_itと同じく、待機処理でじゃなくてswitch_to.alertを安全にする使い方なら需要ありそう
def alert_is_present() -> Callable[[WebDriver], Union[Alert, Literal[False]]]:
def _predicate(driver: WebDriver):
try:
return driver.switch_to.alert
except NoAlertPresentException:
return False
return _predicate
any_of(*expected_conditions: Callable[[D], T])
指定した複数のexpected_conditionsが一つでもTrueになればOK
def any_of(*expected_conditions: Callable[[D], T]) -> Callable[[D], Union[Literal[False], T]]:
def any_of_condition(driver: D):
for expected_condition in expected_conditions:
try:
result = expected_condition(driver)
if result:
return result
except WebDriverException:
pass
return False
return any_of_condition
all_of(*expected_conditions: Callable[[D], Union[T, Literal[False]]])
こちらは指定した複数のexpected_conditionsが全てTrueになればOK
def all_of(
*expected_conditions: Callable[[D], Union[T, Literal[False]]],
) -> Callable[[D], Union[list[T], Literal[False]]]:
def all_of_condition(driver: D):
results: list[T] = []
for expected_condition in expected_conditions:
try:
result = expected_condition(driver)
if not result:
return False
results.append(result)
except WebDriverException:
return False
return results
return all_of_condition
none_of(*expected_conditions: Callable[[D], Any])
こちらは指定した複数のexpected_conditionsが一つもTrueにならなければOK
def none_of(*expected_conditions: Callable[[D], Any]) -> Callable[[D], bool]:
def none_of_condition(driver: D):
for expected_condition in expected_conditions:
try:
result = expected_condition(driver)
if result:
return False
except WebDriverException:
pass
return True
return none_of_condition
あとがき
expected_conditionsは調べてみると知らないだけで多くの関数がありました
presence_of_element_locatedやvisibility_of_element_locatedは有名ですがtext_to_be_present_in_elementやelement_to_be_clickableも有用そうで今後活用してみたい!
引数はTuple型のlocatorとWebElement型のelement、どちらでも対応できるパターンがありますがどうせなら全部どちらにも対応できるパターンにしてほしい・・・
コメント