AWS Lambda(Go)でHTMLのスクリーンショットを撮影して保存する

こんにちは、証券フロンティア事業部の西永です。
今回はAWS LambdaでGo言語を用いてHTMLのスクリーンショットを撮影して保存するところまでをご紹介します。
python(selenium)で良いよねとか言わない

ライブラリについてはsclevine/agoutiを使用します。
(今回は使いませんが、chromedp/chromedpの場合だとchromeのバージョンアップでサイズがでかくなる影響によりS3経由かECRが必須になるみたいです) oedocowboys.sakura.ne.jp

今回の記事について

sclevine/agoutiも長らく更新されておらず、何なら別のライブラリに乗り換えることを推奨とライブラリ自ら書いているのでレガシーな記事となります。

NOTE: Agouti is no longer actively maintained. This was one of my earliest Go projects, and I am no longer happy with the API design. I recommend selecting an alternative WebDriver client for Go.

本題

コード

まずサンプルソースコードの方から。
Lambdaのレイヤーと組み合わせる形で行います。

package main

import (
    "context"
    "encoding/base64"
    "os"

    "github.com/aws/aws-lambda-go/lambda"
    "github.com/sclevine/agouti"
)

func handler(_ context.Context) (string, error) {
    // 素のheadless-chromeだと日本語が表示できないのでフォント参照先変更
    // レイヤーは「/opt/」の中にある
    // /
    // └── opt
    // │   ├── .fonts
    // │   │   └── NotoSansJP-Regular.otf  ※好きなフォント指定
    // │   ├── chromedriver
    // │   └── headless-chromium
    // └── tmp
    if err := os.Setenv("HOME", "/opt/"); err != nil {
        return "", err
    }

    opts := []agouti.Option{
        agouti.Browser("chrome"),
        agouti.ChromeOptions(
            "args", []string{
                // 設定しないとエラーになる
                "--headless",
                "--no-sandbox",
                "--disable-gpu",
                "--single-process",
                // ウィンドウサイズを大きくしたい場合
                // "--window-size=1920,1080",
            },
        ),
        agouti.ChromeOptions(
            // レイヤーのchromiumバイナリ指定
            "binary", "/opt/headless-chromium",
        ),
    }

    // chromium起動
    driver := agouti.NewWebDriver(
        "http://{{.Address}}",
        []string{"/opt/chromedriver", "--port={{.Port}}"},
        opts...)
    if err := driver.Start(); err != nil {
        return "", err
    }
    defer driver.Stop()

    page, err := driver.NewPage()
    if err != nil {
        return "", err
    }
    // ローカルのHTMLを開く場合は「file::///○○.html」等で指定
    if err := page.Navigate("https://www.tecotec.co.jp/"); err != nil {
        return "", err
    }
    // 上記ページのスクリーンショットを撮影して「/tmp/hoge.png」に保存
    // lambdaでは/tmp内でしか自由に取得・保存が行えない
    if err := page.Screenshot("/tmp/hoge.png"); err != nil {
        return "", err
    }

    // 保存した画像を読み取る
    buf, err := os.ReadFile("/tmp/hoge.png")

    // base64で出力(S3に保存する場合はここから書き換えてください)
    return "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf), nil
}

func main() {
    lambda.Start(handler)
}
レイヤー

レイヤーについてはchromedriverとheadless-chromium、fontsの3つをそれぞれ用意します。
chromedriver:chromedriver (古いバージョン)
headless-chromium:serverless-chrome
fonts:Noto Sans Japanese

zip化した際に50MBをオーバーするとS3経由でないと出来なくなるので注意。

今回は
chromedriver:2.37 chromedriver_linux64.zip
headless-chromium: v1.0.0-37 stable-headless-chromium-64.0.3282.167-amazonlinux-2017-03.zip
fonts:Noto_Sans_JP.zip
をそれぞれダウンロード&解凍後、下記のように配置してzipファイルを作成します。

.
├── .fonts
│   └── NotoSansJP-Regular.otf
├── chromedriver
└── headless-chromium

その後、zipファイルを用いてレイヤーを作成。

これでレイヤー側の準備は完了です。

ビルドとデプロイ

templete.yamlについても雑に用意。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Sample Blog Function

Globals:
  Function:
    Timeout: 5
    Tracing: Active

Resources:
  BlogFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: .
      Handler: main
      Layers:
        - !Ref HeadlessChromiumLayer
      Runtime: go1.x
      Architectures:
        - x86_64

  HeadlessChromiumLayer:
    Type: AWS::Serverless::LayerVersion
    name: headless-chromium
    Properties:
      ContentUri: layer/headless-chromium/

全体的なファイル構造としてはこんな感じになります。

.
├──layer       // レイヤー
│   ├── .fonts
│   │   └── NotoSansJP-Regular.otf
│   ├── chromedriver
│   └── headless-chromium
├──go.mod
├──go.sum
├──main.go      // サンプルコード貼り付け
├──Makefile
└──templete.yaml   // 上のtemplete.yaml

この状態から

$ make build

でbuildします。

buildに成功すると.aws-samフォルダが新たに作成され、下記のようなフォルダ構造になります。

.
├──.aws-sam
│   ├──build
│   │   ├──BlogFunction
│   │   │   └──main
│   │   └──template.yaml
│   └──build.toml
├──layer       // レイヤー
│   ├── .fonts
│   │   └── NotoSansJP-Regular.otf
│   ├── chromedriver
│   └── headless-chromium
├──go.mod
├──go.sum
├──main.go      // サンプルコード貼り付け
├──Makefile
└──templete.yaml   // 上のtemplete.yaml

この状態から.aws-sam/build/BlogFunction/mainだけでzipファイルを作成。main.zipを作成します。

そして作成したzipファイルをAWS Lambdaにアップロードしてデプロイします。

デプロイが完了したら、下にスクロールして今度はレイヤーの設定を行います。
「レイヤーの追加」から先程作成したレイヤーを指定します。

レイヤーの指定が終わったら、ランタイム設定の方からハンドラの設定でmainを指定します。

これで準備が完了しました。

最後に、テストタブからそのままテストを実行してみます。
テストに成功するとbase64文字列が出てきます。

この文字列をダブルクォーテーションを除いてアドレスバーにコピペすると、テコテックのホームページが表示できます。

S3に保存する場合はreturnの部分を書き換えてください。
以上で終わりです。
お疲れ様でした。

おわりに

今回は簡略化のために実用的なS3保存ではなくbase64での出力に留めましたが、returnから下の部分を書き換えることでS3保存やSlackへの通知など実用的な使い方にも応用できます。
またagouti+を利用すればseleniumのようにchromeの操作も行えるため、スクレイピングにも活用できます。
「pythonじゃなくてgolangでスクレイピングしたい」となった方はお試しください。


Amazon Web Servicesおよびかかる資料で使用されるその他の AWS 商標 は、米国および/またはその他の諸国における、Amazon.com, Inc. またはその関連会社の商標です。 tecotec.co.jp