【Python】Seleniumで気を付けたいことまとめ

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

私は本業でも副業でもお世話になてるSeleniumさん

普通に使ってると歯痒いところ何気に多く、ラッパーを作ったり微調整してあげてるんですが実際に遭遇した事象と対処法をメモしていくのでSelenium好きの方はちょくちょく覗きに来てもらうと情報共有できると思います

Seleniumはスクレイピングで使うことも多くてスピードを求める人も多いですが根源的にはWEBアプリのテスト用であり、スクレイピングするにしてもWEBサイトに迷惑をかけないことが必須なので”スピード <<< 正確な動作”を前提にプログラムを作成します

利用しているモジュール群

このページでは使っていないものもありますが、私がSeleniumを使う時の基本セットてきなやつです

import dataclasses
import logging
import logging.config
import logging.handlers

import os
import datetime
from time import sleep

import bs4
from bs4 import BeautifulSoup
from fake_useragent import UserAgent
from selenium import webdriver
from selenium.common.exceptions import TimeoutException, NoSuchElementException
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common import utils
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.select import Select
from selenium.webdriver.support.ui import WebDriverWait

# ---今回はoptionsとserviceは考慮しない
driver = webdriver.Chrome()

遭遇した事象と対処方法

ここから実際に遭遇した事象と個人的対処方法を書き残していきます

自分で使うときはラッパークラスを作ってるので厳密には違うコードになってますが考え方は同じなんで参考にどうぞ

Getでページ遷移完了する前に次の処理が走ってしまう

何を言ってるのかわからねーと思うがおれも何をされたのかわからなかった…頭がどうにかなりそうだった…

例えば

  • Getでログインページに遷移
  • ログインページでID/PWを入力してログインボタンをクリック
  • ログイン後のページで処理

なんてよくある処理でもサイトによっては稀に謎エラーが発生します

何度デバックでステップインしても再現せずに苦しみましたが次の処理をすることでその後再現していないので対応完了ってことにしました

driver.get(url=url)
try:
    # JavaScriptを使ってページ全体が読み込まれるまで待機
    WebDriverWait(driver, 30).until(lambda d: d.execute_script("return document.readyState") == "complete") # ---①
    sleep(0.1) # ---②
except Exception:
    logger.error('Page Transition Error!!')

driver.find_element(By.CSS_SELECTOR, '#email').send_keys(id)
driver.find_element(By.CSS_SELECTOR, '#password').send_keys(pw)
driver.find_element(By.CSS_SELECTOR, 'button.login_bunt').click()
try:
    # JavaScriptを使ってページ全体が読み込まれるまで待機
    WebDriverWait(driver, 30).until(lambda d: d.execute_script("return document.readyState") == "complete") # ---①
    sleep(0.1) # ---②
except Exception:
    logger.error('Page Transition Error!!')

'''

~~~ ログイン後の処理 ~~~

'''

やることは2つ

  • htmlファイルとサブリソースの読み込みがどちらも完了まで待機する
  • 一瞬(0.1秒)でいいので無条件で待機処理を入れる

jsの処理が終わってないのに次の処理をしようとしてエラーになってる気がするので適度に待機処理入れることで解消したんじゃないかと推察してます

文字入力ができない、入力後の処理が動かない

どのサイトでも発生するってわけでもないんですが、画面上に入力する要素が見えてないと文字入力ができない時がありまして・・

他にも文字入力処理は気を遣わないと思い通りの処理にならないことが多々あるのでちゃんとエラーハンドリングしようとするならこれくらい必要

  • 画面上に入力対象要素があるか確認する
  • タグが文字入力するinputかtextareaか確認する
  • readonlyやdisable属性が付いてないか確認する
  • ActionChainsで入力要素まで画面遷移する
  • send_keyの仕様が追記処理なので念のため文字入力の前にクリアする
  • 入力が終わったらjs対策でタブキーを押す処理をする
css_selector = '#target_id'
# ---入力する要素が見える状態か確認する
try:
    _ = WebDriverWait(driver, 3).until(EC.visibility_of(driver.find_element(By.CSS_SELECTOR, css_selector)))
except NoSuchElementException:
    print('No Such Element')
except TimeoutException:
    print('Timeout Error')
except Exception:
    print('Unknown Error', exc_info=True)

# ---WebDriverWait以外でもチェックする
element = driver.find_element(By.CSS_SELECTOR, css_selector)
# ---タグがinputかtextareaじゃないか確認する
if element.tag_name != 'input' and element.tag_name != 'textarea':
    print('This element tagName is not input and textarea')
    return False
# ---タグが入力可能か確認する
if element.is_enabled() is False:
    print('This element is disable')
    return False
# ---readonly属性が付いてないか確認する
if not element.get_attribute('readonly') is None:
    print('This element is readonly')
    return False

# ---念のため画面をelementへ動かしておく
ActionChains(driver).move_to_element(element).perform()
# ---send_keysだと追記になってしまうので一旦クリアしておく
element.clear()
# ---念のため待機処理を入れる
sleep(0.1)
# ---ようやく実際に文字入力する
element.send_keys('hoge')
# ---入力後に発火するjsの可能性があるのでタブ移動する
element.send_keys(Keys.TAB)
# ---念のため待機処理を入れる
sleep(0.1)

Frameの遷移が成功しない

まず”driver.switch_to.frame“より”expected_conditions“の”frame_to_be_available_and_switch_to_it“の方がエラーハンドリングしてくれるのでオススメ

中身を見ればやってることは結局”driver.switch_to.frame“だったりする

def frame_to_be_available_and_switch_to_it(locator: Union[Tuple[str, str], str]) -> Callable[[WebDriver], bool]:
    """An expectation for checking whether the given frame is available to
    switch to.

    If the frame is available it switches the given driver to the
    specified frame.
    """

    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

本題ですが”失敗する”じゃなくて”成功しない”と表現したのは”frame_to_be_available_and_switch_to_it“ではTrueで返ってくるのになぜか遷移できていないことに遭遇したから

発生確率も地味に高いのでちょっと面倒だけどこれくらいやらないと安心できないです

Frameの遷移が必要なWEBサイトは少ないのでこれくらいは我慢ってことですかね

css_selector = '#target_id'
try:
    # ---初期Frameに戻す
    driver.switch_to.default_content()
    # ---5回までリトライするようにする
    for _ in range(5):
        # ---5秒でFrameの遷移が完了するかチェック
        if WebDriverWait(driver, 5).until(EC.frame_to_be_available_and_switch_to_it((By.CSS_SELECTOR, css_selector))) is False:
            sleep(1)
        else:
            # 選択したフレームが見えなくなっていることを確認して終わりにする
            if len(driver.find_elements(By.CSS_SELECTOR, css_selector)) == 0:
                return True
            else:
                sleep(1)
                continue
    else:
        raise NoSuchFrameException(msg=f'"{css_selector}" に遷移できませんでした')
except Exception:
    # ---エラーになったら画面ショットを残しておく
    driver.save_screenshot(R'errorshot_{}.png'.format(datetime.datetime.now().strftime("%Y%m%d%H%M%S")))

(Frameと言えばこんな感じのソースで動いているサイトもまだまだ現役のようです)

あとがき

何か問題に遭遇して解決した時は追記していくので気が向いた時にでも見に来てください!

コメント

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