「Yahoo!乗換案内」を自動操作
「Yahoo!乗換案内」の操作をSeleniumで自動化します。Excelから経路情報を読み込んでブラウザに入力し、結果を取得する操作を自動で行います。画面項目の取得にXPathを利用しますが、XPath取得ツールとXPath検証ツールを使用することで、Pythonスクリプトの作成工数を削減します。
作業手順
- 入出力項目の洗い出し
操作対象の画面項目を確定します。 - 画面項目のXPath取得
「XPath取得ツール」を使用し、より簡潔で変更に強いXPathを取得します。
ブラウザの機能で取得する場合と異なり、毎回コピペする必要がありません。 - XPathのチェック
取得したXPathで実際に「Yahoo!乗換案内」の操作ができるかをチェックします。
「XPath検証ツール」を使用すると、チェックとPythonコードの生成が同時に行えます。 - Pythonスクリプトの作成
XPath検証ツールが生成したPythonコードをベースに、Seleniumのスクリプトを作成します。 - Seleniumスクリプトの実行
- エラー対処、スクリプトの改良
いわゆるデバッグ作業です。 - 評価、考察
SeleniumがRPAツールとして使用できるか、「Yahoo!乗換案内」の交通費調査がRPAで代替できるかの2点を評価してみたいと思います。
入出力項目の洗い出し
「Yahoo!乗換案内」から操作対象の画面項目をピックアップします。
入力画面
経路 | 出発、到着、経由1、+ボタン、経由2 |
日時 | 指定なし |
運賃 | 種別(現金優先)、座席(指定席) |
条件 | 表示順序(料金が安い順) |
手段 | 空路(なし)、新幹線(あり)、有料特急(あり)、高速バス(なし)、路線/連絡バス(なし)、フェリー(なし)※使用しない経路も、オンからオフへの操作が発生する |
検索 | 検索ボタン (クリック) |
出力画面
ルート1 | 所要時間、金額 |
ルート2 | 所要時間、金額 |
ルート3 | 所要時間、金額 |
Excelシート
XPathの取得
XPath取得ツールを用いて、画面項目のXPathを取得します。
XPath取得ツール起動
起動されたブラウザで入力画面を開きます。「XPath取得」ボタンを押して、ブラウザの画面項目を右クリックすると、XPathが取得できます。
取得したXPathは履歴ファイルに搭載されています。ブラウザの機能でXPathを取得する場合は、毎回コピーしたXPathをExcelに貼り付ける必要がありますが、XPath取得ツールを用いると、コピペせず作業後にまとめて出力できます。
「解析」ボタンを押すと、取得したXPathの構造を分析し、より使いやすいXPathを提案してくれます。
なお、メモ欄に項目名を残しておくと、後で仕分ける時に役立ちます。
出力画面も同様にXPathを取得していきます。
画面のXPathをすべて取得できたら、履歴ファイルを出力します。(通常はXPath解析結果だけでOK)
XPath取得結果
XPath解析結果(通常はこちらだけでOK)
出力ファイル(タブ区切り)
Excel貼り付け(XPath解析結果)
XPath解析結果ファイルをExcelに貼り付けます。
ファイル形式「タブ区切り」
列「メモ」に数字が入っている場合は、文字列にしておきましょう。
解析結果がExcelに展開されました。取得時に登録したメモを頼りに、画面項目に対応するXPathを判定します。
XPath解析ツール出力結果(必要箇所を抜粋して画面項目と紐づけ)
一番右の列が「フルXPath」、候補1・候補2がXPath解析ツールで解析した内容です。フルXPathに比べて、簡潔で、画面の変更にも強い内容に編集されています。
XPathの確定
XPath解析ツールの結果をもとに「Yahoo!乗換案内」の XPathを確定します。
出力画面で一部のXPathが、実際の時間や金額で編集されています。入力内容によって変わるので、XPathとして不適切です。候補2を使用します。
なお、XPath解析時にテキスト内容を解析対象から外すことで、同様の結果を得ることができます。
完成したXPath
入力画面
項目 | XPath | 操作 | |
---|---|---|---|
経路 | 出発駅 | //input[@id="sfrom"] | テキスト入力 |
到着駅 | //input[@id="sto"] | テキスト入力 | |
経由1 | //input[@id="svia1″] | テキスト入力 | |
+ボタン | //li[@id="via01″]/dl/dd/span[2] | クリック | |
経由2 | //input[@id="svia2″] | テキスト入力 | |
日時 | 指定なし | //input[@id="tsAvr"] | クリック |
運賃 | 種別(現金優先) | //select[@name="ticket"] | 選択(index1) |
座席(指定席) | //select[@name="expkind"] | 選択(index1) | |
条件 | 表示順序(料金が安い順) | //select[@name="s"] | 選択(index2) |
手段 | 空路(なし) | //input[@id="air"] | チェックOFF |
新幹線(あり) | //input[@id="sexp"] | チェックON | |
有料特急(あり) | //input[@id="exp"] | チェックON | |
高速バス(なし) | //input[@id="hbus"] | チェックOFF | |
路線/連絡バス(なし) | //input[@id="bus"] | チェックOFF | |
フェリー(なし) | //input[@id="fer"] | チェックOFF | |
実行 | 検索ボタン | //input[@id="searchModuleSubmit"] | クリック |
出力画面
項目 | XPath | 操作 | |
---|---|---|---|
ルート1 | 所要時間1 | //ul[@id="rsltlst"]/li[1]/dl/dd/ul/li[1] | テキスト取得 |
金額1 | //ul[@id="rsltlst"]/li[1]/dl/dd/ul/li[2] | テキスト取得 | |
ルート2 | 所要時間2 | //ul[@id="rsltlst"]/li[2]/dl/dd/ul/li[1] | テキスト取得 |
金額2 | //ul[@id="rsltlst"]/li[2]/dl/dd/ul/li[2] | テキスト取得 | |
ルート3 | 所要時間3 | //ul[@id="rsltlst"]/li[3]/dl/dd/ul/li[1] | テキスト取得 |
金額3 | //ul[@id="rsltlst"]/li[3]/dl/dd/ul/li[2] | テキスト取得 |
XPathのチェック
確定したXPathで、マウスクリックやテキスト入力などの操作ができるかをXPath検証ツールを用いてチェックします。業務用画面と異なり、コンシュマー向けのサイトはデザインを高めるため、スタイルシートやJavaScriptを多用しているため、素直に画面上のコントロールで操作できないケースがあります。そのため確定したXPathで、実際にブラウザの操作ができるかを確認しておきます。
また、XPath検証ツールを使用することで、画面項目を操作するSeleniumのPythonコードを生成します。
XPath検証ツール起動
- ロケータ欄に、画面項目のXPathを入力して「XPath」ボタンを押す。
XPathが正しければ、Seleniumが項目を認識し、項目情報を表示します。 - 実際に操作ができるかを、操作ボタンで確認
(マウスクリック、キー入力、項目選択)
画像では、出発欄(テキスト項目)に対し、「send_key」ボタンを押しています。すると実際に文字(hoge)が入力され、検証ツールの生成コード欄に、項目を操作するPythonコードが編集されます。
出力画面も同様にして、XPathを検証します。
取得時と同様に、メモ欄に項目名を残しておくと、履歴ファイルから抽出する時に役立ちます。
検証の結果、すべてOKでした。続いて履歴ファイルを出力します。
出力ファイル(タブ区切り)
Excel貼り付け(XPath検証結果)
解析結果の時と同様、テキストファイルをExcelに取り込みます。
XPath検証ツール出力結果(必要箇所を抜粋)
Seleniumで画面項目の操作に必要なPythonコードが出力されていますので、コード部分を抜き出します。
XPath検証ツールの出力結果(画面項目の操作コード)
入力画面
項目 | 操作 | 生成コード | |
---|---|---|---|
経路 | 出発駅 | text入力 | driver.find_element_by_xpath |
到着駅 | text入力 | driver.find_element_by_xpath |
|
経由1 | text入力 | driver.find_element_by_xpath |
|
+ボタン | クリック | driver.find_element_by_xpath |
|
経由2 | text入力 | driver.find_element_by_xpath |
|
日時 | 指定なし | クリック | driver.find_element_by_xpath |
運賃 | 種別(現金優先) | 選択 (index1) |
select = Select(driver.find_element_by_xpath |
座席(指定席) | 選択 (index1) |
select = Select(driver.find_element_by_xpath |
|
条件 | 表示順(安い順) | 選択 (index2) |
select = Select(driver.find_element_by_xpath |
手段 | 空路(なし) | チェックOFF | driver.find_element_by_xpath |
新幹線(あり) | チェックON | driver.find_element_by_xpath |
|
有料特急(あり) | チェックON | driver.find_element_by_xpath |
|
高速バス(なし) | チェックOFF | driver.find_element_by_xpath |
|
路線/連絡バス(なし) | チェックOFF | driver.find_element_by_xpath |
|
フェリー(なし) | チェックOFF | driver.find_element_by_xpath |
|
実行 | 検索ボタン | クリック | driver.find_element_by_xpath |
出力画面
項目 | 操作 | 生成コード | |
---|---|---|---|
ルート1 | 時間1 | text取得 | driver.find_element_by_xpath |
金額1 | text取得 | driver.find_element_by_xpath |
|
ルート2 | 時間2 | text取得 | driver.find_element_by_xpath |
金額2 | text取得 | driver.find_element_by_xpath |
|
ルート3 | 時間3 | text取得 | driver.find_element_by_xpath |
金額3 | text取得 | driver.find_element_by_xpath |
XPath取得ツール・XPath検証ツールを用いた結果、この工程までを定型作業として行うことができました。
次工程からはいよいよSeleniumスクリプトの作成ですが、あらかじめ画面項目のXPathの正当性が確保できているので、スクリプト作成の工数削減効果が見込めます。
また、スクリプトがエラーとなってデバッグ作業を行う場合、XPathに問題があるかだけを、他の不具合要因と切り離してチェックすることができます。
Seleniumスクリプトの作成
いよいよスクリプトの作成です。まずは修正が必要な部分を把握します。
入力画面
項目 | 操作 | 生成コード | |
---|---|---|---|
経路 | 出発駅 | text入力 | driver.find_element_by_xpath |
到着駅 | text入力 | driver.find_element_by_xpath |
|
経由1 | text入力 | driver.find_element_by_xpath |
|
+ボタン | クリック | driver.find_element_by_xpath |
|
経由2 | text入力 | driver.find_element_by_xpath |
|
日時 | 指定なし | クリック | driver.find_element_by_xpath |
運賃 | 種別(現金優先) | 選択 (index1) |
select = Select(driver.find_element_by_xpath |
座席(指定席) | 選択 (index1) |
select = Select(driver.find_element_by_xpath |
|
条件 | 表示順(安い順) | 選択 (index2) |
select = Select(driver.find_element_by_xpath |
手段 | 空路(なし) | チェックOFF | driver.find_element_by_xpath |
新幹線(あり) | チェックON | driver.find_element_by_xpath |
|
有料特急(あり) | チェックON | driver.find_element_by_xpath |
|
高速バス(なし) | チェックOFF | driver.find_element_by_xpath |
|
路線/連絡バス(なし) | チェックOFF | driver.find_element_by_xpath |
|
フェリー(なし) | チェックOFF | driver.find_element_by_xpath |
|
実行 | 検索ボタン | クリック | driver.find_element_by_xpath |
- 経路:hogeを変数にします。
- 条件:表示順の入力値を1から2に変えます。
- 手段:チェックボックスの元の値を取得し、異なる場合のみチェックします。
出力画面
項目 | 操作 | 生成コード |
---|---|---|
時間1 | text取得 | driver.find_element_by_xpath |
金額1 | text取得 | driver.find_element_by_xpath |
時間2 | text取得 | driver.find_element_by_xpath |
金額2 | text取得 | driver.find_element_by_xpath |
時間3 | text取得 | driver.find_element_by_xpath |
金額3 | text取得 | driver.find_element_by_xpath |
- 画面項目の要素からテキスト(innerHTML)を取得します。
修正コード
入力画面
項目 | 修正コード | |
---|---|---|
経路 | 出発駅 | driver.find_element_by_xpath('//input[@id="sfrom"]').send_keys(start_st) |
到着駅 | driver.find_element_by_xpath('//input[@id="sto"]').send_keys(end_st) |
|
経由1 | driver.find_element_by_xpath('//input[@id="svia1"]').send_keys(point1) |
|
+ボタン | driver.find_element_by_xpath('//li[@id="via01"]/dl/dd/span[2]').click() |
|
経由2 | driver.find_element_by_xpath('//input[@id="svia2"]').send_keys(point2) |
|
日時 | 指定なし | driver.find_element_by_xpath('//input[@id="tsAvr"]').click() |
運賃 | 種別(現金優先) | select=Select(driver.find_element_by_xpath('//select[@name="ticket"]')) select.select_by_index(1) |
座席(指定席) | select=Select(driver.find_element_by_xpath('//select[@name="expkind"]')) select.select_by_index(1) |
|
条件 | 表示順(早い順) | select=Select(driver.find_element_by_xpath('//select[@name="s"]')) select.select_by_index(2) |
手段 | 空路(なし) | element=driver.find_element_by_xpath('//input[@id="air"]') if element.is_selected(): element.click() |
新幹線(あり) | element=driver.find_element_by_xpath('//input[@id="sexp"]') if not element.is_selected(): element.click() |
|
有料特急(あり) | element=driver.find_element_by_xpath('//input[@id="exp"]') if not element.is_selected(): element.click() |
|
高速バス(なし) | element=driver.find_element_by_xpath('//input[@id="hbus"]') if element.is_selected(): element.click() |
|
路線/連絡バス(なし) | element=driver.find_element_by_xpath('//input[@id="bus"]') if element.is_selected(): element.click() |
|
フェリー(なし) | element=driver.find_element_by_xpath('//input[@id="fer"]') if element.is_selected(): element.click() |
|
実行 | 検索ボタン | driver.find_element_by_xpath('//input[@id="searchModuleSubmit"]').click() |
出力画面
項目 | 修正コード |
---|---|
時間1 | driver.find_element_by_xpath('//ul[@id="rsltlst"]/li[1]/dl/dd/ul/li[1]').text |
金額1 | driver.find_element_by_xpath('//ul[@id="rsltlst"]/li[1]/dl/dd/ul/li[2]').text |
時間2 | driver.find_element_by_xpath('//ul[@id="rsltlst"]/li[2]/dl/dd/ul/li[1]').text |
金額2 | driver.find_element_by_xpath('//ul[@id="rsltlst"]/li[2]/dl/dd/ul/li[2]').text |
時間3 | driver.find_element_by_xpath('//ul[@id="rsltlst"]/li[3]/dl/dd/ul/li[1]').text |
金額3 | driver.find_element_by_xpath('//ul[@id="rsltlst"]/li[3]/dl/dd/ul/li[2]').text |
Excelシート
この情報から実際にSeleniumで動作するPythonスクリプトを作成します。
実行環境の準備
- Python実行環境を準備します。
Pythonのインストール - Seleniumの 実行環境を準備します。
Seleniumのインストール - Excelを操作するためのPythonライブラリを導入します。
xlwingsのインストール - 使用するブラウザのドライバを導入します。
Google Chrome
Firefox
Microsoft Edge
Internet Explorer
スクリプト構成
Seleniumスクリプトのプログラム構成(フローチャート)を考えます。
- 利用するライブラリを使う準備(import)
- ブラウザを操作するWebドライバの起動
- 入出力Excelシートを開く
- ブラウザ操作
- 「Yahoo!乗換案内」のサイトに移動
- Excelシートの内容を画面の項目に入力
- 「検索ボタン」クリック
- 出力結果を取得して、Excelシートに記入
- Excelシートの保存
1,2,3はブラウザ操作を行うための前準備、5は後処理ですので、1回だけ実行します。
4は画面操作なので、入力シートの件数分だけ繰り返しの処理となります。
1.ライブラリのインポート
Python(に限りませんが)では、利用者にとって役立つ共通部品的なプログラムが「ライブラリ」として提供されています。Pythonでライブラリを使用するには、前もって使用するライブラリ(モジュール)を import文で読み込む必要があります。
今回はSelenium関係のライブラリ、Excel操作のライブラリを利用します。
#Seleniumライブラリ
from selenium import webdriver
from selenium.webdriver.support.select import Select
#Excel操作ライブラリ
import xlwings as xw
2.Webドライバの起動
操作するブラウザのWebドライバを起動します。
#Chrome
driver_path = (Webドライバの配置先)
driver = webdriver.Chrome(executable_path=driver_path)
#Firefox
driver_path = (Webドライバの配置先)
driver = webdriver.Firefox(executable_path=driver_path)
#Microsoft Edge
driver_path = (Webドライバの配置先)
driver = webdriver.Edge(executable_path=driver_path)
#Intenet Explorer
driver_path = (Webドライバの配置先)
driver = webdriver.Ie(executable_path=driver_path)
3.Excelシートを開く
入出力用のExcelシートを開きます。インポートしたxlwingsを利用し、Excelシートのオブジェクトを「ws1」という変数に格納します。こうすることで、セルを ws1.cells(i, j).value という形式で扱うことができます。
book_path = (Excelブックの配置先)
wb1 = xw.Book(book_path)
ws1 = wb1.sheets[0] #シート1枚目
5.Excelシートの保存
Excelブックを保存して閉じます。
wb1.save()
wb1.close()
4.ブラウザ操作
- 「Yahoo!乗換案内」のサイトに移動
- Excelシートの内容を画面の項目に入力
- 「検索ボタン」クリック
- 出力結果を取得して、Excelシートに記入
4-1. 「Yahoo!乗換案内」のサイトに移動
ブラウザに「Yahoo!乗換案内」のURL(https://transit.yahoo.co.jp/)を入力します。
driver.get('https://transit.yahoo.co.jp/')
4-2.画面項目の入力
Excelシートから入力する項目
Excelシートの値を取得して、項目操作のPythonコードに渡します。
項目 | Excelの列 |
---|---|
出発駅 | 2列目 |
到着駅 | 3列目 |
経由1 | 4列目 |
経由2 | 5列目 |
出発駅
start_st = ws1.cells(i, 2).value #(i行,2列)
driver.find_element_by_xpath('//input[@id="sfrom"]').send_keys(start_st)
到着駅
end_st = ws1.cells(i, 3).value #(i行,3列)
driver.find_element_by_xpath('//input[@id="sto"]').send_keys(end_st)
経由1
経由は空欄の場合があるので、シートに記入されている場合だけ処理を行います。
if ws1.cells(i, 4).value is not None:
point1 = ws1.cells(i, 4).value
driver.find_element_by_xpath('//input[@id="svia1"]').send_keys(point1)
経由2
経由1と同様、記入されている場合だけ、表示ボタンのクリックと画面入力を行います。
if ws1.cells(i, 5).value is not None:
point2 = ws1.cells(i, 5).value
driver.find_element_by_xpath('//li[@id="via01"]/dl/dd/span[2]').click()
driver.find_element_by_xpath('//input[@id="svia2"]') .send_keys(point2)
固定項目
作成したコードをそのまま記入します。
日時(指定なし)
driver.find_element_by_xpath('//input[@id="tsAvr"]').click()
種別( 現金優先)
select=Select(driver.find_element_by_xpath('//select[@name="ticket"]'))
select.select_by_index(1)
座席(指定席)
select=Select(driver.find_element_by_xpath('//select[@name="expkind"]'))
select.select_by_index(1)
表示順序(料金が安い順)
select=Select(driver.find_element_by_xpath('//select[@name="s"]'))
select.select_by_index(2)
空路(なし)
element=driver.find_element_by_xpath('//input[@id="air"]')
if element.is_selected():
element.click()
新幹線(あり)
element=driver.find_element_by_xpath('//input[@id="sexp"]')
if not element.is_selected():
element.click()
有料特急(あり)
element=driver.find_element_by_xpath('//input[@id="exp"]')
if not element.is_selected():
element.click()
高速バス(なし)
element=driver.find_element_by_xpath('//input[@id="hbus"]')
if element.is_selected():
element.click()
路線/連絡バス(なし)
element=driver.find_element_by_xpath('//input[@id="bus"]')
if element.is_selected():
element.click()
フェリー(なし)
element=driver.find_element_by_xpath('//input[@id="fer"]')
if element.is_selected():
element.click()
4-3.検索ボタンクリック
作成したコードをそのまま記入します。
driver.find_element_by_xpath('//input[@id="searchModuleSubmit"]').click()
4-4.結果の取得
画面項目からの取得は作成済です。取得した内容をExcelシートに記入します。
所要時間1(シート7列目)
ws1.cells(i, 7).value = driver.find_element_by_xpath('//ul[@id="rsltlst"]/li[1]/dl/dd/ul/li[1]').text
金額1(シート8列目)
ws1.cells(i, 8).value = driver.find_element_by_xpath('//ul[@id="rsltlst"]/li[1]/dl/dd/ul/li[2]').text
所要時間2(9列目)
ws1.cells(i, 9).value = driver.find_element_by_xpath('//ul[@id="rsltlst"]/li[2]/dl/dd/ul/li[1]').text
金額2(10列目)
ws1.cells(i, 10).value = driver.find_element_by_xpath('//ul[@id="rsltlst"]/li[2]/dl/dd/ul/li[2]').text
所要時間3(11列目)
ws1.cells(i, 11).value = driver.find_element_by_xpath('//ul[@id="rsltlst"]/li[3]/dl/dd/ul/li[1]').text
金額3(12列目)
ws1.cells(i, 12).value = driver.find_element_by_xpath('//ul[@id="rsltlst"]/li[3]/dl/dd/ul/li[2]').text
出力画面の結果をExcelに記入するコードができました。
ただし、検索エラーになった場合は出力画面が表示されません。なので、正常に検索ができたかの判定を入れます。具体的には、検索ボタンをクリックした後にブラウザのURL情報を取得し、出力画面のURLが含まれている場合は正常、含まれていない場合はエラーと判断し、シートの7列目に「検索エラー」と記載することにします。
if driver.current_url.find('https://transit.yahoo.co.jp/search/result') == 0:
(正常時:結果の取得)
else:
ws1.cells(i, 7).value ='検索エラー'
ブラウザ操作の繰り返し
上記の画面操作をシートの件数分、実行します。画面処理の先頭に繰り返しの判定を入れます。Excelシート1列目のNoに対し、 2行目~空欄になるまでの間、処理を継続します。
i = 2 #2行目から開始
#シートのi行1列に値がある間、処理を繰り返す
while ws1.cells(i, 1).value is not None:
(シートi行目の操作)
i += 1 #繰り返しの最後にiを加算(i=i+1)
完成スクリプト
各構成ごとのスクリプトを1つにまとめます。
# -*- coding:utf-8 -*- import sys from selenium import webdriver from selenium.webdriver.support.select import Select import xlwings as xw # Chromeドライバ起動 driver_path = r'D:\SeleniumCodeChecker\driver\chromedriver.exe' driver = webdriver.Chrome(executable_path=driver_path) # Excelオープン book_path = r'D:\SeleniumCodeChecker\Tutrial1\交通費.xlsx' wb1 = xw.Book(book_path) ws1 = wb1.sheets[0] # シートの2件目以降、「No」がある間、処理を繰り返す i = 2 while ws1.cells(i, 1).value is not None: try: # 「Yahoo!乗換案内」に遷移 driver.get('https://transit.yahoo.co.jp/') # 可変項目はシートの値から入力 start_st = ws1.cells(i, 2).value driver.find_element_by_xpath('//input[@id="sfrom"]').send_keys(start_st) end_st = ws1.cells(i, 3).value # (i行,3列) driver.find_element_by_xpath('//input[@id="sto"]').send_keys(end_st) if ws1.cells(i, 4).value is not None: point1 = ws1.cells(i, 4).value driver.find_element_by_xpath('//input[@id="svia1"]').send_keys(point1) if ws1.cells(i, 5).value is not None: point2 = ws1.cells(i, 5).value driver.find_element_by_xpath('//li[@id="via01"]/dl/dd/span[2]').click() driver.find_element_by_xpath('//input[@id="svia2"]').send_keys(point2) # 不変項目 driver.find_element_by_xpath('//input[@id="tsAvr"]').click() select = Select(driver.find_element_by_xpath('//select[@name="ticket"]')) select.select_by_index(1) select = Select(driver.find_element_by_xpath('//select[@name="expkind"]')) select.select_by_index(1) select = Select(driver.find_element_by_xpath('//select[@name="s"]')) select.select_by_index(2) # 交通手段 element = driver.find_element_by_xpath('//input[@id="air"]') if element.is_selected(): element.click() element = driver.find_element_by_xpath('//input[@id="sexp"]') if not element.is_selected(): element.click() element = driver.find_element_by_xpath('//input[@id="exp"]') if not element.is_selected(): element.click() element = driver.find_element_by_xpath('//input[@id="hbus"]') if element.is_selected(): element.click() element = driver.find_element_by_xpath('//input[@id="bus"]') if element.is_selected(): element.click() element = driver.find_element_by_xpath('//input[@id="fer"]') if element.is_selected(): element.click() # 検索ボタンをクリック後、画面遷移を判定 driver.find_element_by_xpath('//input[@id="searchModuleSubmit"]').click() if driver.current_url.find('https://transit.yahoo.co.jp/search/result') == 0: ws1.cells(i, 7).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[1]/dl/dd/ul/li[1]').text ws1.cells(i, 8).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[1]/dl/dd/ul/li[2]').text ws1.cells(i, 9).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[2]/dl/dd/ul/li[1]').text ws1.cells(i, 10).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[2]/dl/dd/ul/li[2]').text ws1.cells(i, 11).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[3]/dl/dd/ul/li[1]').text ws1.cells(i, 12).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[3]/dl/dd/ul/li[2]').text else: ws1.cells(i, 7).value = '検索エラー' except: tp, value, traceback = sys.exc_info() ws1.cells(i, 7).value = '例外発生' ws1.cells(i, 8).value = tp.__name__ ws1.cells(i, 9).value = str(value) print(tp.__name__ + ':' + str(value)) i += 1 wb1.save() wb1.close()
例外発生時のハンドリングを追加しています。プログラムエラーで処理が続けられなくなった場合は、except句でエラー情報をExcelシートに記載して、次の行を実行します。
スクリプトの実行
Pythonで作成したSeleniumのスクリプトを実行します。
コマンドプロンプトから実行する場合
- Windows10の場合
「スタートメニュー」「 Windows システムツール」「コマンドプロンプト」 - Windows8の場合
「すべてのアプリ」 「 Windows システムツール」「コマンドプロンプト」 - Windows7の場合
「スタートメニュー」「すべてのプログラム」「アクセサリ」「コマンドプロンプト」
コマンドプロンプトの画面で
python 実行ファイル名
プログラムが起動すると、Excelとブラウザが開き「Yahoo!乗換案内」のサイトに移動します。
新幹線利用など、検索条件が正しく入力されているかを確認します。
※何回か繰り返していると分かりますが、一番最初の名古屋のケースで出力画面まで行かない時があります。
ともかく、Excelの出力結果を見てみます。
Excel出力結果
- No1:例外が発生しています。
- No3:金額が異常に高いです。
- No5:料金の安い順で検索すると、普通電車で行った場合の料金が表示されるようです。
- No9:わざとエラーになるようにセットしましたが、正しく「検索エラー」が表示されています。
エラー対処、スクリプトの改良
エラー及び、想定外の結果が出力される原因を考えます。
A.例外の発生(No1)
Excelシートに記載されている例外情報を参照します。
"Message: element click intercepted: Element <input type=""radio"" name=""type"" id=""tsAvr"" value=""5"" data-rapid_p=""22""> is not clickable at point (370, 501). Other element would receive the click: <a tabindex=""-1"" href=""javascript:setStation("...",24990,"svia1")"">名鉄名古屋</a> (Session info: chrome=79.0.3945.79)"
どうやら「日時(指定なし)」がクリックできていないようです。「名鉄名古屋」のリンクが隠してしまっているとのことです。
確かに「指定なし」が、「経由1」のプルダウンに隠れてしまっています。
「画面に表示されていない要素はクリックできない」というSeleniumの仕様です。
回避策
画面の右上に「駅候補表示 オン|オフ」とあります。オフにするとプルダウンは発生しないようです。オフを選択する操作を加えます。
XPath取得・解析ツールとXPath検証ツールでPythonコードを生成します。
項目 | XPath | 生成コード |
---|---|---|
駅候補 | //a[contains(text(),"オフ")] //span[contains(text(),’オフ’)] |
driver.find_element_by_xpath |
「駅候補表示」は未選択の場合はaタグ、選択済の場合はspanタグに切り替わります。ですから、画面にaタグが存在する場合のみクリックするようにします。(find_elementsに注意)
項目 | 修正コード |
---|---|
駅候補 | if len(driver.find_elements_by_xpath('//a[contains(text(),"オフ")]')) == 1: |
B.料金が異常に高い(No3)
東京の京橋駅でなく、大阪の京橋駅で検索しているのが原因です。
C.新幹線を利用していない(No5)
新幹線利用を条件にしているのに、普通電車の料金が表示されるのは、Yahoo側の仕様なので、こちらでは対処できません。日時を「指定なし」にしても、夜間などで、検索した時間が新幹線が間に合わない時間帯だと、普通電車で計算されるようです。
回避策
料金が安い順だけでなく、到着が早い順も取得対象に入れたいと思います。
項目 | XPath | 生成コード |
---|---|---|
到着時刻順 | //ul[@id="tabflt"]/li[1]/a/span | driver.find_element_by_xpath |
Excelシートにも到着時刻順の項目を追加します。
Excelシート(完成版)
変更をSeleniumスクリプトに反映させます。
修正スクリプト
# -*- coding:utf-8 -*- import sys from selenium import webdriver from selenium.webdriver.support.select import Select import xlwings as xw # Chromeドライバ起動 driver_path = r'D:\SeleniumCodeChecker\driver\chromedriver.exe' driver = webdriver.Chrome(executable_path=driver_path) # Excelオープン book_path = r'D:\SeleniumCodeChecker\Tutrial1\交通費.xlsx' wb1 = xw.Book(book_path) ws1 = wb1.sheets[0] # シートの2件目以降、「No」がある間、処理を繰り返す i = 2 while ws1.cells(i, 1).value is not None: try: # 「Yahoo!乗換案内」に遷移 driver.get('https://transit.yahoo.co.jp/') # 駅候補表示オフ if len(driver.find_elements_by_xpath('//a[contains(text(),"オフ")]')) == 1: driver.find_element_by_xpath('//a[contains(text(),"オフ")]').click() # 可変項目はシートの値から入力 start_st = ws1.cells(i, 2).value driver.find_element_by_xpath('//input[@id="sfrom"]').send_keys(start_st) end_st = ws1.cells(i, 3).value # (i行,3列) driver.find_element_by_xpath('//input[@id="sto"]').send_keys(end_st) if ws1.cells(i, 4).value is not None: point1 = ws1.cells(i, 4).value driver.find_element_by_xpath('//input[@id="svia1"]').send_keys(point1) if ws1.cells(i, 5).value is not None: point2 = ws1.cells(i, 5).value driver.find_element_by_xpath('//li[@id="via01"]/dl/dd/span[2]').click() driver.find_element_by_xpath('//input[@id="svia2"]').send_keys(point2) # 不変項目 driver.find_element_by_xpath('//input[@id="tsAvr"]').click() select = Select(driver.find_element_by_xpath('//select[@name="ticket"]')) select.select_by_index(1) select = Select(driver.find_element_by_xpath('//select[@name="expkind"]')) select.select_by_index(1) select = Select(driver.find_element_by_xpath('//select[@name="s"]')) select.select_by_index(2) # 交通手段 element = driver.find_element_by_xpath('//input[@id="air"]') if element.is_selected(): element.click() element = driver.find_element_by_xpath('//input[@id="sexp"]') if not element.is_selected(): element.click() element = driver.find_element_by_xpath('//input[@id="exp"]') if not element.is_selected(): element.click() element = driver.find_element_by_xpath('//input[@id="hbus"]') if element.is_selected(): element.click() element = driver.find_element_by_xpath('//input[@id="bus"]') if element.is_selected(): element.click() element = driver.find_element_by_xpath('//input[@id="fer"]') if element.is_selected(): element.click() # 検索ボタンをクリック後、画面遷移を判定 driver.find_element_by_xpath('//input[@id="searchModuleSubmit"]').click() if driver.current_url.find('https://transit.yahoo.co.jp/search/result') == 0: #料金が安い順 ws1.cells(i, 7).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[1]/dl/dd/ul/li[1]').text ws1.cells(i, 8).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[1]/dl/dd/ul/li[2]').text ws1.cells(i, 9).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[2]/dl/dd/ul/li[1]').text ws1.cells(i, 10).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[2]/dl/dd/ul/li[2]').text ws1.cells(i, 11).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[3]/dl/dd/ul/li[1]').text ws1.cells(i, 12).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[3]/dl/dd/ul/li[2]').text #到着が早い順 driver.find_element_by_xpath('//ul[@id="tabflt"]/li[1]/a/span').click() ws1.cells(i, 13).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[1]/dl/dd/ul/li[1]').text ws1.cells(i, 14).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[1]/dl/dd/ul/li[2]').text ws1.cells(i, 15).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[2]/dl/dd/ul/li[1]').text ws1.cells(i, 16).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[2]/dl/dd/ul/li[2]').text ws1.cells(i, 17).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[3]/dl/dd/ul/li[1]').text ws1.cells(i, 18).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[3]/dl/dd/ul/li[2]').text else: ws1.cells(i, 7).value = '検索エラー' except: tp, value, traceback = sys.exc_info() ws1.cells(i, 7).value = '例外発生' ws1.cells(i, 8).value = tp.__name__ ws1.cells(i, 9).value = str(value) print(tp.__name__ + ':' + str(value)) i += 1 wb1.save() wb1.close()
スクリプトの実行
コマンドプロンプトから、改良したSeleniumスクリプトを実行します。
Excelシートの結果を確認します。
エラーへの対処
A.「指定なし」がクリックできない例外は解消しましたが、別の例外が発生しています。到着順の取得はできているようです。新たに発生した例外への対処を考えます。
"Message: stale element reference: element is not attached to the page document (Session info: chrome=79.0.3945.79)"
(訳)古い要素を参照:要素はHTMLページ文書に存在しません
ちょっと分かりにくいですが、例外メッセージは、『XPathで指定した項目が画面に存在せず、古いページの画面項目を取得しにいっている』と指摘しています。しかしながら「到着が早い順」の画面には、所要時間・金額とも指定したXPathの項目が実在しています。
通常Seleniumは、ブラウザがサーバーと通信して新たなページを開く場合、ページ読み込みの間は待機して、読み込みが終わってから要素の取得を行います。
ところが、今回のケースでは「料金が安い順」のページにも「到着が早い順」 のページにも、同一XPathの所要時間・金額の項目が存在します。するとSeleniumは「到着が早い順」ボタンを押した瞬間、「料金が安い順」の時間・金額を見て、通信が発生する前に項目が存在すると判定します。しかし判定終了後、項目の値を取りに行く段階では、サーバーとの通信が始まって、時間・金額が取得できない状態になります。
一方、うまく通信が終わった段階で、時間・金額を取得できた場合はエラーにならないので、通信のタイミングによって、正常に動作したり、エラーになったりするようです。
回避策
Pythonの待機関数を使用します。
Pythonのライブラリ time にある待機関数を使い、「到着が早い順」ボタンを押した後に待機します。
import time
time.sleep(秒数)
なお、sleepを使わない方法もありますが、カスタムクラスを作る必要があるので少々厄介です。(参考:sleepを使わない方法)
「Yahoo!乗換案内」自動操作スクリプト(完成版)
# -*- coding:utf-8 -*- import sys import time from selenium import webdriver from selenium.webdriver.support.select import Select import xlwings as xw # Chromeドライバ起動 driver_path = r'D:\SeleniumCodeChecker\driver\chromedriver.exe' driver = webdriver.Chrome(executable_path=driver_path) # Excelオープン book_path = r'D:\SeleniumCodeChecker\Tutrial1\交通費.xlsx' wb1 = xw.Book(book_path) ws1 = wb1.sheets[0] # シートの2件目以降、「No」がある間、処理を繰り返す i = 2 while ws1.cells(i, 1).value is not None: try: # 「Yahoo!乗換案内」に遷移 driver.get('https://transit.yahoo.co.jp/') # 駅候補表示オフ if len(driver.find_elements_by_xpath('//a[contains(text(),"オフ")]')) == 1: driver.find_element_by_xpath('//a[contains(text(),"オフ")]').click() # 可変項目はシートの値から入力 start_st = ws1.cells(i, 2).value driver.find_element_by_xpath('//input[@id="sfrom"]').send_keys(start_st) end_st = ws1.cells(i, 3).value # (i行,3列) driver.find_element_by_xpath('//input[@id="sto"]').send_keys(end_st) if ws1.cells(i, 4).value is not None: point1 = ws1.cells(i, 4).value driver.find_element_by_xpath('//input[@id="svia1"]').send_keys(point1) if ws1.cells(i, 5).value is not None: point2 = ws1.cells(i, 5).value driver.find_element_by_xpath('//li[@id="via01"]/dl/dd/span[2]').click() driver.find_element_by_xpath('//input[@id="svia2"]').send_keys(point2) # 不変項目 driver.find_element_by_xpath('//input[@id="tsAvr"]').click() select = Select(driver.find_element_by_xpath('//select[@name="ticket"]')) select.select_by_index(1) select = Select(driver.find_element_by_xpath('//select[@name="expkind"]')) select.select_by_index(1) select = Select(driver.find_element_by_xpath('//select[@name="s"]')) select.select_by_index(2) # 交通手段 element = driver.find_element_by_xpath('//input[@id="air"]') if element.is_selected(): element.click() element = driver.find_element_by_xpath('//input[@id="sexp"]') if not element.is_selected(): element.click() element = driver.find_element_by_xpath('//input[@id="exp"]') if not element.is_selected(): element.click() element = driver.find_element_by_xpath('//input[@id="hbus"]') if element.is_selected(): element.click() element = driver.find_element_by_xpath('//input[@id="bus"]') if element.is_selected(): element.click() element = driver.find_element_by_xpath('//input[@id="fer"]') if element.is_selected(): element.click() # 検索ボタンをクリック後、画面遷移を判定 driver.find_element_by_xpath('//input[@id="searchModuleSubmit"]').click() #time.sleep(1) #Microsoft Edge Legacy if driver.current_url.find('https://transit.yahoo.co.jp/search/result') == 0: #料金が安い順 ws1.cells(i, 7).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[1]/dl/dd/ul/li[1]').text ws1.cells(i, 8).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[1]/dl/dd/ul/li[2]').text ws1.cells(i, 9).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[2]/dl/dd/ul/li[1]').text ws1.cells(i, 10).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[2]/dl/dd/ul/li[2]').text ws1.cells(i, 11).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[3]/dl/dd/ul/li[1]').text ws1.cells(i, 12).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[3]/dl/dd/ul/li[2]').text #到着が早い順 driver.find_element_by_xpath('//ul[@id="tabflt"]/li[1]/a/span').click() #ページの切替わりを待機 time.sleep(2) ws1.cells(i, 13).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[1]/dl/dd/ul/li[1]').text ws1.cells(i, 14).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[1]/dl/dd/ul/li[2]').text ws1.cells(i, 15).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[2]/dl/dd/ul/li[1]').text ws1.cells(i, 16).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[2]/dl/dd/ul/li[2]').text ws1.cells(i, 17).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[3]/dl/dd/ul/li[1]').text ws1.cells(i, 18).value = driver.find_element_by_xpath \ ('//ul[@id="rsltlst"]/li[3]/dl/dd/ul/li[2]').text else: ws1.cells(i, 7).value = '検索エラー' except: tp, value, traceback = sys.exc_info() ws1.cells(i, 7).value = '例外発生' ws1.cells(i, 8).value = tp.__name__ ws1.cells(i, 9).value = str(value) print(tp.__name__ + ':' + str(value)) i += 1 wb1.save() wb1.close()
スクリプトの実行
コマンドプロンプトから、改良したSeleniumスクリプトを実行します。
シナリオ実行結果(動画)
Excelシートの結果を確認します。
全件正常に終了しました。品川-仙台間も新幹線利用の料金が取得できています。
なお、Chrome・Firefox・Internet Explorer・Edge(Chromium版)は正常に動作しましたが、Microsoft Edgeのレガシー版では64行目に待機処理を入れないと、正常に動作しませんでした。
結果の考察、評価
スクリプト実行結果
参考として、 Excel関数で「A.全体の最安料金」、「B.到着が早い中での最安料金」を表示させています。
結果の考察
- No1.申請額と最安料金が異なる。
申請額は「のぞみ」、最安料金は「ひかり」を利用しているためです。単純に申請額と検索結果が異なればNGという訳にはいかないようで、こういった違いは人間が判断する必要がありそうです。 - No3.東京の京橋駅が、大阪の京橋駅で検索される。
「Yahoo!乗換案内」を使用する以上、Yahoo側が対処してくれないので、Selenium側で対処する必要があります。一応、辞書登録のような形で、「京橋」が指定されたら自動的に「京橋(東京都)」と入力するような対応が考えられます。
しかし、そうすると、今度は「京橋(大阪府)」が検索できなくなります。また、同一駅名が出てくるたびに辞書登録するのは、現実的ではありません。
そのうち、AIが勝手に判断してくれるかもしれませんが、現時点では人間が判断するしかないと思います。なおRPAソフトの中には、申請額と異なると自動的にメールを送ることもできるようですが、社内とはいえ、この状態でメールを送るのは問題かと思います。 - No4、No8.申請額と検索結果が異なる。
これは経路の取り方の問題です。このぐらいの誤差は認めている企業も多いのではないでしょうか。 - No7.他に安い経路がある。
品川から八王子まで、東京からJRで行くと820円ですが、新宿で京王線に乗り換えると、「京王八王子」まで570円で行けます。時間もあまり変わりません。会社の内務規定によりますが、もし安い料金が推奨されているなら、京王線を使うべきです。
しかしながらプログラムは申請書に記載の「八王子」しか検索しないので、京王線を使うルートに気づくことができません。このようなケースも人間の判断が必要となります。
評価
評価は、「1.Seleniumの性能評価」と「2.自動操作で業務を置き換えることができるか」の2点で行います。
1.SeleniumはRPAソフトとして使えるか?
- 「Yahoo!乗換案内」の自動操作、画面内容の取得は問題なくできました。
Seleniumには画像認識(画像マッチング)機能はありませんが、画面項目の取得にはXPathの方が確実性が高いので、なくても大丈夫だと思います。 - Pythonライブラリと組み合わせることで、Excelからの入出力が可能です。
Pythonは他にも豊富なライブラリがあります。 - プログラミングの負荷、難易度
人間が行う操作をプログラムにするので、操作手順を細分化して整理できれば、自動操作のシナリオ(プログラム構成)に落とし込むことは難しくありません。
プログラム構成(フローチャート)が作成できれば、スクリプトを書く作業はXPath検証ツールの助けもあり、比較的スムーズに行えるかと思います。
むしろ後述の修正作業(デバッグ作業)の方が、負荷・難易度とも、高いと思われます。 - 想定内のエラーについては、エラーを判定のロジックを入れることで、処理を止めずに継続できます。
想定外のエラーについても、例外処理を入れることで、 処理を止めずに継続できます。 - 「画面上に表示されていない項目はクリックできない」等、Seleniumの特性に依存する部分があります。スムーズな自動操作の実現には、 Seleniumの特性に対するノウハウを蓄積していく必要があります。
- スクリプトを作成しても、エラーが発生せず実際に使用できる段階まで、ある程度の修正作業(デバッグ)が必要です。ここの部分の時間を短縮するためにも、ノウハウの蓄積が重要となります。
- ノウハウが重要であることを考えると、誰にでも使えるツールというよりは、担当者がいて、その担当者にノウハウを蓄積させていく方が生産性は高まると思われます。ただし、画面のXPath取得などは単純作業なので、作業タスクの切り出しを検討してもよいかと思います。
ソフトの特性を理解しなければならないことや、エラー発生時の修正作業(デバッグ)にノウハウを要求されるのは、他のRPAソフトも同様だと思います。そして、ノウハウを蓄積させることを考えると外部委託は好ましくありません。
有償ソフトの場合は、販売元のサポートが期待できますが、Seleniumはフリーソフトなのでサポートはありません。ただしWeb自動テストのツールとして、長年に渡り世界中で使用されているので、ユーザーコミュニティが発達しており、日本フォーラムもあります。Seleniumの場合、ネットを検索すれば、日本語も含めて多くのQ&A情報が得られます。
2.自動操作は業務を置き換えるか?
- 人間の目でチェックする部分は残る
申請額 と「Yahoo!乗換案内」の検索結果を突き合わせるという程度の作業でも、実は多くの判断要素があり、コンピュータで置き換えることは難しいということが分かりました。しかしながら、「ブラウザを操作して、結果をExcelに記入する部分」は定型作業であり、自動化することができました。
また、例えば「多くの件数から不一致の場合のみExcelで強調表示する」など、判断レベルを下げることで、より効果的に使用することも可能かと思います。 - 例外ケースが多いと厄介
「京橋駅」のように同一駅名が存在する場合、「八王子」と「京王八王子」のように名前は異なるが同一駅と扱えるような場合、定型入力では正しい結果を得ることができません。イレギュラー(例外ケース)として扱う必要があります。
しかしながら、個々の例外ケースを処理ロジックを組み込んで対応するとしても、一律に処理対象外として人間がチェックするとしても、どちらにせよ何らかの判定ロジックを組み入れる必要があります。例外ケースのバリエーションがが多い場合や、どんな例外ケースが存在するか把握できない場合は対応が難しくなります。 - 外部のサイトを利用する場合は、サイト特性を理解する必要がある
自社サイトであれば、自社向けにカスタマイズされているので、特別な対応を要求されることは少ないと思います。しかし外部サイトの場合は、外部サイトに合わせて、自動操作のシナリオを組み立てる必要があります。また使用するRPAソフトとサイトの相性の問題で対応が困難になるケースや、最悪の場合は利用できない可能性もあります。
また、コンシュマー(消費者)向けのサイトはデザインを良くするため、スタイルシートやJavaScript等を用いた高度な表現テクニックを使用しています。一般的にシンプルな業務用サイトと異なり、画面項目の操作が煩雑になる傾向があり、スクリプト作成の作業工数が増加しやすい点にも注意が必要です。
ディスカッション
コメント一覧
まだ、コメントがありません