PRESSMAN*Tech(プレスマンテック)

東日本橋の制作・開発会社 プレスマンのスタッフブログ

Puppeteer on LambdaでWebページのキャプチャを撮る

Puppeteer on Lambda

AWS Lambdaで遂にNode v8.10を使用できるようになりました。
これでPuppeteerが使える!と意気揚々としていたのですが、いつの間にか必須要件がv6.4.0以上となっていました。以前はv7.10以上必須と書かれていたはずですが・・・

いきなり出鼻を挫かれましたが、気を取り直していきます。

はじめに:Puppeteerとは

Google謹製のGoogleChromeを自動操作するライブラリです。Nodeで動作します。
「このサイトにアクセスして、このボタンをクリックして・・・」といった操作を自動で行うことができます。雑に言うとExcelのマクロみたいなものです。
Chrome59で実装されたヘッドレスモードに合わせる形でプロジェクトが立ち上がり、主にWebアプリの自動化テストの分野で注目されています。
似たプロジェクトにPhantomJSChromelessがあります。

サンプル

サンプルで行っている処理の概要は以下のとおりです。

  1. PRESSMAN*Tech (本サイト)にアクセスし、
  2. 先頭の記事(=最新記事)へのリンクをクリックして、
  3. 記事のページを開いてキャプチャを取り、
  4. S3に保存する
'use strict';

const AWS = require('aws-sdk');
const launchChrome = require('@serverless-chrome/lambda');
const CDP = require('chrome-remote-interface');
const puppeteer = require('puppeteer');

const SAVE_BUCKET_NAME = '<キャプチャを保存するバケット名>';

/**
 * Lambdaハンドラ
 * @param {Object} event Lambdaイベントデータ
 * @param {Object} context Contextオブジェクト
 * @param {function} callback コールバックオプション
 */  
exports.handler = async (event, context, callback) => {
    let slsChrome = null;
    let browser = null;
    let page = null;

    try {
        // 前処理
        // serverless-chromeを起動し、PuppeteerからWebSocketで接続する
        slsChrome = await launchChrome();
        browser = await puppeteer.connect({ 
            browserWSEndpoint: (await CDP.Version()).webSocketDebuggerUrl 
        });
        page = await browser.newPage();

        // ブラウザ操作
        await page.goto('https://www.pressmantech.com', { waitUntil: 'domcontentloaded' });
        // page.click('a[rel="bookmark"]');
        // ↑本来はこうしたい。けどなぜか動作しないのでJavascriptでリンクをクリック
        page.evaluate(() => document.querySelector('a[rel="bookmark"]').click());
        await page.waitForNavigation({ timeout: 30000, waitUntil: 'domcontentloaded' });
        // PDFで日本語フォントを表示するためにWebフォントを強制的に適用させる
        // 注:クロスドメインなiframe内コンテンツは操作できないため豆腐のまま
        await page.evaluate(() => {
            var style = document.createElement('style');
            style.textContent = `
                @import url('//fonts.googleapis.com/css?family=Source+Code+Pro');
                @import url('//fonts.googleapis.com/earlyaccess/notosansjp.css');`;
            document.head.appendChild(style);
            document.body.style.fontFamily = "'Noto Sans JP', sans-serif";

            document.querySelectorAll('pre > code').forEach((el, idx) => {
                el.style.fontFamily = "'Source Code Pro', 'Noto Sans JP', monospace";
            });
        });
        await page.waitFor(1000);
        const jpgBuf = await page.screenshot({ fullPage: true, type: 'jpeg' });
        const pdfBuf = await page.pdf({ printBackground: true });

        // S3に保存
        const s3 = new AWS.S3();
        const now = new Date();
        now.setHours(now.getHours() + 9);
        const nowStr = '' +
            now.getFullYear() + '-' +
            (now.getMonth() + 1 + '').padStart(2, '0') + '-' +
            (now.getDate() + '').padStart(2, '0') + ' ' + 
            (now.getHours() + '').padStart(2, '0') + ':' +
            (now.getMinutes() + '').padStart(2, '0') + ':' + 
            (now.getSeconds() + '').padStart(2, '0');
        const fileName = nowStr.replace(/[\-:]/g, '_').replace(/\s/g, '__');
        let s3Param = {
            Bucket: SAVE_BUCKET_NAME,
            Key: null,
            Body: null
        };

        s3Param.Key = fileName + '.jpg';
        s3Param.Body =  jpgBuf;
        await s3.putObject(s3Param).promise();

        s3Param.Key = fileName + '.pdf';
        s3Param.Body = pdfBuf;
        await s3.putObject(s3Param).promise();

        return callback(null, JSON.stringify({ result: 'OK', createDate: nowStr }));
    } catch (err) {
        console.error(err);
        return callback(null, JSON.stringify({ result: 'NG' }));
    } finally {
        if (page) {
            await page.close();
        }

        if (browser) {
            await browser.disconnect();
        }

        if (slsChrome) { 
            await slsChrome.kill();
        }
    }
};

テスト実行してみます。
Lambda

テスト実行すると以下のようにS3にファイルが作成されます。
S3

また、実際に作成された画像とPDFは以下のようになっています。
キャプチャ画像
キャプチャPDF

ポイント・注意事項

Puppeteerのインストールについて

PuppeteerはデフォルトでインストールするとChromium本体もインストールするようになっています。今回は@serverless-chrome/lambdaを使用するのでインストールしないよう、npm install時にPUPPETEER_SKIP_CHROMIUM_DOWNLOAD=TRUEを環境変数に設定するか、.npmrcに記述しておく必要があります。
仮にインストールしてしまっても動作に支障はないかと思いますが、Lambdaのパッケージサイズ上限に引っかかると思います。

@serverless-chrome/lambdaのインストールについて

npm installする際、バージョンは1.0.0-38を指定してください。
2018/4/9現在、Chromium65ベースの1.0.0-39〜41だと何故かHTMLを取得できないという現象が発生します。

日本語フォントについて

Lambdaのインスタンスには日本語フォントが搭載されていないそうです。
そのため、日本語を含むページをPDFにすると文字化けします。
AWS LambdaでPhantomJS日本語フォント対応のようにインスタンス自体にフォントをインストールする方法もあります(というかこちらが正攻法だと思います)が、今回はページのCSSを操作してWebフォントを適用させることで日本語を表示させています。
ちなみに画像ファイルのキャプチャでは特別な処理なしでも日本語が表示されてました。

さいごに

“Chromeless on Lambda”や”PhantomJS on Lambda”はよく見かけますが、”Puppeteer on Lambda”はあまり見かけなかったので投稿しました。
いかがでしたでしょうか?何かの参考になれば幸いです。

参考

Puppeteer
@serverless-chrome/lambda
AWS LambdaでPhantomJS日本語フォント対応

この記事をシェアする:
投稿者:ヨウスケ
ヨウスケ

ヨウスケ の紹介

プログラマをやってます。
ペーペーです。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

▲ 先頭へ戻る