新しいことにはウェルカム

技術 | 電子工作 | ガジェット | ゲーム のメモ書き

Seleniumの使い方メモ

最近Seleniumを試していたので、使い方を忘れてもまた思い出せるよう、Seleniumの使い方をメモしておこうと思います。

インストール

Selenium

pip install selenium

ブラウザとそのブラウザを駆動するWebDriverは別途インストールする必要がある。

WebDriver

  • WebDriverはブラウザのバージョンに合ったものをインストールする必要がある。
  • 手動で行うと手間なのでwebdriver_managerを使って自動インストールする。
pip install webdriver-manager

webdriver_managerの使い方

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install())

サンプル

例)Googleで「test」と検索し、結果の2ページ目のソースを表示する。

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
driver.implicitly_wait(30)

driver.get('https://www.google.com/')
driver.find_element(By.CSS_SELECTOR, 'input[name="q"]').send_keys("test\n")
driver.find_element(By.CSS_SELECTOR, '#pnnext').click()
print(driver.page_source)

driver.quit()

要素(選択)

ブラウザ操作の流れは、対象となるdomを取得し、そのdomに対して操作の繰り返しになる。

自動待機

  • dom取得時にそのdomがまだ生成されれていないことがある。
  • 対象domが生成されるまで自動で待つよう設定する。
  • 設定時間まで待ってdomが見つからない場合はエラーになる。
# selector auto-wait 30sec
driver.implicitly_wait(30)

CSSセレクター

from selenium.webdriver.common.by import By
driver.find_element(By.CSS_SELECTOR, 'button').click()

セレクター拡張

セレクターから更にセレクターを呼ぶと、子供のdomを検索する。

# same as 'div input'
driver.find_element(By.CSS_SELECTOR, 'div')\
    .find_element(By.CSS_SELECTOR, 'input').click()

複数要素

セレクターが複数要素にマッチする場合がある。

  • driver.find_element()は最初に見つかった要素を返す
  • driver.find_elements()は見つかった要素全てのlistを返す
elems = driver.find_elements(By.CSS_SELECTOR, 'a')
for elem in elems:
    print(elem.get_attribute('href'))

要素(操作・属性)

操作

  • webelement.click()
    • クリック
  • webelement.send_keys(<string>)
    • 文字入力
    • inputの場合valueをセットしてくれる。
    • inputtypefileの場合、ファイルパスをセットする。
  • webelement.clear()
    • 入力文字列クリア

ラジオボタン・チェックボックス・セレクトのオプションの選択はwebelement.click()で行う。

スクロール

ウィンドウ内の1番下の要素を取得してwebelement.location_once_scrolled_into_viewを参照すると、その要素が見えるまでスクロールしてくれる。

  • webelement.location_once_scrolled_into_viewはプロパティなので参照するだけでよい
  • 内部的には要素に対してscrollIntoView()を呼び出している
driver.find_element(By.CSS_SELECTOR, '#bottom-elem').location_once_scrolled_into_view

属性

  • webelement.text
  • webelement.get_attribute(<attribute_name>)
    • 属性値取得
    • 属性例
      • name
      • id
      • value
  • webelement.is_selected()
    • ラジオボタン・チェックボックス・セレクトのオプションの選択状態を取得

例(オプション)

# inverse option
options = driver.find_elements(By.CSS_SELECTOR, 'select option')
for option in options:
    if option.is_selected():
        option.click()

ナビゲーション

遷移

  • driver.get(<url>)
  • driver.refresh()
  • driver.quit()
    • ブラウザを終了する時は必ず呼び出す

ページロード完了は、driver.find_element()を使って、ページの特定要素が見つかったかで検知する。

属性

  • driver.title
  • driver.current_url
  • driver.page_source

フレーム

  • driverは1つのフレームに紐づく
  • html内にあるiframeの中の要素を操作するには、driver.switch_to.frame(<frame_elem>)で、driverが指すフレームを切り替える
  • 元のフレームに戻すにはdriver.switch_to.default_content()を使う
frame = driver.find_element(By.CSS_SELECTOR, "iframe")
driver.switch_to.frame(frame)
driver.find_element(By.CSS_SELECTOR, "button").click()
driver.switch_to.default_content()

