Goalist Developers Blog

Lambda & PhantomJS & Selenium WebDriver for Java

f:id:t-moritsugu:20170623103856p:plain
こんにちわ、 ゴーリスト開発のモリツグです。
EC2インスタンス100台で毎日クローリングしてたら請求額が洒落にならなくなったので必要にせまられてLambdaでクローラを作ることにしました。
あっさりできるかと思ったのですが、思いのほか手こずったので同じ経験をする人が居なくなることを願ってブログに残しておこうと思います。

今回の課題を列挙してみます
1.Lambda & PhantomJSで複数のフォントを見れるようにする(日本、中国、韓国、タイ、アラビア、ヘブライ)
2.PhantomJSはSelenium WebDriverを使ってJavaから操作する
3.クローリング結果の一時保存以外では/tmp領域は使わない

Lambda & PhantomJSで複数のフォントを見れるようにする 前半

とりあえずリクルートさんのブログが参考になります。
LinuxのPhantomJSはfontconfigに依存しています。
Lambdaにはデフォルトでfontconfigは入ってないので日本語などは文字化けします。
Lambdaでユーザが一時的に自由に扱える領域は/tmpだけです。とりあえず/tmp以下にfontconfigをぶち込めば動くのですが、/tmp以下に同じ名前のディレクトリを作ると衝突することがあるので乱数をつけたり衝突を回避する手段が必要です。
ここで制限つきですが、もう1ヶ所それなりに好き放題できる場所があります。
それが/var/task以下です。Lambdaでとりあえずpwdすると/var/taskが返ってきます。
jarやzipが展開されるディレクトリもここになります。書き込みはできません。(これが制限になります)
したがってここにfontconfigをつっこみます。 私の環境はWindows7だったので、とりあえずVagrantでVirtualBoxにCentOSをインストールしました。
https://github.com/2creatives/vagrant-centos/releases/tag/v6.4.2

せっかくminimalなのに無理やりGUIを入れたせいでちょっと大変でしたが、みなさんはminimalのままで良いと思います。
では先ほどのブログを丸ぱくり参考にして設定をしていきます。

#無心で以下のコマンドを打ちます。  
yum install epel-release  
  
#PhantomJSをインストール
rpm -ivh http://repo.okay.com.mx/centos/6/x86_64/release/okay-release-1-1.noarch.rpm  
yum search all phantomjs  
yum install phantomjs.x86_64  

# 一旦アップデート
yum update

# GUIとかいれてるせいで怒られる
yum update --skip-broken

# 依存ライブラリをインストール
yum install gperf freetype-devel libxml2-devel python-lxml

# ソースを落としてディレクトリ移動
git clone http://anongit.freedesktop.org/git/fontconfig
cd fontconfig

# configure
./autogen.sh --sysconfdir=/var/task/fontconfig/etc --prefix=/var/task/fontconfig/usr --mandir=/var/task/fontconfig/usr/share/man --enable-libxml2

# make & install
make
make install

# 上手くいけばいいが、以下のエラーで怒られた
# ImportError: No module named six.moves
# とりあえずpipを落としてきてインストール
cd ..
curl -kL -O https://bootstrap.pypa.io/get-pip.py
python get-pip.py

# pipをアップデート python2.6早くアップデートしろ!と出てるが一旦無視
# pip install -U pip

# なんかエラーでるけどとりあえず入る
pip install six

# とりあえずmakeしてみたら通った
cd fontconfig
make
make install

Lambda & PhantomJSで複数のフォントを見れるようにする 後半

引き続き、最初に紹介したブログを丸パクリ参考に設定を進めていきます。
/var/task/fontconfig/etc/fonts/local.confというファイルを以下の内容で作成します。

<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
  <dir>/var/task/fontconfig/usr/share/fonts</dir>
</fontconfig>

続いて、/var/task/fontconfig/usr/share/fontsというディレクトリを作成し、 IPAフォントとかを突っ込んでおきます。

mkdir /var/task/fontconfig/usr/share/fonts/
curl http://dl.ipafont.ipa.go.jp/IPAexfont/ipaexg00301.zip > ipaexg00301.zip
unzip ipaexg00301.zip
cp ipaexg00301/ipaexg.ttf /var/task/fontconfig/usr/share/fonts/

ここまででPhantomJSで無事に日本語が表示できるかを確認します。
以下のようなファイルを作成しcap.jsという名前で保存してください。

var page = require('webpage').create();
page.open('http://developers.goalist.co.jp/entry/2017/06/23/120000', function () {
    window.setTimeout(function () {
        page.render('debug.png');
        phantom.exit();
    }, 5000);
});

フォントキャッシュを再構築して実行

/var/task/fontconfig/usr/bin/fc-cache -fs
LD_LIBRARY_PATH=/var/task/fontconfig/usr/lib/ phantomjs cap.js

日本語以外のフォントも見れるようにする

基本は yum search thai font とかして出てきたなかで一番それっぽいフォントを入れます。 あんまり色々なフォントを入れるとLambdaのデプロイ上限の250Mを超えます。
yum installすると/usr/share/fonts/にインストールされるので/var/task/fontconfig/usr/share/fontsにコピーしましょう。
私は本当に大丈夫か確認するためにコピー後にyum removeし、消えないことがあるので/usr/share/fonts/以下の該当フォントをrmで削除しました。
以下は今回入れたフォント一覧です。

韓国 baekmuk-ttf-gulim-fonts.noarch
タイ thai-scalable-kinnari-fonts.noarch
中国(繁体) cjkuni-ukai-fonts.noarch
中国(簡体) wqy-microhei-fonts.noarch
アラビア amiri-fonts.noarch
ヘブライ culmus-aharoni-clm-fonts.noarch

