こんにちは、けいすけと申します。
今回はDockerコンテナからseleniumを利用してchromeを操作して、スクレイピングに挑戦しようと思います。
seleniumとは
さまざまな用途があると思いますが、私はスクレイピングをする際に利用します。
最近だとページをスクロールした時に次のコンテンツを表示するといったwebページが増えてきました。
このような、JavaScriptを用いた動きのあるページの情報を取得する際にseleniumを用います。
Dockerでの利用について
このseleniumはローカル環境であれば簡単に動作します。
私はPythonによるビジネスに役立つWebスクレイピングというudemy講座で学習しましたが、ビデオ通りに動作しました。
しかしDocker上で動かそうとすると話は別で、簡単には操作できませんでした。
そこで今回はDockerにこだわってseleniumの使い方をご紹介します。
前提知識
下記を使用したことがあると読みやすいです。
- スクレイピング
- Python
- Docker
- docker-compose
- JupyterLab
実装環境
- macOS
seleniumのイメージを起動し、ローカルからアクセス
SeleniumHQ/docker-seleniumというGitHubページにはseleniumのイメージを用いた方法が紹介されています。
ローカルのターミナルappなどで
$ docker run -d -p 4444:4444 -v /dev/shm:/dev/shm selenium/standalone-chrome:4.0.0-beta-1-prerelease-20201208
と、実行するだけで、chrome環境がDockerコンテナ上に構築されます。
そして、同じくローカルでJupyterLabなどを立ち上げて、下記のPythonコードを動かすことでコンテナ上のchromeを操作してスクレイピングを行うことができます。
※)コードの内容は10分で理解する Seleniumから拝借しております。
※)seleniumが入っていない場合は事前に$ pip install selenium
としてseleniumをインストールしておきましょう。
※)掲載の通り、command_executor にhttp://localhost:4444/wd/hub
を指定してアクセスしてください。
from selenium import webdriver
# Chrome のオプションを設定する
options = webdriver.ChromeOptions()
# Selenium Server に接続する
driver = webdriver.Remote(
command_executor='http://localhost:4444/wd/hub',
options=options,
)
# Selenium 経由でブラウザを操作する
driver.get('https://qiita.com')
print(driver.current_url)
# ブラウザを終了する
driver.quit()
https://qiita.com/
と出力されれば接続ができています。
seleniumをDockerコンテナから操作する
上記ではchromeはDockerコンテナ上で動いていましたが、ローカルのJupyterLabからアクセスしていました。
ここではJupyterLabもコンテナ上に構築して、chromeのコンテナを操作してみましょう。
つまり、コンテナからコンテナにアクセスして操作するということです。
ここでDockerのネットワーク機能を知っていただきたいです。
詳しくはDocker Compose入門 (3) ~ネットワークの理解を深める~をご覧ください。
要約すると、Dockerはコンテナを起動する際、ネットワークという空間にコンテナを配置します。
そして、そのネットワーク内であればコンテナ同士で通信ができるのです。
試しにターミナルに$ docker network ls
と入力してみてください。
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
e5dd457767fd bridge bridge local
8836c0e5c268 host host local
acdbf891b515 none null local
このような表示がされるのではないでしょうか?
NAMEがネットワーク名です。デフォルトではbridge、host、noneの3種類のネットワークが存在します。
新たなネットワークを作成してみましょう。
以下のコードを実行します。
$ docker network create mybridge
これでmybridge
というネットワークが作成できました。
確認してみると、mybridgeというネットワーク名が追加されています。
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
e5dd457767fd bridge bridge local
8836c0e5c268 host host local
bc51faf6c0b7 mybridge bridge local
acdbf891b515 none null local
そして、2つのコンテナを起動させる際に、どちらもmybridge
ネットワークに配置するようにすることで相互に通信を行うことができるようになります。
では、例をお見せします。
まず、上記のseleniumのイメージを起動し、ローカルからアクセスの時に登場した$ docker run -d -p 4444:~~~
というコマンドを実行するのですが、そこに--net {network_name}
を追加します。
こうすることでコンテナを起動させる際に指定した{network_name}ネットワーク内にコンテナを配置します。
下記のコマンドでコンテナを起動します。コンテナ名はchrome_driver
という名前を指定しました。
$ docker run -d -p 4444:4444 -v /dev/shm:/dev/shm --net mybridge --name chrome_driver selenium/standalone-chrome:4.0.0-beta-1-prerelease-20201208
では次に、chrome_driver
コンテナを操作するコンテナを、同じくmybridge
ネットワーク内に起動させたいと思います。
私は以下のDockerfileで作成したイメージを利用します。
ubuntuのイメージにAnacondaをインストールしてパスを通し、seleniumをインストールし、最後にJupyterLabを起動するというものです。
FROM ubuntu:latest
RUN apt-get update && apt-get install -y \
sudo \
wget \
vim \
git
WORKDIR /opt
RUN wget https://repo.anaconda.com/archive/Anaconda3-2020.07-Linux-x86_64.sh && \
sh Anaconda3-2020.07-Linux-x86_64.sh -b -p /opt/anaconda3 && \
rm -f Anaconda3-2020.07-Linux-x86_64.sh
ENV PATH /opt/anaconda3/bin:$PATH
RUN pip install selenium
WORKDIR /work
CMD ["jupyter", "lab", "--ip=0.0.0.0", "--allow-root"]
このDockerfileがあるディレクトリで$ docker build .
として、下記コマンドでコンテナを起動します。
コンテナ名はselenium
という名前を指定しました。
※)ポートとマウンティングディレクトリは適当に指定しています。
※)最後のd15d2c3cf86cの部分は、$ docker build .
した際に作成されたイメージIDを指定してください。
docker run -p 2222:8888 -v ~/Desktop/:/work/ --net mybridge --name selenium d15d2c3cf86c
どちらのコンテナもmybridgeネットワークに配置できましたでしょうか?
下記コマンドでmybridgeネットワーク内を確認してみましょう。
$ docker network inspect mybridge
[
{
"Name": "mybridge",
"Id": "04954f265ec2bf1db15b211037ebe345bd4d4421c17f315e80e445232271341b",
"Created": "2020-12-11T05:55:28.1440964Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "192.168.48.0/20",
"Gateway": "192.168.48.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"32ba38def25833c2632f04c560e69718b2505fd60eea728d5485f1474f8478f6": {
"Name": "selenium",
"EndpointID": "a8d9b9fad5810fca32231addfa21057e384833e9167e15da8d10838aa64fdebe",
"MacAddress": "02:42:c0:a8:30:03",
"IPv4Address": "192.168.48.3/20",
"IPv6Address": ""
},
"58089eec577c69c13124328e1b83f68b1da4c8e1d907312c3e91b40996c28fa8": {
"Name": "chrome_driver",
"EndpointID": "165c0a9281ba038782108fd61f7f435986f50f8eac2768025e836beea3ce65cd",
"MacAddress": "02:42:c0:a8:30:02",
"IPv4Address": "192.168.48.2/20",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
]
Containersを見るとselenium
とchrome_driver
という2つのコンテナが存在しています。
では、seleniumコンテナからchrome_driver
コンテナにアクセスして、先ほどと同様のスクレイピングを行いましょう。
ポートは2222に設定したのでlocalhost:2222
からJupyterLabにアクセスできます。
ここで注意です。先ほどはJupyterLabからhttp://localhost:4444/wd/hub
にアクセスしましたが、今回はchrome_driver:4444/wd/hub
もしくは192.168.48.2:4444/wd/hub
にアクセスしてください。
なぜかというとここはローカルではなくDockerコンテナだからです。
上記でネットワークを指定してコンテナを起動させたことにより、selenium
コンテナとchrome_driver
コンテナは同じネットワークに属しています。よってIPアドレスもしくはコンテナ名を指定して通信をすることができます。
コンテナ名とIPアドレスは$ docker network inspect mybridge
でmybridge内を確認した際に表示される上記内容を確認してください。
よってJupyterLab内で実行するのは下記コードになります。
from selenium import webdriver
# Chrome のオプションを設定する
options = webdriver.ChromeOptions()
# Selenium Server に接続する
driver = webdriver.Remote(
command_executor='chrome_driver:4444/wd/hub', # コンテナ名を指定
# command_executor='192.168.48.2:4444/wd/hub', # もしくはIPアドレスを指定
options=options,
)
# Selenium 経由でブラウザを操作する
driver.get('https://qiita.com')
print(driver.current_url)
# ブラウザを終了する
driver.quit()
https://qiita.com/
と出力されれば接続ができています。
これでselenium
コンテナからseleniumを使ってchrome_driver
コンテナを操作してスクレイピングを行うことができました。
Composeを利用する
先ほどはdocker run --net {network_name}
でそれぞれのコンテナを起動しましたが、Composeを利用するとネットワーク名を指定しなくても、自動で同じネットワークにコンテナを配置してくれるので非常に便利です。
例えば下記のようなディレクトリ構成を考えます。
Desktop/
└ test/
├ Dockerfile
├ docker-compose.yml
└ work/
docker-compose.ymlは下記のように定義します。
version: "3"
services:
chrome_driver:
image: selenium/standalone-chrome:4.0.0-beta-1-prerelease-20201208
ports:
- '4444:4444'
volumes:
- '/dev/shm:/dev/shm'
selenium:
build: .
ports:
- '2222:8888'
volumes:
- './work/:/work/'
先ほどと同じように名前をchrome_driver
とselenium
とします。
Dockerfileは先ほどと同じ。
workディレクトリは空です。(※存在するディレクトリを指定しないとちゃんとマウントしてくれません。)
では、testディレクトリで$ docker-compose up --build
を実行します。
うまく起動できたら$ docker network ls
を実行してネットワークを確認してみましょう。
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
94d4705b1f33 bridge bridge local
6ac0e9dbf238 host host local
04954f265ec2 mybridge bridge local
6c6c6ce2e790 none null local
5c64b45dc7fc test_default bridge local
一番下にtest_defaultというネットワークが作成されているのがわかります。
ネットワーク名は{ディレクトリ名_default}となります。
先ほどtestディレクトリでcomposeを動かしたのでtest_defaultというネットワークが作成されたのです。
ではこのネットワーク内を確認します。
$ docker network inspect test_default
[
{
"Name": "test_default",
"Id": "5c64b45dc7fc744f546edbb13da4acc14b28aadffe51b9ed113f3911e8fb286f",
"Created": "2020-12-11T06:58:17.366581Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "192.168.96.0/20",
"Gateway": "192.168.96.1"
}
]
},
"Internal": false,
"Attachable": true,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"07e16ac792bb3361f916f9f451931c81a7aa2624b17cf92fd04ac34003499b32": {
"Name": "test_selenium_1",
"EndpointID": "eca2c1373918979fa0f20d9e2767b5a05715cb615514acec64193eb260e7c7e3",
"MacAddress": "02:42:c0:a8:60:02",
"IPv4Address": "192.168.96.2/20",
"IPv6Address": ""
},
"4d5d5b94087a10bbb19c4b37273e1d3486db98809ad4c1aa67b97269a130d441": {
"Name": "test_chrome_driver_1",
"EndpointID": "9f83480675bf95e74f90000d6bd175ecd6ea940dd2126b0200b7b50cb4b93acc",
"MacAddress": "02:42:c0:a8:60:03",
"IPv4Address": "192.168.96.3/20",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {
"com.docker.compose.network": "default",
"com.docker.compose.project": "test",
"com.docker.compose.version": "1.27.4"
}
}
]
test_defaultネットワーク内にtest_selenium_1
とtest_chrome_driver_1
というコンテナが作成されました。
※)docker-compose.ymlファイルにselenium
とchrome_driver
という名前を指定し、testディレクトリでcomposeを動かしたのでこの名前になりました。
では同じようにlocalhost:2222
にアクセスしてtest_selenium_1
コンテナーのJupyterLabに入りましょう。
そしてJupyterLab内で上記と同じようにtest_chrome_driver_1
コンテナーへ接続を試みます。
from selenium import webdriver
# Chrome のオプションを設定する
options = webdriver.ChromeOptions()
# Selenium Server に接続する
driver = webdriver.Remote(
command_executor='test_chrome_driver_1:4444/wd/hub', # コンテナ名を指定
# command_executor='192.168.96.3:4444/wd/hub', # もしくはIPアドレスを指定
options=options,
)
# Selenium 経由でブラウザを操作する
driver.get('https://qiita.com')
print(driver.current_url)
# ブラウザを終了する
driver.quit()
https://qiita.com/
と出力されれば接続ができています。
Dockerコンテナでスクレイピングしている様子を動画で確認する
ここまでで、Dockerコンテナ上で作業することができるようになったと思います。
しかし、ローカルであれば自動でchromeが立ち上がって実際にスクレイピングされる様子を確認することができると思いますが、Dockerコンテナ上ではそれができません。
そこでGitHubで紹介されている方法を使ってスクレイピングされる様子をmp4形式の動画でダウンロードしてみましょう。
といっても作業は簡単で、紹介されている以下のコードを実行します。
$ docker network create grid
$ docker run -d -p 4444:4444 -p 6900:5900 --net grid --name selenium -v /dev/shm:/dev/shm selenium/standalone-chrome:4.0.0-beta-1-prerelease-20201208
$ docker run -d --net grid --name video -v /tmp/videos:/videos selenium/video:ffmpeg-4.3.1-20201208
# Run your tests
$ docker stop video && docker rm video
$ docker stop selenium && docker rm selenium
ここまで記事を読んでいただいた方であれば、内容は理解できると思いますが注意事項がありますのでご紹介します。
- 基本ですが、
grid
というネットワークを作成し、全てのコンテナをそこに配置します。 -
video
コンテナを起動させる際、マウントするディレクトリは添付の通り/tmp/videos:/videos
でなければなりません。(他のディレクトリを指定すると、mp4ファイルは指定ディレクトリ内に取得できますが、なぜか動画を再生できません。) -
# Run your tests
のところでは今回実装したDockerfileを使ってJupyterLabからアクセスする方法を実行することができます。もちろんネットワークはgrid
で指定してください。 - 最後の2行で
video
コンテナとselenium
コンテナを削除していますが、これをしないとなぜかビデオが再生されません。
以上でDockerを用いたseleniumのご紹介をおわらせていただきます。
これでローカル環境に依存しない環境でスクレイピングを行うことができるようになりました。
そして、ビデオを取得できたことにより、さらにローカル環境に近い環境を実装することができました。
考察
まさかスクレイピングをするためにDockerのネットワーク機能を知ることになるとは思いませんでした。
非常に便利な機能だと思いますので、今後使う機会があれば積極的に使っていきたいと思います。