"BOKU"のITな日常

還暦越えの文系システムエンジニアの”BOKU”は新しいことが大好きです。

Webアプリケーションで自動的に値を参照、スクロール他Tips/Selenium&python

Seleniumpythonの組合せで、Webアプリケーションを自動化して使う「個人的なTips」3回続き物の3回目です。

今回は値の参照・スクロールなど前2回の範囲以外のその他をまとめます。

f:id:arakan_no_boku:20190914133925j:plain

 

記事の前提と留意点

 

内容は「自分がよく使う」ケースに偏っています。

ひとりでも、似たような使い方をする誰かの、参考になれば・・というスタンスです。

長くなるので、以下の3回にわけた、3回目が今回です。

今回の主な内容は以下です。

クラスに各部品を定義するイメージで、ソースサンプルは書いています。

使用例および、各機能をまとめたクラスのソース全体は末尾の方にまとめてます。

さて。

まずは「スクロール」から。

 

画面を少しずつスクロール&最下部まで一気にスクロール

 

Seleniumで入力を自動化して楽できる画面というのは、入力項目が多い画面です。

必然的に縦に長くなり、下の方の入力項目やボタンが隠れている場合が多いです。

隠れている項目を、Seleniumのsend_keys()とか、click()とかで操作しようとするとエラーになって失敗します。

人間の入力に近い感じになっているわけです。

そこで。

入力しながら、隠れている項目を見えるところにもってくるスクロールが、とても重要な働きをするわけです。

そんな時に使う部品です。

    def scroll_to(self, pitch):
        self.scroll_height = self.scroll_height + pitch
        self.driver.execute_script("window.scrollTo(0," + str(self.scroll_height) + ")")

これは指定した「pitch」分縦にスクロールさせるものです。

自分の環境だと、こんな感じで項目2行ごとに「60」スクロールさせてます。

auto.input_text('name_1', '漢字のなにか')
auto.input_text('name_2', '99999999')
auto.scroll_to(60)
auto.input_text_kana('name_3', 'カタカナデス')
auto.input_text_kana('name_4', 'コレモカタカナデス')
auto.scroll_to(60)
auto.select_by_value('name_5', '1')
auto.select_by_value('name_5', '2')

この60という数字に何かの法則性があるわけではなく、数字を変えながら、いろいろ試して、一番うまくいくのが、たまたま「60」だったというわけなので、他の環境で使う時は調整が必要なのであしからず。

もちろん。

たまには、画面表示で一気に一番下までスクロールして、下に配置されているボタンを押したいという場合もあるので、そういうときには、こっちを使ってます。

    def scroll_to_bottom(self):
        self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")

今のところ、この2種類でなんとかなってます。  

 

各エレメントのValue値の取得

 

値の参照と言えば、基本は「Value」です。

エレメントを取得して、そのvalue値を読みます。

 ただ。

Seleniumの機能だと画面から隠れている項目を選択しようとするとエラーになります。

入力の場合は、上記のようにスクロールを使って表示させながらやっていくわけですが、参照の場合だと、入力し終わって一番下まで来てるときに、最初の項目の値を参照したい・・とかは普通にあります。

その一番上の項目が実はスクロールによって隠れてしまっている・・となると、ちょっと面倒なわけです。

なので、基本はJavaScriptでやってます。

    def get_value_by_name(self, name):
        return self.driver.execute_script("return document.getElementsByName('" + name + "')[0].value")

nameをセレクタに、valueを読みだしてます。

戻り値は、pythonの変数に受け取ることがでいます。

 

チェックボックスラジオボタンの選択状態の取得

 

チェックボックスラジオボタンは上記のようにはいきません。

チェックしてるかしてないかは、checkedで読み取ります。

単一のチェックボックスの場合と、複数のチェックボックスがグループ化していて、その何番目のチェックボックスかをindexで指定する場合の2パターンです。

    def get_checked_by_name(self, name):
        return self.driver.execute_script("return document.getElementsByName('" + name + "')[0].checked")

    def get_checked_by_names(self, name, index):
        return self.driver.execute_script("return document.getElementsByName('" + name + "')[" + str(index) + "].checked")