LambdaでPhantomJSをSelenium WebDriverを使ってJavaから操作する

以下は主にJava向けの内容になります。
どの言語にも共通する重要なポイントは「/var/task/libはデフォルトでLD_LIBRARY_PATHに含まれる」という所だと思います。AWSの中のイケメンがやってくれたようです。

今回はeclipseにawsのプラグインを入れてLambdaプロジェクトを作りました。
このあたりは詳しくは説明しませんが、S3とかを扱う目的でaws-java-sdkを入れるとそれだけでLambdaに乗らなくなる可能性があるので、aws-java-sdk-coreとaws-java-sdk-s3だけ入れるみたいな努力が必要です。
Seleniumライブラリ、fontconfigと各言語のフォント、PhantomJSのバイナリを入れるので結構カツカツになります。
適当なフォルダ(今回はdriverという名前)を作ってビルドパスのソースに含めます。

f:id:t-moritsugu:20170623022716p:plain

画像のようにdriverには以下の3つを配置します。
・/var/taskにあるfontconfigディレクトリ
・PhantomJSバイナリ
・LD_LIBRARY_PATHで指定していた/var/task/fontconfig/usr/lib/をlibとして配置

/var/task/libがLD_LIBRARY_PATHにデフォルトで含まれることを利用して/var/task/fontconfig/usr/lib/以下のファイルをそこに配置する目的で置いています。
/var/task/fontconfig/usr/lib/pkgconfig/fontconfig.pcの中とかを見ると絶対パスで色々書かれてるので大丈夫ぽいです。
あとはeclipseの機能でエクスポートから実行可能JARファイルを作成すればいいのですが、ライブラリ形式は一番上の「生成されるJARに必須ライブラリーを抽出」を選んでください。

あとはこれをLambdaにデプロイすればすべて上手く。。。行きません。

以下の2つの問題をクリアしないといけません。
1.PhantomJSや/var/task/fontconfig/usr/bin/fc-cacheに実行権限が無いため怒られる、さらにJARにした段階(antによるjar作成)でパーミッションが引き継がれず事前に実行権限を付与していても実行権限が無くなってしまう。
2.デフォルトでphantomjsdriver.logがカレントディレクトリに作成されるが/var/taskには書き込めないので怒られる。

1については私の環境がWindowsでそもそも実行権限を付与できなかったため、VirtualBoxにCentOSとeclipseをいれました。しかし何故かKDEデスクトップとの相性が悪いせいかJAR作成画面が固まって作れなかったため、Windowsで実行可能JARをつくるときにantファイルを保存してそれを利用するというちょっと特殊なことを行ったので、もしかしたら不正確かもしれません。 ただ、Windowsユーザは事前に実行権限を付与する術がないためLinux環境は必須です。
以下のようなスクリプトを作成し、antファイルを一部書き換えることで無理やり実行権限を引継げるようにしました。肝はfilesetは実行権限を設定できないけれど、zipfilesetはfilemodeで設定できる所だとおもいます。期待通りに固められているかは拡張子をzipとかにして中身を見れば良いと思います。

antの前にゴニョゴニョするスクリプト

#!/usr/bin/bash
cd /work/workspace/Hoge/bin
zip -r bin.zip *
cd /work
ant -buildfile hoge.xml

antファイル hoge.xml

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project default="create_run_jar" name="Create Runnable Jar for Project Hoge">
    <!--this file was created by Eclipse Runnable JAR Export Wizard-->
    <!--ANT 1.7 is required                                        -->
    <target name="create_run_jar">
        <jar destfile="/work/dest/Hoge.jar" filesetmanifest="mergewithoutmain">
            <manifest>
                <attribute name="Main-Class" value="com.goalist.hoge.LambdaFunctionHandler"/>
                <attribute name="Class-Path" value="."/>
            </manifest>
        <!-- 権限を引き継ぐために一旦事前のスクリプトでzipにしてそっちを使う
            <fileset dir="/work/workspace/Hoge/bin"/>
        -->
        <zipfileset filemode="755" src="/work/workspace/Hoge/bin/bin.zip"/>
            (.....libraryのjar系の記述なので省略.....)
        </jar>
    </target>
</project>

2については–webdriver-logfileパラメータがどうやってもPhantomJSに渡らない(実行時にコンソールでパラメータを確認できる)ので以下のような方法しかなさそうです。

Runtime.getRuntime().exec(new String[]{"/var/task/fontconfig/usr/bin/fc-cache", "-fs"})

String phantomJsBinaryPath = "/var/task/phantomjs"; 
String userAgent = "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.41 Safari/535.1";

DesiredCapabilities capabilities = DesiredCapabilities.phantomjs();  
capabilities.setJavascriptEnabled(true);  
capabilities.setCapability(  
  PhantomJSDriverService.PHANTOMJS_EXECUTABLE_PATH_PROPERTY,
  phantomJsBinaryPath
);

capabilities.setCapability(
  PhantomJSDriverService.PHANTOMJS_PAGE_SETTINGS_PREFIX + "userAgent", userAgent);

File logfile = new File("/dev/null");
PhantomJSDriverService pjsds = new PhantomJSDriverService.Builder()
  .usingPhantomJSExecutable(new File(phantomJsBinaryPath))
  .usingAnyFreePort()
  //.withProxy(proxy)                       
  //.usingCommandLineArguments(commandLineArguments)
  .withLogFile(logfile)
  .build();

return new PhantomJSDriver(pjsds, capabilities);

やっとLambda上で快適にクローリングできます!

まとめ

Javaじゃなかったら最後の方の問題は関係ないと思います。
この作業中に仮想環境が2回ぐらい壊れて再インストールしたので物凄く疲れました。
Lambdaは夢が広がるので面白いなぁと思います。