[Python] Dockerコンテナでseleniumを使ってみる

#はじめに
急遽業務でSereniumを使いスクレイピングをすることになったので、勉強がてらDocker + Python + Sereniumを使いブラウザを操作することにチャレンジしてみます。

#Dockerでsereniumを実行してみる
Dockerコンテナはその仕様上、ホスト内の他のから完全に隔離されたプロセスになります。
なので複数のコンテナ間で通信する時はdocker networkの環境を整えてあげる必要があります。
まず先にdocker networkを構築することから始めます。

###Docker network環境の構築
まずはホストのdocker network環境を構築します。
selenium実行コンテナとブラウザ起動用リモートサーバーコンテナ間での通信が必要なため、docker networkの設定します。

まずはdocker network lsコマンドで現状のネットワーク設定を確認。

```bash
ssablo-no-MacBook-Pro:test_selenium yasuhiro$ docker network ls
NETWORK ID     NAME                         DRIVER    SCOPE
c351541e3f57   bridge                       bridge    local
0ee4e6220144   host                         host      local
ae8ad6a8a4cd   none                         null      local
```

docker network createコマンドでこれから作成するコンテナ同士をブリッジするネットワークを作成します。

```bash
ssablo-no-MacBook-Pro:test_selenium yasuhiro$ docker network create selenium
045d85e9c2bbeb4325842016bfe7dffce7f2c4a043f580501cbbab32b9410980
```

作成できたか確認してみます。

```bash
ssablo-no-MacBook-Pro:test_selenium yasuhiro$ docker network ls
NETWORK ID     NAME                         DRIVER    SCOPE
c351541e3f57   bridge                       bridge    local
0ee4e6220144   host                         host      local
ae8ad6a8a4cd   none                         null      local
045d85e9c2bb   selenium                     bridge    local
```

seleniumという名前のネットワークが出来たので、新しくコンテナをrunする際のオプションに--netで指定してあげればOKです。

これでコンテナ作成前の準備はOKです。
あと上記作業はdocker-composeを使用し、docker-compose.yml内でネットワーク設定するなら不要です。


###リモートサーバー用のコンテナを作成する
それではブラウザのドライバーが入ったリモートサーバーを構築します。(今回はChrome)