TrueかFalseが返ってきます。 

 これもPython側の変数にうけとることができます。

 

Selectで選択中のValueの取得

 

最後にSelectです。

これは「今、選択されている値」を取得しないと意味がないので、ちょっと違うアプローチになります。

    def get_selected_value_by_name(self, name):
        selected_index = self.driver.execute_script("return document.getElementsByName('" + name + "')[0].selectedIndex")
        return self.driver.execute_script("return document.getElementsByName('" + name + "').item(0).options[" + str(selected_index) + "].value")

選択されているindex(selectedindex)を受取、それをキーにしてValueを読むという2段階操作が必要になります。

これも、Python側で値を受け取れます。

自分は今のところ、機能テストでバリバリ・・という使い方はしていないので、この程度でなんとかなってます。 

 

ブラウザの強制終了

 

処理終了と同時に、ブラウザを閉じたい場合とかに使います。

例えば、複数の操作やテストをバッチにして繰り返してやるときとかは、ブラウザが閉じていないとエラーになったりするので、これを最後にいれて、毎回閉じるようにして続ける必要があります。

    def quit(self):
        self.driver.quit()

でも、自分は滅多に使いません。

なぜかというと、複数のテストケースを自動化するようなことには使ってなくて、どちらかというと、面倒な入力だけ自動化して、入力し終わった状態から操作を引き続き行う・・みたいな入力補助的な使い方をすることが多いからです。

 

 使い方のイメージ

 

Seleniumをダイレクトに書くとゴチャゴチャして、読みづらくなります。

なので、MyRpaToolsというクラスに部品をまとめて定義して、操作の流れにあわせて、1行ずつペタペタ部品を貼り付けていくような使い方を想定しています。

使い方のイメージはこうです。

import my_rpa_tools as my
from selenium import webdriver
import time


# Edge WebDriver initial setting
driver = webdriver.Edge(
    executable_path='C:\\windows\\system32\\MicrosoftWebDriver.exe')
driver.get('https://coopweb.itecs.local/pro_staff/ps_login.php')
auto = my.MyRpaTools(driver)
# login
auto.input_text('login_id', 'user0001')

クラスは「my_rpa_tools.py」という名前のファイルに定義している前提です。

上記例だと、Edgeドライバーを、「driver」という名前のオブジェクトにセットして、それを引数に「MyRpaTools」クラスインスタンスを生成してます。

上記ではその名前を「auto」にしていて、あとは、「auto.input_text」のように、クラスで定義したメソッドを並べていく感じです。


ツールクラスのソース全体です

ソース全文です。

my_rpa_tools.py

from selenium.webdriver.support.ui import Select
from selenium.webdriver.support.ui import WebDriverWait as wait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC


