【Python】Seleniumの待機処理はWebDriverWaitを使おう

本ページはプロモーションが含まれています

Seleniumでブラウザ操作をする際、「要素が表示されるまで待ちたい」「クリック可能になるまで待ちたい」などのタイミング調整は非常に重要です

単に time.sleep() を使うと、処理が無駄に遅くなったり、逆に待ちが足りずにエラーになることが頻繁にあります

今回はPython + Selenium における待機処理のベストプラクティスとして、WebDriverWaitexpected_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_locatedDOM上に存在しているか
visibility_of_element_located画面上に表示されているか
invisibility_of_element_located画面上で非表示になっているか
element_to_be_clickable画面上に表示されてかつ利用できる状態か
分類返値の型
DOMで判定する ※画面上に見えているかは加味しない
presence_of_element_locatedWebElement
presence_of_all_elements_locatedWebElement
画面上の表示・非表示で判定する
visibility_ofWebElement
visibility_of_element_locatedWebElement
visibility_of_all_elements_locatedList
visibility_of_any_elements_locatedList
invisibility_of_elementbool
invisibility_of_element_locatedbool
elementの状態で判定する
element_attribute_to_includebool
element_located_selection_state_to_bebool
element_located_to_be_selectedbool
element_selection_state_to_bebool
element_to_be_clickableWebElement
element_to_be_selectedbool
文字や要素の値で判定する
text_to_be_present_in_elementbool
text_to_be_present_in_element_attributebool
text_to_be_present_in_element_valuebool
ページタイトルで判定する
title_containsbool
title_isbool
ページURLで判定する
url_changesbool
url_containsbool
url_matchesbool
url_to_bebool
ブラウザのウィンドウで判定する
number_of_windows_to_bebool
new_window_is_openedbool
frame要素で判定する
frame_to_be_available_and_switch_to_itbool
staleness_ofbool
アラートポップアップで判定する
alert_is_presentAlert
複合条件にする
all_ofList
any_ofList
none_ofbool

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上になければTrueDOM上にあるけど非表示なら対象の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_locatedvisibility_of_element_locatedは有名ですがtext_to_be_present_in_elementelement_to_be_clickableも有用そうで今後活用してみたい!

引数はTuple型のlocatorとWebElement型のelement、どちらでも対応できるパターンがありますがどうせなら全部どちらにも対応できるパターンにしてほしい・・・

コメント

タイトルとURLをコピーしました