Selenium公式が出しているDocker imageを取得します。(URL: https://github.com/SeleniumHQ/docker-selenium)
他のブラウザを使用する場合はそれに合わせてimageを取得してください。


今回は手順を間違えてしまい、既に本ホストにはselenium/standalone-chromeイメージがあるので、それを指定します。
(本来は、 docker run -d -p 4444:4444 --net selenium --shm-size="2g" selenium/standalone-chrome:4.0.0-20211013 )

```bash
ssablo-no-MacBook-Pro:test_selenium yasuhiro$ docker run -d -p 4444:4444 --net selenium --name chromedriver --shm-size="2g" selenium/standalone-chrome
Unable to find image 'selenium/standalone-chrome:latest' locally
latest: Pulling from selenium/standalone-chrome
Digest: sha256:02a72f2c7f412da0ce720a0924786d3236f52f51f87fded2aacc0d6ccbebfbd4
Status: Downloaded newer image for selenium/standalone-chrome:latest
a0b073f2e4f4f8ebf1b681edcf3afc310d145af5d6dd8c777be771640dc89650
```

ネットワークにselenium, コンテナ名にchromedriverを指定しておきました。

コンテナが作成されているか確認します。

```bash
ssablo-no-MacBook-Pro:test_selenium yasuhiro$ docker ps -a
CONTAINER ID   IMAGE                        COMMAND                  CREATED         STATUS                      PORTS                                                  NAMES
3bcf93a9c90b   selenium/standalone-chrome   "/opt/bin/entry_poin…"   7 seconds ago   Up 6 seconds                0.0.0.0:4444->4444/tcp, :::4444->4444/tcp, 5900/tcp    chromedriver
```

リモートサーバーコンテナの起動が確認出来ました。


docker networkも確認してみます。
docker network inspectコマンドで指定したネットワークの内容を確認出来ます。

```bash
ssablo-no-MacBook-Pro:test_selenium yasuhiro$ docker network inspect selenium
[
    {
        "Name": "selenium",
        "Id": "045d85e9c2bbeb4325842016bfe7dffce7f2c4a043f580501cbbab32b9410980",
        "Created": "2021-10-16T02:29:50.1126456Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.21.0.0/16",
                    "Gateway": "172.21.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "3bcf93a9c90b070a1fc3bc894445b4ffcd9ceeee9b8b0beb91a68be0e5facb52": {
                "Name": "chromedriver",
                "EndpointID": "5048b1365398572dc046781b22b1ef44652b2dafa5c9b48bda656f99445508b6",
                "MacAddress": "02:42:ac:15:00:02",
                "IPv4Address": "172.21.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]
```

先程作成したchromedriverコンテナがseleniumネットワークに設定されipアドレスも付与されています。
あとはseleniumを実行するコンテナを作成し、同ネットワークに設定してあげればコンテナ間で通信できます。


###serenium実行用のコンテナを作成する
次にselenium実行用コンテナを作成しpython環境を構築します。

Dockerfileを記述します。

```text:Dockerfile
FROM ubuntu:latest

RUN apt-get update && apt-get install -y \
    python3 \
	python3-pip

RUN pip3 install --upgrade pip \
	selenium 

WORKDIR /work/
```

このDockerfileをビルドし、imageを作成します。

```bash
ssablo-no-MacBook-Pro:test_selenium yasuhiro$ docker build . -t selenium
```
そしてdocker runでコンテナを作成します。
その際、--netオプションでネットワーク名を指定してあげるのを忘れないようにしてください。

```bash
ssablo-no-MacBook-Pro:test_selenium yasuhiro$ docker run -it -v ~/Desktop/test_selenium/:/work/ -p 5000:5000 --net selenium --name selenium selenium bash
```

コンテナが起動したらdocker network inspectコマンドでネットワーク内を確認します。

```bash
ssablo-no-MacBook-Pro:test_selenium yasuhiro$ docker network inspect selenium
[
    {
        "Name": "selenium",
        "Id": "045d85e9c2bbeb4325842016bfe7dffce7f2c4a043f580501cbbab32b9410980",
        "Created": "2021-10-16T02:29:50.1126456Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.21.0.0/16",
                    "Gateway": "172.21.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "3bcf93a9c90b070a1fc3bc894445b4ffcd9ceeee9b8b0beb91a68be0e5facb52": {
                "Name": "chromedriver",
                "EndpointID": "5048b1365398572dc046781b22b1ef44652b2dafa5c9b48bda656f99445508b6",
                "MacAddress": "02:42:ac:15:00:02",
                "IPv4Address": "172.21.0.2/16",
                "IPv6Address": ""
            },
            "9f672ec558d818d03c07af23749746d86498ae994ff9802e398492bc5a60e14a": {
                "Name": "selenium",
                "EndpointID": "e4867f107ba4a845f691bf6b95e56f7e3522d3879f2336cf06d3a54ad21eae88",
                "MacAddress": "02:42:ac:15:00:03",
                "IPv4Address": "172.21.0.3/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]
```
selenium実行用コンテナも設定されていますね。
これでこのコンテナ同士は通信できるようになりました。

さっそくコンテナに入りpingコマンドで疎通確認してみます。

```bash
ssablo-no-MacBook-Pro:test_selenium yasuhiro$ docker exec -it selenium bash
# ここからコンテナ環境
root@9f672ec558d8:/work# ping 172.21.0.2
bash: ping: command not found
```

まさかのpingが無い
ということでpingをインストールします。

```bash
root@9f672ec558d8:/work# apt-get install iputils-ping net-tools
Reading package lists... Done
Building dependency tree       
Reading state information... Done
~ 長いので省略 ~
Setting up iputils-ping (3:20190709-3) ...
Processing triggers for libc-bin (2.31-0ubuntu9.2) ...
```

これでpingが使えるはず。

```bash
root@9f672ec558d8:/work# ping 172.21.0.2
PING 172.21.0.2 (172.21.0.2) 56(84) bytes of data.
64 bytes from 172.21.0.2: icmp_seq=1 ttl=64 time=0.772 ms
64 bytes from 172.21.0.2: icmp_seq=2 ttl=64 time=0.221 ms
64 bytes from 172.21.0.2: icmp_seq=3 ttl=64 time=0.204 ms
64 bytes from 172.21.0.2: icmp_seq=4 ttl=64 time=0.109 ms
64 bytes from 172.21.0.2: icmp_seq=5 ttl=64 time=0.162 ms
64 bytes from 172.21.0.2: icmp_seq=6 ttl=64 time=0.202 ms
64 bytes from 172.21.0.2: icmp_seq=7 ttl=64 time=0.214 ms
64 bytes from 172.21.0.2: icmp_seq=8 ttl=64 time=0.200 ms
^C
--- 172.21.0.2 ping statistics ---
8 packets transmitted, 8 received, 0% packet loss, time 7190ms
rtt min/avg/max/mdev = 0.109/0.260/0.772/0.196 ms
```
ちゃんと疎通していますね。
試しにコンテナ名でもpingを送ってみます。

```bash
root@9f672ec558d8:/work# ping chromedriver
PING chromedriver (172.21.0.2) 56(84) bytes of data.
64 bytes from chromedriver.selenium (172.21.0.2): icmp_seq=1 ttl=64 time=2.40 ms
64 bytes from chromedriver.selenium (172.21.0.2): icmp_seq=2 ttl=64 time=0.208 ms
64 bytes from chromedriver.selenium (172.21.0.2): icmp_seq=3 ttl=64 time=0.189 ms
64 bytes from chromedriver.selenium (172.21.0.2): icmp_seq=4 ttl=64 time=0.168 ms
^C
--- chromedriver ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3047ms
rtt min/avg/max/mdev = 0.168/0.741/2.401/0.958 ms
```
コンテナ名でも疎通確認が取れました。
seleniumを実行する環境は整ったので、次はseleniumを実行するスクリプトを記述していこうと思います。

###selenium実行のスクリプトを作成する
python環境も構築できたのでseleniumを実行するスクリプトを記述していきます。
今回はdocker_selenium.pyを作成します。
まずはお約束のgoogle.comにアクセスしてみます。

```python:docker_selenium.py
from selenium import webdriver

# webdriverのChromeOptionsインスタンスを作成し変数に格納
options = webdriver.ChromeOptions()

print('Connect remote browser...')

# 接続先のリモートサーバーをipアドレスで指定
driver = webdriver.Remote(command_executor='172.21.0.2:4444/wd/hub', options=options)

# https://google.comにアクセスし、URLを出力する
driver.get('https://google.com')
print('current URL: ', driver.current_url)

# webdriverのRemoteインスタンスを終了する
driver.quit()
```

実行してみます。

```bash
root@9f672ec558d8:/work# python3 docker_selenium.py 
Connect remote browser...
current URL:  https://www.google.com/
``` 

seleniumがブラウザを起動してウェブサイトにアクセスして画面要素を取得するので若干時間はかかりますが、ちゃんとChromeが起動して https://www.google.com/ にアクセスして、その結果がdriverオブジェクトに格納されているようです。

今回はseleniumから接続する接続先リモートサーバーをipアドレスで指定しました。
次にコンテナ名でも接続できるか試してみます。

```python:docker_selenium.py
from selenium import webdriver

options = webdriver.ChromeOptions()

print('Connect remote browser...')

# 接続先のリモートサーバーをコンテナ名で指定
driver = webdriver.Remote(command_executor='http://chromedriver:4444/wd/hub', options=options)

driver.get('https://google.com')
print('current URL: ', driver.current_url)

driver.quit()
```
実行してみます。

```bash
root@9f672ec558d8:/work# python3 docker_selenium.py 
Connect remote browser...
current URL:  https://www.google.com/
```
正常に動作していますね。
ちなみにコンテナ名指定の際、僕の環境ではコンテナ名の前にhttp://をつけないとエラーになりました。参考サイトではつけなくても動作しているようですが、違いはよく分かっていません。。。
もし分かる方いたら教えて頂ければありがたいです。

###seleniumでスクレイピングしてみる
それでは実際に自分のサイトを使いseleniumで色々試してみます。
まずは下記の処理を試してみます。
①webサイトのトップページにアクセスする

![A2C1EB4F-25FD-4251-9808-025FA2AB8C25_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505203/c647f94b-f946-8b5c-f3d8-270a77d8ae8c.jpeg)



②自転車の項目のボタン要素のオブジェクトを生成する

![6FD2D458-BD6E-4886-A250-E5CD737A0696_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505203/9b09e804-4935-ef99-0ebf-02aa36ccbb74.jpeg)


③ボタンオブジェクトのclick()メソッドを実行する

④自転車ページに遷移するので、遷移先のURLを取得する

![2220DDF1-A608-4B4F-ABA0-C867B49CE549_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505203/24f18f52-8533-d1d9-893e-06d2a381bffb.jpeg)

それではコーディングしていきます。　

```python:docker_selenium.py
from selenium import webdriver
from selenium.webdriver.common.by import By

options = webdriver.ChromeOptions()

print('Connect remote browser...')

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

driver.get('https://xxxxxxxx.herokuapp.com/')
print('current URL: ', driver.current_url)
# print('page source: ', driver.page_source)

# idからinputタグ要素を識別し、対象の要素のオブジェクトを生成
bicycle_button = driver.find_element(By.XPATH, '//input[@id="button_bicycle"]')
print('bicycle_button: ', bicycle_button)

# 上で生成したオブジェクトのclick()メソッドを実行
bicycle_button.click()
print('current URL: ', driver.current_url)
# print('page source: ', driver.page_source)

driver.quit()
```
サイトのURLは一部非表示にしています。
seleniumでchromeを起動し、webサイトにアクセスするところまでは前回と同じです。
今回は要素のid属性を元にinput要素のオブジェクトを生成し、そのオブジェクトでclick()メソッドを実行し、ページ遷移をしています。

実行してみます。

```bash
root@9f672ec558d8:/work# python3 docker_selenium.py 
Connect remote browser...
current URL:  https://xxxxxxxx.herokuapp.com/
bicycle_button:  <selenium.webdriver.remote.webelement.WebElement (session="6973aacc54c2656088622c00f68eac22", element="4c284153-2db6-4482-846d-e5f4d93c2dec")>
current URL:  https://xxxxxxxx.herokuapp.com/bicycle_contents
```
出力された値の最終行で遷移先のURLを取得できましたので、①から④の工程が正常に処理されました。

それでは次に、
⑤として、
・Top画面の要素のtextを取得する

![06275632-C4AC-4532-A77C-F9616FA5E3B8_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505203/703f3d9a-5ab0-61af-e3da-3ded9cb4ecf5.jpeg)


・遷移先画面の要素のテキストを2つ取得する 

![E8174B2B-4B99-4E48-8F84-E49748FECFE5_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/505203/5ac44c94-bdc9-60b2-2cb7-4453ba6e8f23.jpeg)


を追加して、要素内のテキストの取得を試してみます。

```python:docker_selenium.py
from selenium import webdriver
from selenium.webdriver.common.by import By


options = webdriver.ChromeOptions()

print('Connect remote browser...')

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

driver.get('https://xxxxxxxx.herokuapp.com/')
print('current URL: ', driver.current_url)

# Topページのp要素のテキストを取得する処理
top_page_message = driver.find_element(By.XPATH, '//*[@id="top_block"]/p').get_attribute('textContent')
print('top_page_message: ', top_page_message)

# 自転車の項目のbutton要素のオブジェクトを生成し、click()メソッドを実行する
bicycle_button = driver.find_element(By.XPATH, '//input[@id="button_bicycle"]')
bicycle_button.click()
print('current URL: ', driver.current_url)

# 遷移先の自転車ページのp要素のテキストを2つ取得する
bicycle_page_message_1 = driver.find_element(By.XPATH, '//*[@id="top_block"]/p[3]').get_attribute('textContent')
print('bicycle_page_message_1: ', bicycle_page_message_1)

bicycle_page_message_2 = driver.find_element(By.XPATH, '//p[@id="sum_milage"]').get_attribute('textContent')
print('bicycle_page_message_2: ', bicycle_page_message_2)


driver.quit()
```
要素内のテキストを取得するには要素のオブジェクトを生成し、get_attribute('textContent')メソッドを実行します。

それでは実行してみます。

```bash
root@9f672ec558d8:/work# python3 docker_selenium.py 
Connect remote browser...
current URL:  https://xxxxxxxx.herokuapp.com/
top_page_message:  Welcome To Training Log Application
current URL:  https://xxxxxxxx.herokuapp.com/bicycle_contents
bicycle_page_message_1:  This page is to your bicycle training logs.
bicycle_page_message_2:  
```
Topページのp要素のテキストと自転車ページの1つ目のp要素のテキストは取得できていますが、2つ目が取得できていません。


この2つ目のp要素のテキストはhtmlファイルがブラウザに読み込まれた時にサーバーから値を取得して表示しているデータです。(他の2つはHTML内にハードコーディングしている。)
なのでseleniumがページ遷移したタイミングではまだ値の取得が行われていないと思われます。

では、⑤遷移先の要素のテキストを取得する 処理をする前に、ファイルの読み込みが完全に終了するまで待つ処理を加えてみます。
python標準ライブラリのtimeをimportして一定時間処理を停止する処理を加えてみます。

```python:docker_selenium.py
import time

from selenium import webdriver
from selenium.webdriver.common.by import By


options = webdriver.ChromeOptions()

print('Connect remote browser...')

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

driver.get('https://xxxxxxxx.herokuapp.com/')
print('current URL: ', driver.current_url)

# Herokuのインスタンスが立ち上がるのを待つ時間
time.sleep(30)
print('30 secound passed...')
# time.sleep(30)

top_page_message = driver.find_element(By.XPATH, '//*[@id="top_block"]/p').get_attribute('textContent')
print('top_page_message: ', top_page_message)


bicycle_button = driver.find_element(By.XPATH, '//input[@id="button_bicycle"]')
# print('bicycle_button: ', bicycle_button)

bicycle_button.click()
print('current URL: ', driver.current_url)

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

bicycle_page_message_1 = driver.find_element(By.XPATH, '//*[@id="top_block"]/p[3]').get_attribute('textContent')
print('bicycle_page_message_1: ', bicycle_page_message_1)

bicycle_page_message_2 = driver.find_element(By.XPATH, '//p[@id="sum_milage"]').get_attribute('textContent')
print('bicycle_page_message_2: ', bicycle_page_message_2)


driver.quit()
```
top_page_messageを取得する直前のtime.sleep(30)はHerokuサーバー用の処理です。
Herokuの仕様上、無料プランの場合は一定時間サーバーへアクセスが無いとインスタンスが落とされてしまうので、サーバーへアクセスした際にインスタンスが立ち上がるのを待つ処理になってます。

実行してみます。

```bash
root@9f672ec558d8:/work# python3 docker_selenium.py 
Connect remote browser...
current URL:  https://xxxxxxxx.herokuapp.com/
30 secound passed...
top_page_message:  Welcome To Training Log Application
current URL:  https://xxxxxxxx.herokuapp.com/bicycle_contents
3 secound wait...
bicycle_page_message_1:  This page is to your bicycle training logs.
bicycle_page_message_2:  今月は 1576.54 km 走っています。
```

今度は値が取れました。
スクレイピングする際はこの辺りもちゃんと考えないといけないですね。

#おわりに
初めてSeleniumを使ってみましたが便利ですね。
今回はスクレイピングのようなことをしてみましたがseleniumはwebアプリのテストでもよく使用されているようなので時間を見つけてそちらも試してみたいと思います。

###Chromeが立ち上がらないことについて
今回seleniumを使いchromeを起動してサイトにアクセスしていましたが、結果の確認はターミナルでしか行えませんでした。
参考にしたサイトや動画ではブラウザが立ち上がりweb画面でも確認が出来ているようですが僕の環境ではブラウザが立ち上がりませんでした。。。
Dockerを使用しているからか設定に問題があるからか切り分けが出来ていないので、解決したらまたQiitaにアップしようと思います。

#参考サイト
[10分で理解する Selenium](https://qiita.com/Chanmoro/items/9a3c86bb465c1cce738a)
Dockerでseleniumを実装する方法をかなり詳細に記述しています。かなり参考にいたしました。ありがとうございます。

[Dockerコンテナからseleniumを使ってスクレイピング](https://qiita.com/kei0919/items/f6f696169c92c936374c)
上記参考サイトを簡単に実行しています。こちらもかなり参考になりました。ありがとうございます。

[クローラ作成に必須！XPATHの記法まとめ](https://qiita.com/rllllho/items/cb1187cec0fb17fc650a)
XPATHを記述する際、大変参考になりました。ありがとうございます。

[Docker Compose入門 (3) ～ネットワークの理解を深める～](https://knowledge.sakura.ad.jp/23899/)
Dockerのネットワーク構成について参考にしました。ありがとうございます。

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