25
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Dockerコンテナからseleniumを使ってスクレイピング

こんにちは、けいすけと申します。

今回は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を見るとseleniumchrome_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は下記のように定義します。

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_driverseleniumとします。
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_1test_chrome_driver_1というコンテナが作成されました。
※)docker-compose.ymlファイルにseleniumchrome_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のネットワーク機能を知ることになるとは思いませんでした。
非常に便利な機能だと思いますので、今後使う機会があれば積極的に使っていきたいと思います。

参照

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
25
Help us understand the problem. What are the problem?