生産性のない話

趣味の範囲でサイバーセキュリティの話

Docker上でGoogle Chromeを使ったWebクローラを作る

最近Webサイトのクローリング(スクレイピング?)に興味を持ちまして、Webサイトのクローラ的なものを作りたいと思い、いろいろ試行錯誤していました。

Webサイトのコンテンツを取得するなら、一番簡単なものだとwgetcurl、ちょっと手を加えるならスクリプトを書く、Pythonならrequestsやmechanizeなどのライブラリを使うと、比較的単純なことなら簡単にできます。 より高度なことをやろうとすると、やはり実際のブラウザを利用するのが一番でしょう。

昨年末にこちらの記事を読みまして、

qiita.com

Docker上でGoogle ChromeをHeadlessモードで動かし、Seleniumで制御してスクレイピングしようという記事です。

とても面白そうだったのですが、自分の欲しい機能にあと一歩というところでした。

これにmitmproxyを組み合わせれば、幸せになれそうだったので、試してみました。

mitmproxy - an interactive HTTPS proxy

mitmproxyというのは、その名の通り、MITM(Man In The Middle)を行うプロキシです。 暗号化された通信に割り込んで、盗聴、改ざんを行う攻撃手法ですが、このテクニック自体は攻撃に限らず色々な場面で利用されています。

mitmproxyを利用すると、HTTPS(HTTP)の通信の中身を覗き見たり、書き換えたりが可能になります。

コンテンツを取得するだけであればそんなことをする必要はないのですが、これができると色々と便利なので、何とか組み込みたいと思いました。

で、さっそく、上の記事にある内容をトレースしつつ、mitmproxyを挟んでみたのですが、結論を言うとダメでした。 できました。

mitmproxyを利用するとき、「ブラウザ⇔mitmproxy⇔Webサイト」という形で通信が流れますが、ブラウザとmitmproxyの間はmitmproxyが発行する自己証明書が利用されます。こちらはブラウザから見ると不正な証明書になります。そこで、Chromeに「--ignore-certificate-errors」というオプションを付けてやることで、不正な証明書のエラーを無視して通常通りアクセスできるようになるはずです。

ですが、なぜかChromeをHeadlessモードで動かすと、このあたりのオプションが正常に動かないらしく、Chrome Headlessでmitmproxyを動かすのは現状難しそうという結論に至りました。

もう少ししたら、Chrome Headlessでも正常に動くようになるかもしれないので、そこは気長に待つことにします(あるいは僕の知らない別のオプションがあるのかもしれませんが、どなたか知っていたら教えていただきたい……)。

追記(2018/5/15)

個人のプロファイルにルート証明書をちゃんと読み込ませてやると、証明書のエラーがでなくなり、うまくいきました。 一番下に追記しています。

ということで、今回はChrome Headlessを使うことは諦めて、普通のGoogle ChromeをDockerから動かすことを考えました。

unskilled.site

こういう方法を使ってDocker内のGUIアプリケーションを起動させることができるようです。Linuxでリモートから(ここではローカルで動くコンテナ)のX Window Systemを許可してUNIXソケット経由でディスプレイを共有する方法です。

この方法を使ってGoogle ChromeをインストールしたコンテナでChromeを起動させると見事にChromeが立ち上がり、普通のGUIアプリケーションとして利用可能となりました。

f:id:blueBLUE:20180509211449p:plain

つまり、わざわざHeadlessモードを使わなくとも、GUIアプリケーションとしてDocker内で起動させることもできるということです。

ということで、Chrome Headlessは無視してとりあえず、GUIアプリケーションとして動作するGoogle Chromeのコンテナイメージを作ってみました。

インストールしたもの:

一応日本語環境で利用することを想定して日本語関係のフォントやパッケージも入れました。

本当はイメージを分けて作った方が後々やりやすいのかもしれませんが、mitmproxyの起動時に作成される自己証明書をChromeが入っているコンテナにインストールする必要があり、いろいろと面倒だったので、ひとまず全部一つのコンテナイメージにぶち込みました(イメージサイズが1.5Gくらいになってます……)。

出来上がったDockerfileがこちらになります。

github.com

起動時に色々やることがあるので、docker-entrypoint.shが色々面倒なことになっています。

docker-composeでそのまま起動すると、普通にGoogle Chromeが起動するはずです。mitmproxyのログはそのまま標準出力に流れていきます。 (追記 : 今はHeadlessでGoogleにアクセスするようになっています。GUIで利用する場合はdocker-compose.ymlと、実行スクリプトを書き換えるなどする必要があります)

ただし、X Window Systemの認証を許可するために、ホスト側で以下のコマンドを実行しておく必要があります。

xhost local:

認証を許可してそのままにしておくのはセキュリティ的によろしくないので、終わったら戻しておきましょう。

xhost -local:

Chromeを自動で動かしたい場合はscriptsフォルダ内に「crawling-script.sh」を作成すると、そこに記載されているコマンドを実行します。 例として、特定のサイトにアクセスするスクリプトを実行します。

  • scripts/crawling-script.sh
#!/bin/sh

python3 /home/chrome/scripts/crawling.py
  • scripts/crawling.py
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from datetime import datetime
from time import sleep

