ネットで調べてもあまり正解に辿り着けなかったので。
書き殴りなので、すみませんが動作保証はしないです。
やりたいこと
タイトル通りですが、AWS LambdaでWebスクレイピングをしたい。
諸事情あって言語はRuby2.7になります。
RubyでのスクレイピングにはCapybara + selenium-webdriver を利用します。
LambdaのデプロイにはServerless Frameworkを使うので、この記事はServerlessについての知識を事前条件としています。
Capybaraやselenium-webdriverの説明はしないので、ググってくだしあ。
注意
Ruby2.5はサポート終了が宣告されているので今からやるにはRuby2.7一択です。
実は今現在(この記事が公開された日)ネット上に溢れている「Lambda(Ruby)でスクレイピングする」系の記事はほとんどがRuby2.5だったりするので、注意しましょう。
後述しますが、Ruby2.5とRuby2.7ではランタイムのOSが異なるので、Chromeを動かすために必要な条件が大分異なります。
ディレクトリ構成
こんな感じの構成で、Lambda Layerをデプロイするディレクトリを作成します。
それぞれのファイルを個別に説明していきます。
また、package.jsonとか .dockerignoreなどは省略していますが、実際には勿論あります。
chromedriver/
├── Dockerfile
├── build
│ ├── .fonts
│ │ ├── fonts.conf
│ │ └── ipaexg.ttf
│ ├── bin
│ ├── lib
│ └── ruby
│ └── lib
│ └── setup_cabybara.rb
└── serverless.yml
serverless.yml
こんな感じで。
service: chromedriver
plugins:
- serverless-hooks-plugin
custom:
hooks:
before:deploy:deploy:
- docker build -t chromedriver .
- docker run --rm -v "$PWD"/build:/opt chromedriver
provider:
name: aws
runtime: ruby2.7
region: ap-northeast-1
timeout: 900
layers:
chromedriver:
path: build
compatibleRuntimes:
- ruby2.7
functionsがないですが、これだけもLambda Layerだけをデプロイして他のサービスから参照することができます。
serverless-hooks-pluginはserverlessコマンドの前後に任意の処理を挟むことができるプラグインです。
ここでは sls deploy
コマンドを実行した時に、後述のDockerfileを使ってchromeの動作に必要なバイナリやライブラリを /build/bin
と /build/lib
に出力しています。
Dockerfile
Dockerのプロではないので、変なところがあるかもしれませんがお許しを。
FROM lambci/lambda:build-ruby2.7
WORKDIR /tmp
RUN yum install -y unzip && \
curl -SL https://chromedriver.storage.googleapis.com/2.43/chromedriver_linux64.zip > chromedriver.zip && \
curl -SL https://github.com/adieuadieu/serverless-chrome/releases/download/v1.0.0-55/stable-headless-chromium-amazonlinux-2017-03.zip > headless-chromium.zip && \
unzip chromedriver.zip && \
unzip headless-chromium.zip
RUN yum install -y libX11
CMD cp /tmp/chromedriver /opt/bin/ && \
cp /tmp/headless-chromium /opt/bin/ && \
# chromium
cp /usr/lib64/libexpat.so.1 /opt/lib/ && \
cp /usr/lib64/libuuid.so.1 /opt/lib/ && \
# chromedriver
cp /usr/lib64/libglib-2.0.so.0 /opt/lib/ && \
cp /usr/lib64/libX11.so.6 /opt/lib/ && \
cp /usr/lib64/libxcb.so.1 /opt/lib/ && \
cp /usr/lib64/libXau.so.6 /opt/lib/
ここでは、以下2点を行っています。
- ChromeのバイナリとChromeの操作に必要なchromedriverをダウンロード
- Lambda Ruby2.7で動かすために不足しているネイティブのライブラリをローカルにコピー
chromeとchromedriverのバイナリをダウンロード
当然Lambdaの環境にはChromeはインストールされていないので、自分で実行バイナリをパッケージしてデプロイしないといけないわけですが、公式のバイナリだとサイズがデカすぎてLambda Layerのサイズ制限(250MB)に引っかかってデプロイできません。
そこで、サイズを小さくしてビルドしたものを配布してくれている方を見つけたので、そちらを利用させてもらっています。(感謝。。。!)
なお、releasesを見るとv1.0.0-57 が最新で、Ruby2.7のOSであるAmazon Linux 2向けにビルドされているようですが、自分が試したところ何故か動かなかったので少し前の v1.0.0-55 を利用しています。
また、rubyからChromeを操作するためにchromedriverもダウンロードしています。(こちらは公式から)
chromedriverのバージョンはChromeのバージョンに合わせたものを使う必要があるので注意しましょう。(参考:https://chromedriver.chromium.org/downloads/version-selection)
ネイティブのライブラリをローカルにコピー
Ruby2.5はAmazon Linuxで動くのですが、Ruby2.7からはAmazon Linux 2 になるため、インストール済みのパッケージやそれの共有ライブラリが大きく異なります。というか大分少ないです。
なので、ネイティブライブラリの依存を自分で解決して必要なものをバイナリと一緒にパッケージしないといけないわけです。
これがネットで探してもなかなか出てこなくて、lddコマンド打ったりしてハマったところですが、経緯はともかくとして、上記のライブラリたちがあればなんとか動作しました。
また、ネイティブライブラリの取得のためにlambci/lambda:build-ruby2.7のイメージを利用していますが、今は公式のイメージが公開されているらしいのでそちらの方がベターかもしれません。(未検証)
fonts.conf と ipaexg.ttf
ここまで動くだろうと思いきや、まだ足りないものがあります。フォントファイルです。
ネット上だとこれがないと文字化けするという記事も見ましたが、自分の場合何故かそもそもChromeが起動しなかったです。
フォントの設定はよくわかってないですが、とりあえずLambda Layerがデプロイされる /opt
に .fontsを置いておきます。
fonts.conf
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<dir>/opt/.fonts/</dir>
<cachedir>/tmp/fonts-cache/</cachedir>
<config></config>
</fontconfig>
ipaexg.ttfはこちらの文字情報技術促進協議会さんからダウンロードしてます。
setup_cabybara.rb
これはLambda Layerに含める必要はないですが、Layerにしておけば利用するLambda側で require 'setup_cabybara'
するだけで、Capybaraの設定が完了します。
require 'capybara'
require 'selenium-webdriver'
ENV['FONTCONFIG_FILE'] = '/opt/.fonts/fonts.conf'
Capybara.register_driver :chrome_headless do |app|
version = Capybara::Selenium::Driver.load_selenium
options_key = Capybara::Selenium::Driver::CAPS_VERSION.satisfied_by?(version) ? :capabilities : :options
browser_options = ::Selenium::WebDriver::Chrome::Options.new.tap do |options|
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument("--disable-gpu")
options.add_argument("--window-size=1280x1696")
options.add_argument("--single-process")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--homedir=/tmp")
end
browser_options.binary = '/opt/bin/headless-chromium'
driver_path = '/opt/bin/chromedriver'
service = Selenium::WebDriver::Service.chrome(path: driver_path)
Capybara::Selenium::Driver.new(app,
browser: :chrome,
service: service,
options_key => browser_options)
end
Capybara.javascript_driver = :chrome_headless
Lambda Layerは実行環境の/opt
に配置されるので、これまで用意してきたchrome、chromedriver、フォントそれぞれのパスを設定しています。
デプロイ
sls deploy
すればDockerから出力した諸々と一緒にデプロイされます。
Lambda Layer利用側
疲れたので、Lambda Layerの利用方法自体は公式ドキュメント参照で。。。
rubyコードはこんな感じで使えます。
capybara と selenium-webdriverのgemはインストール前提です。
require 'setup_cabybara'
session = Capybara::Session.new(:chrome_headless)
終わりに
書き殴ったので、動作保証はしないです。(2度目)
余裕ができたら動作確認できたサンプルをgithubで公開したいですね。