[Python] Docker+Python+Seleniumでスクレイピング

#はじめに
前回の記事でDockerコンテナでPython環境を作成しSeleniumを操作してみました。
今回はWebページのフォームで値を入力し、その結果の表示確認をしてみます。
SeleniumでWebページを操作する基本のような部分を備忘録として残すために記載したので見づらいかもしれませんが、そこはご了承ください。


#実施すること
今回は下記の項目を実施してみようと思います。
1. Seleniumを使用しChromeを起動する。
2. Webページを表示する。
3. ページ内のボタン要素をクリックし、ページ遷移する。
4. 遷移後のページで必要な要素が表示されるまで待機する。
5. ページのp要素からテキストを取得する。
6. ページ内のボタン要素をクリックし、ダイアログボックスを表示させる。
7. ダイアログボックス内のinput要素に値を入力する。
8. ダイアログボックス内のdropdownメニューから値をセレクトする。
9. ダイアログボックス内のボタン要素をクリックし、データを送信する。
10. 送信完了のアラートが表示されるまで待機する。
11. アラート内のOKボタンをクリックする。
12. ページ内の情報を更新するため、ページのリロードを行う。
13. リロード後のページで必要な要素が表示されるまで待機する。
14. ページのp要素からテキストを取得する。

DockerでPythonのSelenium環境を構築する方法については前回記事([[Python] Dockerコンテナでseleniumを使ってみる](https://qiita.com/y_kitchens/items/eb5a2f4d176807335fcc))を参考にしてみてください。


#Seleniumを使ってみる
###実装コード
今回実装したコードはこちらになります。

```python:docker_selenium.py
import time

from selenium import webdriver
from selenium.webdriver.common.alert import Alert
from selenium.webdriver.common.by import By
from selenium.webdriver.support.select import Select
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# webのフォームに入力する内容を変数に格納
MILAGE = 38.38
ELEVATION = 218
WEATHER = ['晴れ', '曇り', '雨']

# webdriverのオプションを設定
options = webdriver.ChromeOptions()
options.add_argument('--headless')

print('Connect remote browser...')

driver = webdriver.Remote(command_executor='172.21.0.3:4444/wd/hub', options=options)
print('remote browser connected...')

try:
	# ブラウザでWebページを開く
	driver.get('https://xxxxxxxx.herokuapp.com/')
	print('current URL: ', driver.current_url)

	# ボタンをクリックしページ遷移する処理
	bicycle_button = driver.find_element(By.XPATH, '//input[@id="button_bicycle"]')
	bicycle_button.click()
	print('current URL: ', driver.current_url)

	# ページの読み込みのための最大待ち時間
	print('Max 5 secound wait...')
	wait = WebDriverWait(driver, 5)
	wait.until(EC.visibility_of_element_located((By.XPATH, '//p[@id="sum_milage"]')))

	# 新規データ入力前の今月の走行距離のテキストを取得する
	bicycle_page_message_2 = driver.find_element(By.XPATH, '//p[@id="sum_milage"]').get_attribute('textContent')
	print('bicycle_page_message_2 before: ', bicycle_page_message_2)

	# データをインプットするためのダイアログを開く処理
	input_dialog_button = driver.find_element(By.XPATH, '//*[@id="button_to_input_dialog"]')
	input_dialog_button.click()
	print('input dialog open...')

	# ページの読み込みのための待ち時間
	print('3 secound wait...')
	time.sleep(3)

	# 走行距離を入力するinputタグ要素を取得
	input_milage = driver.find_element(By.XPATH, '//*[@id="input_milage"]')
	# 入力内容をクリア(何も入力されていないけど念のため)
	input_milage.clear()
	# 入力内容をセット
	input_milage.send_keys(MILAGE)

	# 獲得標高を入力するinputタグ要素を取得し、値を入力
	input_elevation = driver.find_element(By.XPATH, '//*[@id="input_elevation"]')
	input_elevation.clear()
	input_elevation.send_keys(ELEVATION)

	# ドロップダウン要素を取得する
	dropdown = driver.find_element(By.XPATH, '//*[@id="select_weather"]')
	# ドロップダウン要素からSelectオブジェクトを作成する
	select_weather = Select(dropdown)
	# ドロップダウンの値を表示されているテキストを選択して記入する
	select_weather.select_by_visible_text(WEATHER[0])

	# DBへの入力(Input)ボタンをクリックする処理
	input_button = driver.find_element(By.XPATH, '//*[@id="button_input_to_db"]')
	input_button.click()
	print('Input button click...')

	# データが送信されalert表示のための最大待ち時間
	print('Max 10 secound wait...')
	alert_wait = WebDriverWait(driver, 10)
	alert_wait.until(EC.alert_is_present())

	# アラートのokをクリックする処理
	Alert(driver).accept()
	print('alert ok button click...')

	# ページのリロード
	driver.refresh()
	print('reload page...')

	# ページの読み込みのための最大待ち時間
	print('Max 5 secound wait...')
	wait = WebDriverWait(driver, 5)
	wait.until(EC.visibility_of_element_located((By.XPATH, '//p[@id="sum_milage"]')))

	# 新規データ入力後の今月の走行距離のテキストを取得する
	bicycle_page_message_2 = driver.find_element(By.XPATH, '//p[@id="sum_milage"]').get_attribute('textContent')
	print('bicycle_page_message_2 after: ', bicycle_page_message_2)

except Exception as e:
	print('Exception: ', e)

finally:
	# リモートサーバーとの接続を終了する
	print('Connection stop...')
	driver.quit()
```
都合上、WebサイトのURLを一部非表示にしています。
try,except,finally文についてはドライバーを確実に終了させる目的で記載しましたので正確な位置に記述されていないかもしれません。
おかしな点についてはご指摘等いただければありがたいです。


それでは各項目の処理について詳細を見てみます。

###XPathについて
各項目の処理を見る前に、Seleniumで要素を指定する際によく使用されるXPathについて解説します。
XPathはXML文章中の要素、属性値などを指定するための言語で、XML文章をツリーとして捉えることで、要素や属性の位置を指定します。
要素の指定はロケーションパスを指定する方式を使用し、属性の指定には@マークを使用します。
また、HTMLもXMLの一種とみなすことができるため、XPathを使ってHTML文章中の要素を指定することができます。

● 記述方法
対象の要素にid, class等の属性がある場合と無い場合で若干記述の仕方が変わります。

1. 対象の要素にid, class等の属性がある場合

  ```text
  //p[@id="sum_milage"]
  ```
対象の要素を指定する形で記述します。
分解して内容を見てみると、

```text
// : ノードpathを記述する。(「//」はルートからのpathの省略形)
p : 要素の種類を記述
@id : 「@」で要素の属性を指定する。classを指定する時は@class=""
```
ノードpathはhtmlからのフルパスで記述しても問題ありません。 → 例). html/body/div/p

2. 対象の要素に属性がない場合

```text
//*[@id="top_block"]/p
```
対象の要素に属性がない場合はその要素の親要素を指定し、その親要素の配下にある〇〇要素という形で記述します。
同じくこの要素を分解すると、

```text
* : 全ての要素を選択
[@id=""]: 対象要素の親要素を指定する
/p : 「/」の前で取得した要素の配下の要素を取得している
```
今回の処理では全ての要素の中からidがtop_blockの要素を取得し、その配下のpタグ要素を取得しています。
同じタグ要素が複数ある時はリスト形式で表現します。 → 例) //*[@id="top_block"]/p[3]

