Docker ( AmazonLinux2 ) 上で Perl から Headless Chrome を操作する

Perl で Headless Chrome を操作する日本語記事がほぼないし、しかもそれを Docker 内で動かすという例がなかったので書いておくことにする。

全部のファイルと、動かし方は gist にまとめておいた。

https://gist.github.com/takaya1992/6fc6878fb936559344fac068ab6e90f2

抜粋して一部を書いておく。

まずは、Dockerfile

FROM amazonlinux:2

WORKDIR /app

RUN yum update -y \
  && yum install -y perl perl-core perl-App-cpanminus gcc expat-devel \
  && rm -rf /var/cache/yum/* \
  && yum clean all \
  && cpanm Carton

COPY google-chrome.repo /etc/yum.repos.d/google-chrome.repo
RUN yum install -y google-chrome-stable unzip wget lsof ipa-gothic-fonts ipa-mincho-fonts

RUN CHROME_MAJOR_VERSION=$(google-chrome --version | sed -E "s/.* ([0-9]+)(\.[0-9]+){3}.*/\1/") \
  && CHROME_DRIVER_VERSION=$(wget --no-verbose -O - "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_${CHROME_MAJOR_VERSION}") \
  && echo "Using chromedriver version: "$CHROME_DRIVER_VERSION \
  && wget --no-verbose -O /tmp/chromedriver_linux64.zip https://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip \
  && unzip /tmp/chromedriver_linux64.zip chromedriver -d /usr/local/bin/

先に断っておくと、サンプルとしてわかりやすく書いているので、Dockerfile を書く上でのベストプラクティスは守っていない。 前半は、Perl とそれに必要なパッケージのインストールなのでとくに説明はしない。

重要なのは後半の Chrome と ChromeDriver のインストール部分で、Chrome のインストールから説明していく。

Chrome のインストール

まずは、Chromeリポジトリを追加するために設定ファイルをコピーしてくる。google-chrome.repo の中身は以下のようになってる。

[google-chrome]
name=google-chrome
baseurl=http://dl.google.com/linux/chrome/rpm/stable/$basearch
enabled=1
gpgcheck=1
gpgkey=https://dl-ssl.google.com/linux/linux_signing_key.pub

これを /etc/yum.repo.d/ 以下に置くことで、yum のパッケージ検索対象になる。 Chromegoogle-chrome-stable というパッケージ名で登録されている。

残りの Chrome と一緒にインストールしてるパッケージは、次の ChromeDriver のインストールに必要なもの ( unzip, wget )と、Chrome を起動するのに必要なもの ( lsof ) 、Chrome で日本語を表示する際に必要なフォント ( ipa-gothic-fonts, ipa-mincho-fonts ) 。

ChromeDriver のインストール

Chrome を外から操作するために ChromeDriver を使う。
ChromeDriver はブラウザを操作するための WebDriver という仕様に基づいて実装されており、ChromeDriver を起動すると HTTP サーバーが立ち上がり JSON API でブラウザを操作できる。
他のブラウザ、例えば Firefox では geckodriver というドライバーが用意されている。
この WebDriver を使って各ブラウザを操作できるのが Selenium である。

話がそれたけど、ChromeDriver をインストールする。 ChromeDriver は Chrome のバージョンに合ったバージョンをインストールする必要がある。 そのため、 Chrome のバージョンを確認しそのバージョンに対応する ChromeDriver のバージョンを取得し、ダウンロードしている。

Perl から Headless な Chrome を操作する

直接 Docker を実行するのも面倒なので、docker-compose.yml を用意して Docker Compose で操作できるようにしてある。

$ docker-compose build

でビルドして、

$ docker-compose run --rm app /bin/bash

で Docker コンテナ内に入る。

cpanfile を用意しているので Docker コンテナ内で以下のコマンドを実行して必要なパッケージ ( Selenium::Remote::Driver ) をインストールしておく。

$ carton install

以下のスクリプトcarton exec -- perl selenium_chrome_test.pl で実行すると、 Google と表示されれば成功。

use strict;
use warnings;
use utf8;

use feature qw/say/;

use Selenium::Chrome;

# Selenium を介さず直接 chromedriver 経由で Chrome を操作する
my $driver = Selenium::Chrome->new(
    extra_capabilities => {
        'goog:chromeOptions' => {
            args => [ 'headless', 'disable-gpu', 'window-size=1920,1080', 'no-sandbox' ],
        }
    }
);

$driver->get('https://www.google.com');

say $driver->get_title();  # => Google

$driver->shutdown_binary();

Headless な Chrome として実行するポイントは、Selenium::Chrome->new 時のオプションで headless を指定すること。

順番に説明していく。
まず、インストールした Selenium::Remote::Driver にはいくつかのパッケージが含まれていて、 Selenium::Chrome もその一つ。
Selenium::ChromeSelenium を介さずに前述した ChromeDriver に直接アクセスし、API をコールして Chrome の操作を行う。

Selenium::Chrome に渡した goog:chromeOptions は、ChromeDriver へセッションを作成する際に渡され、 goog:chromeOptions 内の args 配列は Chrome 起動時の引数として設定される。
headless を指定するとヘッドレスな Chrome として起動される。
disable-gpuheadless 指定時に追加で指定することが推奨されるオプションである。
window-size はその名の通りウィンドウのサイズを指定している。これは必須ではない。
最後に no-sandboxChromeサンドボックスを無効化するオプション。詳しく調べられていないけど、これをつけないと実行できなかった。セキュリティはゆるくなるので信用できないサイトを開かないように注意する。