if __name__ == '__main__':

    url = 'https://www.google.co.jp/'

    outputdir = '/home/chrome/output/'
    timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")

    options = webdriver.ChromeOptions()
    options.binary_location = '/usr/bin/google-chrome'
    options.add_argument('--no-sandbox')
#    options.add_argument('--headless')
    options.add_argument('--disable-gpu')

    options.add_argument("--start-maximized")

    options.add_argument('--ignore-certificate-errors')
    options.add_argument('--ssl-protocol=any')
    options.add_argument('--allow-running-insecure-content')
    options.add_argument('--disable-web-security')

    PROXY = 'localhost:8080'
    options.add_argument('--proxy-server=http://%s' % PROXY)

    LANGUAGE = 'ja'
    options.add_argument('--lang=%s' % LANGUAGE)

    capabilities = options.to_capabilities()

    driver = webdriver.Remote(command_executor='http://localhost:9515', desired_capabilities=capabilities)

    try:
        driver.get(url)
        sleep(3)
        print('title:', driver.title)
        driver.save_screenshot(outputdir + 'screenshot-' + timestamp + '.png')
    finally:
        driver.quit()

これが置いてある状態で「docker-compose up」すると、サイトにアクセスしてスクリーンショットを撮って終了します。

f:id:blueBLUE:20180509211734p:plain

「--headless」のオプションを付けると、Headlessモードで動きますが、先に述べた通りの理由でHTTPSのサイトではmitmproxyのエラーのために正常にアクセスできません。 アクセス先がHTTPのみのサイトであれば、問題はないですが。

次にscriptsフォルダに「mitmproxy-script.py」を作成します。例として、アクセス先のレスポンスからhtmlファイルをすべて保存するスクリプトです。 (こちらのサイトを参考にしました)

  • scripts/mitmproxy-script.py
#!/usr/bin/env python

from mitmproxy import io
from mitmproxy.exceptions import FlowReadException

outputpath = '/home/chrome/output/'

def response(flow):
    content_type = flow.response.headers.get('Content-Type', '')
    path = flow.request.url.replace('/', '_').replace(':', '_')
    if content_type.startswith('text/html'):
        with open(outputpath + path, 'w') as f:
            f.write(flow.response.text)

これで起動すると、outputフォルダに保存されたダウンロードしたhtmlファイルが保存されているのを確認できます。

f:id:blueBLUE:20180509212120p:plain

mitmproxyのサンプルはこちらにあります。

こんな感じでmitmproxyとseleniumスクリプトを書いて、うまくやれば好きなWebサイトに好きなようにアクセスした際の通信を解析できるようになる、と思います。 スクリプトについては今後作っていこうかと思います。 まあ、seleniumもmitmproxyも割とサンプルはあるので書きやすいのではないかなー、と。

とにかくWebサイトの解析用のクローラを作ることはできたので、満足はしました。ただ、Headlessで動かせなかったのは残念でした。 なにかしら方法はありそうなので、もうしばらく調べてみたいです。 あと、FirefoxにもHeadlessモードが搭載されているそうなので、いずれはそっちも試してみたいです。

最後に、Webサイトのクローリング、スクレイピングはマナーを守って行うようお願いします。

追記(2018/5/15)

headlessモードでうまく動作したので、追記です。 ルート証明書をちゃんとインストールすると、エラーがなくなりました。 docker-entrypoint.shの中でコンテナの起動時にやるようにしています(ので、コンテナ起動に若干余計な時間がかかります)。

まず、OSに証明書を認識させます(ここまでは前回やっていた)。

openssl x509 -in /home/chrome/.mitmproxy/mitmproxy-ca-cert.pem -inform PEM -out /home/chrome/.mitmproxy/mitmproxy-ca-cert.crt
mkdir /usr/share/ca-certificates/extra
cp /home/chrome/.mitmproxy/mitmproxy-ca-cert.crt /usr/share/ca-certificates/extra/
echo 'extra/mitmproxy-ca-cert.crt' >> /etc/ca-certificates.conf 
update-ca-certificates

chromeを一度起動させると、デフォルトのプロファイルが作成されます。 すると、ホームディレクトリの「.pkiディレクトリ配下に証明書を関連のデータベースができるので、ここにcertutilコマンドを使ってmitmproxyの証明書をインストールします。

for certDB in $(find /home/chrome/ -name "cert9.db")
do
  prefdir=$(dirname ${certDB});
  echo ${prefdir};
  certutil -A -n ${certname} -t "TCu,Cu,Tu" -i ${certfile} -d sql:${prefdir}
done

これで、headlessモードでchromeが動いてくれるようになりました。

ちなみに、Firefox版も作りました。こちらも同じように動いてくれるかと思います。 (Firefox版を作っていて証明書に気づいた)

これで幸せになれそうです。

参考

Dockerで手軽にスクレイピング環境を手に入れる - Qiita

Dockerコンテナの中でGUIアプリケーションを起動させる | Unskilled?

mitmproxyを使ってどんなサイトでもクローリング・スクレイピングする - Qiita

Making Chrome Headless Undetectable

AmazonLinuxでselenium + chromedriver + headlessするメモ - 私事ですが……