● Chromeの検証ツールからコピーする方法
XPathは下記手順でChromeの検証ツールからコピーしてくることも出来ます。
1. 対象ページの開発者ツールのElementを確認する
2. 必要な要素を選択する
3. 右クリックでコンテキストメニューを開く
4. Copy → Copy XPATHを選択する
5. エディタに貼り付ける 
比較的簡単にコピーできるし、ノードpathや属性のスペルミスも無くなるのでこの方法も便利です。

XPathの他のメソッドについてはこちらのサイト([クローラ作成に必須！XPATHの記法まとめ](https://qiita.com/rllllho/items/cb1187cec0fb17fc650a))を参考にしてみてください。
それでは実際の処理を見てみます。

###webページを表示する
正確にはブラウザドライバーでリクエストしたwebページのレスポンスを取得する処理です。表現の都合上、表示と記載します。

![A2C1EB4F-25FD-4251-9808-025FA2AB8C25_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505203/3559270d-695d-c26e-efea-c37d4634c689.jpeg)

```python:抜粋
from selenium import webdriver

options = webdriver.ChromeOptions()

driver = webdriver.Remote(command_executor='172.21.0.3:4444/wd/hub', options=options)

# ブラウザでWebページを開く
driver.get('https://xxxxxxxx.herokuapp.com/')
```
ライブラリからseleniumのwebdriverをimportします。
オプションでどのブラウザを起動するか指定します。今回はChromeドライバーを設定。
別コンテナに作成しているリモートサーバーのオブジェクトを生成し、get()メソッドにURLを引数として渡してあげればWebページを表示できます。


###ボタン要素をクリックする
赤枠内のボタンをクリックする処理です。
![6FD2D458-BD6E-4886-A250-E5CD737A0696_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505203/b6c3fa9c-507f-e5e2-51af-fa31afc93606.jpeg)

```python:抜粋
bicycle_button = driver.find_element(By.XPATH, '//input[@id="button_bicycle"]')
bicycle_button.click()
```
ボタン要素をクリックするには、find_element()メソッドで対象要素のオブジェクトを生成し、click()メソッドを呼び出します。


###要素からテキストを取得する
赤枠内のp要素から青文字のテキストを取得する処理です。
![9FFDC7A2-EE51-4A8A-B173-D1341EFEF823_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505203/514c8d24-a0c5-ad2f-283e-4c6219bb6364.jpeg)

```python:抜粋
bicycle_page_message_2 = driver.find_element(By.XPATH, '//p[@id="sum_milage"]').get_attribute('textContent')
```
要素のテキストを取得するにはfind_element()メソッドで対象要素のオブジェクトを生成し、get_attribute()メソッドを呼び出します。
他にも.textでテキストを取得できるようですが、属性で非表示に指定されている場合(display:none 等)は取得できないようなので、属性に関係なくテキストを取得できるget_attribute()メソッドが良いかと思います。

###input要素に値を入力する
画像の赤枠内の①と②に値を入力する処理です。
![22C0C689-D0FB-437D-9672-4E4D0EA1011B_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505203/a514c77d-3173-d7e2-b7a9-974755deb20f.jpeg)

```python:抜粋
# 入力したい値を変数に格納
MILAGE = 38.38
ELEVATION = 218

# ①の処理
# 走行距離を入力するinputタグ要素を取得
input_milage = driver.find_element(By.XPATH, '//*[@id="input_milage"]')
# 入力内容をクリア(何も入力されていないけど念のため)
input_milage.clear()
# 入力内容をセット
input_milage.send_keys(MILAGE)

# ②の処理
# 獲得標高を入力するinputタグ要素を取得し、値を入力
input_elevation = driver.find_element(By.XPATH, '//*[@id="input_elevation"]')
input_elevation.clear()
input_elevation.send_keys(ELEVATION)
```
①と②は同じ処理をしています。
input要素に値を入力するにはfind_element()メソッドで対象要素のオブジェクトを生成し、send_keys()メソッドを呼び出し、引数に入力したい値を渡し実行すれば値の入力が出来ます。
input要素に値がセットされている場合には事前にclear()メソッドで値をクリアしますが、値が入力されていない要素でもバグ防止のため実施しておいた方が無難かと思います。


###dropdownメニューで項目を選択する
画像の赤枠内③のdropdownメニューで項目を選択する処理です。
![22C0C689-D0FB-437D-9672-4E4D0EA1011B_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505203/a514c77d-3173-d7e2-b7a9-974755deb20f.jpeg)

```python:抜粋
# 必要なライブラリのimport
from selenium.webdriver.support.select import Select

# 入力したい値を変数に格納
WEATHER = ['晴れ', '曇り', '雨']

# ドロップダウン要素を取得する
dropdown = driver.find_element(By.XPATH, '//*[@id="select_weather"]')
# ドロップダウン要素からSelectオブジェクトを作成する
select_weather = Select(dropdown)
# ドロップダウンの値を表示されているテキストを選択して記入する
select_weather.select_by_visible_text(WEATHER[0])
```
dropdown要素で値を選択するには、ライブラリからSelectクラスをimportしSelectインスタンスを作成します。
Selectインスタンスで値を選択する方法はいくつかありますが、今回はdropdownメニューの項目に表示されているテキストで指定する方法でコーディングしています。
Selectクラスに用意されているselect_by_visible_text()メソッドに選択したいメニューのテキストを渡してあげればその項目が選択された状態になります。

他の方法についてはこちらのサイト([Selenium API(逆引き)・・・Selenium APIを利用目的から検索できます](https://www.seleniumqref.com/api/webdriver_gyaku.html))を参考にしてみてください。

###Alertの「OK」ボタンをクリックする
画面に表示されるAlertのオブジェクトを取得し「OK」ボタンをクリックする処理です。
スクショは取れなかったので画像はありません。

```python:抜粋
from selenium.webdriver.common.alert import Alert

# データが送信されalert表示のための最大待ち時間
alert_wait = WebDriverWait(driver, 10)
alert_wait.until(EC.alert_is_present())

# アラートのokをクリックする処理
Alert(driver).accept()
```
ライブラリからAlertクラスをimportします。
Alertインスタンスを生成する際にdriverオブジェクトを渡し、accept()メソッドを実行すればOKボタンのクリックが完了します。

またAlertが画面に表示されるのを待つ処理について、Alertの表示確認にはalert_is_present()メソッドを使用します。


###最大待ち時間を指定して処理を止める
ページ内の要素の読み込みやAlertの表示を待ったりと、ページ内処理を待つ際に使えるメソッド。

```python:抜粋
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 5)
wait.until(EC.visibility_of_element_located((By.XPATH, '//p[@id="sum_milage"]')))
```
まずライブラリからWebDriverWaitとexpected_conditionsをimportします。
expected_conditionsをECとするのはSeleniumのお作法みたいですね。
WebDriverWaitインスタンスを作成し、waitオブジェクトを作成します。その際、第二引数に処理が完了するまでの「最大」待ち時間を指定します。
そしてオブジェクトの持っているuntil()メソッドで望む状態になっているか確認します。
until()メソッドの引数には、expected_conditionsのメソッドでXPathで指定した要素に対する期待動作を記述します。
今回はvisibility_of_element_located()メソッドで、指定要素が表示されているかを確認しています。
XPathで指定した要素が画面に表示されているのが確認できたらuntil()メソッドは正常終了し次の処理に移ります。
もしWebDriverWaitインスタンスを作成する際に指定した最大待ち時間を過ぎても指定要素が画面に表示されなかったらExceptionが返されます。

*標準ライブラリtimeのsleep()との違いは、処理の完了が確認できれば最大時間待つことなく次の処理に移ることです。


###ページをリロードする

```python:抜粋
driver.refresh()
```
ページをリロードするには、driverオブジェクトのrefresh()メソッドを使用します。


#実行してみる
実際に処理が動作するか、コードを実行して確認してみます。

まずターミナル上で確認してみます。

```bash
root@9f672ec558d8:/work# python3 docker_selenium.py
Connect remote browser...
remote browser connected...
current URL:  https://xxxxxxxx.herokuapp.com/
current URL:  https://xxxxxxxx.herokuapp.com/bicycle_contents
Max 5 secound wait...
bicycle_page_message_2 before:  今月は 2036.59 km 走っています。
input dialog open...
3 secound wait...
Input button click...
Max 10 secound wait...
alert ok button click...
reload page...
Max 5 secound wait...
bicycle_page_message_2 after:  今月は 2074.97 km 走っています。
Connection stop...
```
bicycle_page_message_2のbefore, afterで走行距離が変化していますね。
その他の処理も問題なく動いているようです。


次にデータベース(MySQL)のテーブル内のデータも実行前後で確認してみます。
実行前

```text:MySQL
| 12044 | 2021-10-29          |  22.75 |       318 |         71 |       224 |
| 12054 | 2021-10-29          |  22.27 |       173 |         71 |       224 |
+-------+---------------------+--------+-----------+------------+-----------+
1197 rows in set (9.14 sec)

mysql> 
```

実行後

```text:MySQL
| 12044 | 2021-10-29          |  22.75 |       318 |         71 |       224 |
| 12054 | 2021-10-29          |  22.27 |       173 |         71 |       224 |
| 12064 | 2021-10-30          |  38.38 |       218 |         71 |       224 |
+-------+---------------------+--------+-----------+------------+-----------+
1198 rows in set (9.16 sec)

mysql> 
```
rowの数が1つ増え、新しい日付のデータが追加されています。
送信ボタンがタップすることでデータが送信されてテーブルに反映されていることが確認できますね。


最後に画面上でも確認してみます。

![80106950-8F44-421D-9EC2-3B3E586F1084_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505203/89f4902a-4443-df08-6a69-2c488e159c15.jpeg)
赤枠内の青文字のp要素のデータが更新されていることが確認できました。
ページをリロードする処理が正常に動作し、新しいテキストデータを取得できることが確認できました。


#おわりに
たぶん今回試してみたことが出来ればSeleniumでスクレイピングやテストを行うための基本的な部分はカバーできるかと思います。
あとは記事としてアップするか分かりませんが、マウスオーバーに対する処理やテスト時のUIチェックのための動画撮影等は応用的な感じになるかと思いますが、応用可能だと思います。
ご覧いただきありがとうございました。

#参考サイト
[クローラ作成に必須！XPATHの記法まとめ](https://qiita.com/rllllho/items/cb1187cec0fb17fc650a)
XPathの記法について詳しく記載されています。
要素の指定の際、かなり参考になりました。ありがとうございます。

[Selenium API(逆引き)・・・Selenium APIを利用目的から検索できます](https://www.seleniumqref.com/api/webdriver_gyaku.html)
seleniumの画面操作について細かく記載されていて、かなり参考になりました。ありがとうございます。

[現役シリコンバレーエンジニアが教えるPython 3 入門 + 応用 +アメリカのシリコンバレー流コードスタイル](https://www.udemy.com/share/101tIw3@AK_mL1NPNlZOtOkKOnn4-gD404veZ-efiXpWfxjAQfm_pLtojQiE73ib5Tr6tX90/)
Udemy, Youtube, Twitterで活躍中の酒井潤さんのpython入門。
内容が濃くかつ教え方が丁寧なのですごく分かりやすいです。
pythonについて学びたい方は一度は見た方が良いお勧めの動画です。
pythonの実装で迷った時やコーディングスタイルで迷った時はいつもこの動画で動画で復習しています。ありがとうございます。