タブ(window)

<a target="_blank">で新しいタブ(window)が開く場合の扱い方。

  • タブが作成されるとwindowが生成される。つまり、tabwindowは同じ
  • driverの操作対象は1つのwindow
  • タブ一覧はdriver.window_handlesにある
  • 現在のwindow_handledriver.current_window_handleで取得する
  • driverの操作対象のwindowを切り替えるにはdriver.switch_to.windoe(<window_handle>)を使う

新しく開かれたタブ内の要素を操作し、その後元のタブに戻って再度要素を操作する。

  • タブが生成されwindow_handleの個数が増えるまで待つ
  • タブが生成される前後のwindow_handle一覧の差分から、新しいタブのwindow_handleを取り出す
from selenium.webdriver.support import expected_conditions as EC

handle_count = len(driver.window_handles)
before_handles = driver.window_handles
original_handle = driver.current_window_handle

# open new tab
driver.find_element(by=By.CSS_SELECTOR, value='#new_tab').click()

# wait for new window created
WebDriverWait(driver, 30).until(
    EC.number_of_windows_to_be(handle_count+1))

# find new window handle
after_handles = driver.window_handles
diff_handles = list(set(after_handles) ^ set(before_handles))

# switch new tab
driver.switch_to.window(diff_handles[0])
driver.find_element(By.CSS_SELECTOR, 'button').click()

# switch original tab
driver.switch_to.window(original_handle)
driver.find_element(By.CSS_SELECTOR, 'button').click()

accept-language

Seleniumの機能ではできない。ブラウザの起動オプションで設定する。

options = webdriver.ChromeOptions()
options.add_experimental_option('prefs', {'intl.accept_languages': 'ja'})
driver = webdriver.Chrome(
    ChromeDriverManager().install(),
    options=options,
)

ブラウザの状態保存

通常、ブラウザは毎回新しい状態で立ち上がる。

ブラウザの起動オプションで、ユーザーデータ保存先を指定することにより、ブラウザの状態を保存し再利用できるようになる。

多要素認証のページを開く場合などに用いる。

options = webdriver.ChromeOptions()
options.add_argument('--user-data-dir=<user_data_dir>')
driver = webdriver.Chrome(
    ChromeDriverManager().install(),
    options=options,
)

ファイルダウンロード

ファイルダウンロードは、Seleniumの機能だけではできず、ブラウザ毎に固有の実装が必要になる。

Seleniumで出来ないこと

  • ダウンロードしたファイルのパスの取得
  • ダウンロード完了の状態取得

手順

Google Chrome

  • ブラウザの起動オプションでダウンロード先のフォルダを指定する
  • ダウンロード先のファイルの更新をウォッチして、ダウンロード完了とダウンロードされたファイルのパスを取得する
    • ファイル更新検知にwatchdogを使用
    • ダウンロード時にテンポラリファイルが作られるので検知対象から除外する
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager

from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

import os
import re
import time


class ChromeDownloadHandler(FileSystemEventHandler):
    def __init__(self, observer):
        self.result = None
        self.observer = observer
        self.tmpfiles = \
            [re.compile('^\.com\.google\.Chrome\..+$'),
             re.compile('^.+\.crdownload$')]

    def on_closed(self, event):
        if event.is_directory == True:
            return

        basename = os.path.basename(event.src_path)
        for tmpfile in self.tmpfiles:
            if re.match(tmpfile, basename) != None:
                return

        self.result = event.src_path
        self.observer.stop()


# setup browser
download_dir = os.path.abspath('./download')
options = webdriver.ChromeOptions()
options.add_experimental_option('prefs', {
    'download.default_directory': download_dir,
})

driver = webdriver.Chrome(
    service=Service(ChromeDriverManager().install()),
    options=options,
)
driver.implicitly_wait(30)

# setup watchdog
observer = Observer()
handler = ChromeDownloadHandler(observer)
observer.schedule(
    handler,
    download_dir,
    recursive=False
)
observer.start()

# file download
driver.get('https://the-internet.herokuapp.com/download')
driver.find_element(By.CSS_SELECTOR, '#content a').click()

# wait for file download finish
try:
    while observer.is_alive():
        time.sleep(1)