class MyRpaTools():
    def __init__(self, web_driver):
        self.driver = web_driver
        self.scroll_height = 0

    def input_text(self, name, value):
        element = self.driver.find_element_by_xpath(
            "//input[@name='" + name + "'][@type='text']")
        element.send_keys(value)

    def input_number(self, name, value):
        element = self.driver.find_element_by_xpath(
            "//input[@name='" + name + "'][@type='number']")
        element.send_keys(value)

    def input_by_name(self, name, value):
        self.driver.execute_script(
            "document.getElementsByName('" + name + "')[0].value = '" + value + "';")

    def input_by_names(self, name, value, index):
        self.driver.execute_script(
            "document.getElementsByName('" + name + "')[" + index + "].value = '" + value + "';")

    def input_text_kana(self, name, value):
        self.input_by_name(name, value)

    def input_password(self, name, value):
        element = self.driver.find_element_by_xpath(
            "//input[@name='" + name + "'][@type='password']")
        element.send_keys(value)

    def input_textarea(self, name, value):
        element = self.driver.find_element_by_xpath(
            "//textarea[@name='" + name + "']")
        element.send_keys(value)

    def input_datepicker_current_month(self, name, select_day):
        self.driver.find_element_by_name(name).click()
        wait(self.driver, 10).until(EC.visibility_of_element_located(
            (By.XPATH, "//td[@data-handler='selectDay']/a[text()='" + select_day + "']"))).click()

    def input_direct_by_id(self, id, value):
        self.driver.execute_script(
            "$('#" + id + "').val('" + value + "');")

    # 2004年9月1日(水) のフォーマットで入力すること
    def input_datepicker_dairect(self, id, value):
        self.driver.execute_script(
            "$('#" + id + "').datepicker().datepicker('setDate','" + value + "');")
     
    def select_by_value(self, name, value):
        element = self.driver.find_element_by_name(name)
        element_select = Select(element)
        element_select.select_by_value(value)

    def click_submit(self, name):
        element = self.driver.find_element_by_xpath(
            "//input[@name='" + name + "'][@type='submit']")
        element.click()

    def click_button(self, name):
        self.driver.find_element_by_xpath(
            "//input[@type='button'][@name='" + name + "']").click()
    
    def click_checkbox(self, name):
        self.driver.find_element_by_xpath(
            "//input[@type='checkbox'][@name='" + name + "']").click()

    def click_by_name(self, name):
        self.driver.execute_script(
            "document.getElementsByName('" + name + "')[0].click();")

    def click_by_names(self, name, index):
        self.driver.execute_script(
            "document.getElementsByName('" + name + "')[" + index + "].click();")

    def click_link_titled(self, name):
        element = self.driver.find_element_by_xpath("//a[@title='" + name + "']")
        element.click()

    def click_link_by_text(self, text):
        element = self.driver.find_element_by_link_text(text)
        element.click()

    def click_link_by_index(self, index):
        self.driver.execute_script("document.getElementsByTagName('a')[" + str(index) + "].click();")
        
    def execute_script(self, scr):
        self.driver.execute_script(scr)

    def scroll_to(self, pitch):
        self.scroll_height = self.scroll_height + pitch
        self.driver.execute_script("window.scrollTo(0," + str(self.scroll_height) + ")")

    def scroll_to_bottom(self):
        self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")

    def get_value_by_name(self, name):
        return self.driver.execute_script("return document.getElementsByName('" + name + "')[0].value")

    def get_checked_by_name(self, name):
        return self.driver.execute_script("return document.getElementsByName('" + name + "')[0].checked")

    def get_checked_by_names(self, name, index):
        return self.driver.execute_script("return document.getElementsByName('" + name + "')[" + str(index) + "].checked")

    def get_selected_value_by_name(self, name):
        selected_index = self.driver.execute_script("return document.getElementsByName('" + name + "')[0].selectedIndex")
        return self.driver.execute_script("return document.getElementsByName('" + name + "').item(0).options[" + str(selected_index) + "].value")

    def quit(self):
        self.driver.quit()

内部で、self.driverに引数のweb_driverをうけて各メソッドの中で使っていきます。

セレクタは特に理由ない限り、「name」を使っています。

ただ、内部でjQueryを使っているものは区別のために「id」を使ってます。

nameを使っている理由は、自分が使っている環境ではそれが適切だからです。

正直。

Webアプリケーションの作り方のバリエーションは多く、適切なセレクタがどれか・・というのは、自動化対象になるアプリケーションによって違います。

もし、nameが適切でない環境で使うときは、適宜読み替えてください。

 

最後に

 

個人的なTipsを3回にわけて書きました。

Seleniumは操作対象のWebコンテンツによって、違うアプローチが必要になります。

なので、今はいけてても、対象が変わるとまた部品の追加が必要になるでしょうから、適当なタイミングで追加・更新していくつもりです。

なお。

seleniumのインストールやWEBドライバの取得等については、こちらの記事にまとめてます。

arakan-pgm-ai.hatenablog.com

今回はこんなところで。

ではでは。