#はじめに
急遽業務でSereniumを使いスクレイピングをすることになったので、勉強がてらDocker + Python + Sereniumを使いブラウザを操作することにチャレンジしてみます。
#Dockerでsereniumを実行してみる
Dockerコンテナはその仕様上、ホスト内の他のから完全に隔離されたプロセスになります。
なので複数のコンテナ間で通信する時はdocker networkの環境を整えてあげる必要があります。
まず先にdocker networkを構築することから始めます。
###Docker network環境の構築
まずはホストのdocker network環境を構築します。
selenium実行コンテナとブラウザ起動用リモートサーバーコンテナ間での通信が必要なため、docker networkの設定します。
まずはdocker network lsコマンドで現状のネットワーク設定を確認。
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コマンドでこれから作成するコンテナ同士をブリッジするネットワークを作成します。
ssablo-no-MacBook-Pro:test_selenium yasuhiro$ docker network create selenium
045d85e9c2bbeb4325842016bfe7dffce7f2c4a043f580501cbbab32b9410980
作成できたか確認してみます。
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 )
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を指定しておきました。
コンテナが作成されているか確認します。
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コマンドで指定したネットワークの内容を確認出来ます。
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を記述します。
FROM ubuntu:latest
RUN apt-get update && apt-get install -y \
python3 \
python3-pip
RUN pip3 install --upgrade pip \
selenium
WORKDIR /work/
このDockerfileをビルドし、imageを作成します。
ssablo-no-MacBook-Pro:test_selenium yasuhiro$ docker build . -t selenium
そしてdocker runでコンテナを作成します。
その際、--netオプションでネットワーク名を指定してあげるのを忘れないようにしてください。
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コマンドでネットワーク内を確認します。
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コマンドで疎通確認してみます。
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をインストールします。
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が使えるはず。
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を送ってみます。
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にアクセスしてみます。
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()
実行してみます。
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アドレスで指定しました。
次にコンテナ名でも接続できるか試してみます。
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()
実行してみます。
root@9f672ec558d8:/work# python3 docker_selenium.py
Connect remote browser...
current URL: https://www.google.com/
正常に動作していますね。
ちなみにコンテナ名指定の際、僕の環境ではコンテナ名の前にhttp://をつけないとエラーになりました。参考サイトではつけなくても動作しているようですが、違いはよく分かっていません。。。
もし分かる方いたら教えて頂ければありがたいです。
###seleniumでスクレイピングしてみる
それでは実際に自分のサイトを使いseleniumで色々試してみます。
まずは下記の処理を試してみます。
①webサイトのトップページにアクセスする
②自転車の項目のボタン要素のオブジェクトを生成する
③ボタンオブジェクトのclick()メソッドを実行する
④自転車ページに遷移するので、遷移先のURLを取得する
それではコーディングしていきます。
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()メソッドを実行し、ページ遷移をしています。
実行してみます。
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を取得する
・遷移先画面の要素のテキストを2つ取得する
を追加して、要素内のテキストの取得を試してみます。
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')メソッドを実行します。
それでは実行してみます。
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して一定時間処理を停止する処理を加えてみます。
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の仕様上、無料プランの場合は一定時間サーバーへアクセスが無いとインスタンスが落とされてしまうので、サーバーへアクセスした際にインスタンスが立ち上がるのを待つ処理になってます。
実行してみます。
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
Dockerでseleniumを実装する方法をかなり詳細に記述しています。かなり参考にいたしました。ありがとうございます。
Dockerコンテナからseleniumを使ってスクレイピング
上記参考サイトを簡単に実行しています。こちらもかなり参考になりました。ありがとうございます。
クローラ作成に必須!XPATHの記法まとめ
XPATHを記述する際、大変参考になりました。ありがとうございます。
Docker Compose入門 (3) ~ネットワークの理解を深める~
Dockerのネットワーク構成について参考にしました。ありがとうございます。
現役シリコンバレーエンジニアが教えるPython 3 入門 + 応用 +アメリカのシリコンバレー流コードスタイル
Udemy, Youtube, Twitterで活躍中の酒井潤さんのpython入門。
内容が濃くかつ教え方が丁寧なのですごく分かりやすいです。
pythonについて学びたい方は一度は見た方が良いお勧めの動画です。
pythonの実装で迷った時やコーディングスタイルで迷った時はいつもこの動画で動画で復習しています。ありがとうございます。