except KeyboardInterrupt as e:
    observer.stop()
    observer.join()
    raise (e)

# file download finished
observer.join()

# print download file path
print(handler.result)

driver.quit()

ファイルアップロード

フォーム

  • <input type="file" />にアップロードするファイルの絶対パスをwebelement.send_key()で設定する
driver.find_element(By.CSS_SELECTOR, 'input[type="file"]').send_key(<full_file_path>)
driver.find_element(By.CSS_SELECTOR, 'input[type="submit"]').click()

Seleniumでできないもの

下記のタイプのファイルアップロードはSeleniumの機能だけでは再現できない。

  • クリックするとファイル選択ダイアログが出て、ファイル選択してアップロード
  • ファイルをドロップしてアップロード

これらの場合、ページ内に非表示のフォームが設置されている場合があるので、前述のフォームと同じ方法を試してみる。

JavaScript

基本

  • driver.execute_script()でブラウザ上でJavaScriptを実行できる
  • returnで戻り値を返すことができる
  • 戻り値は、スカラー、もしくはJSON、もしくはブラウザ上でのJavaScriptのオブジェクトへの参照
  • 戻り値がJSONの場合はdictになる
res = driver.execute_script("""
    return 'abc';
    """)

# abc
print(res)

res = driver.execute_script("""
    return { val1: 123, val2: 'abc' };
    """)

# {'val1': 123, 'val2': 'abc'}
print(res)

引数

  • JavaScript文の後に引数を置くと、JavaScriptに引数を渡すことができる
  • 値はJavaScript側のarguments配列に格納される
res = driver.execute_script("""
    return { val1: arguments[0], val2: arguments[1] };
    """, 123, 'abc')

# {'val1': 123, 'val2': 'abc'}
print(res)

JavaScriptオブジェクトの参照

  • 戻り値にJavaScriptオブジェクトを渡した時は、JavaScriptオブジェクトへの参照が戻る
  • JavaScriptオブジェクトへの参照をdriver.execute_script()に渡すと、JavaScript内で参照できる

html

<input name="test" value="before" />

code

# print->"before"
print(driver.find_element(By.CSS_SELECTOR, 'input[name="test"]').get_attribute('value'))

obj = driver.execute_script("""
    return document.querySelector('input[name="test"]');
    """)

# print->"<selenium.webdriver.remote.webelement.WebElement (session="...", element="...")>"
print(obj)

driver.execute_script("""
    arguments[0].value = 'after';
    """, obj)

# print->after
print(driver.find_element(By.CSS_SELECTOR, 'input[name="test"]').get_attribute('value'))

driver.find_element()

driver.find_element()で返されるのもJavaScriptオブジェクトへの参照なので、前述例と同様、driver.execute_script()の引数に渡して、JavaScript内で参照できる

# print->"before"
print(driver.find_element(By.CSS_SELECTOR, 'input[name="test"]').get_attribute('value'))

obj = driver.find_element(By.CSS_SELECTOR, 'input[name="test"]')

# print->"<selenium.webdriver.remote.webelement.WebElement (session="...", element="...")>"
print(obj)

driver.execute_script("""
    arguments[0].value = 'after';
    """, obj)

# print->after
print(driver.find_element(By.CSS_SELECTOR, 'input[name="test"]').get_attribute('value'))

ドキュメント

リファレンスを見る際の注意

基底クラスが記載されていない

[source]をクリックして、ソースを見て調べる。

例えば、selenium.webdriver.chrome.webdriverの基底クラスは、クラスの定義がclass WebDriver(ChromiumDriver):なので、ChromiumDriverとなるのが分かる。

ChromiumDriverはどこにあるかは、importを見るとfrom selenium.webdriver.chromium.webdriver import ChromiumDriverなので、selenium.webdriver.chromium.webdriverにあるのが分かる。

基底クラスの関数・変数が、派生クラスのリファレンスからは分からない

[source]で地道に基底クラスを見つけ、そのリファレンスを辿っていく。

感想など

Chromeのヘッドレスモードで動かすと挙動が異なるサイトがあるので、ヘッドレスモードの使用は避けた方がいいかも。

関連カテゴリー記事

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com

www.kwbtblog.com