EC2/Ubuntuにヘッドレスブラウザを入れてWebページの画面キャプチャツールを作ってみる

## はじめに
Chrome/Chromiumは最近ヘッドレスに対応しています。ヘッドレスは簡単に言えば画面を表示せずブラウザのエンジン部分だけを実行するようなモードで、GUIを有効にしていないVMでもコマンドラインや他のプログラムから起動・操作でき、自動テストや画面キャプチャなどに便利なツールです。今までは私はPhamtomJSを使ってきたのですが、今後の潮流はこちらに移るようですので試しにheadless chrome/chromiumの動作を確認しながらサンプルアプリを作ってみました。

## 作るサンプルアプリの仕様
```
http://ホスト名:8888/?width=1000&height=600&&url=http%3A%2F%2Fwww.yahoo.co.jp%2F
```
こんな感じで呼ぶと、指定されたページのスクリーンショットをPNGで返してくれるアプリを作ることにします。ブラウザでこのURLを叩くとPNGが表示されるイメージです。ページはURLのQuery Stringで与え、w, hのパラメータが返す画像のサイズ（それぞれ幅・高さ、単位はpixel）、スクリーンショットを撮りたいページのURLはURLエンコードした上で、urlパラメータに渡します。
サンプルアプリですので、セキュリティ、パフォーマンス、同時接続性などは考慮されていません。あくまで原理確認用のサンプルアプリです。

## 材料
Node.js上で動くPuppeteerを使ってヘッドレスモードのChromeを制御します。また今回はRESTのリクエストに答えて動作させるため、同じくNode.js上のフレームワークのexpressを使います。

## UbuntuのVMを準備
今回はAWSのこのマシンイメージを使いました。
"Ubuntu Server 16.04 LTS (HVM), SSD Volume Type - ami-bec974d8"

## Chromeのインストール
あらかじめGoogleのChromeのサイトの「Chrome for Linux をダウンロード」のぺージから、64bit版のdebianパッケージをダウンロードしておきます（この記事書いている時は`google-chrome-stable_current_amd64.deb`というファイル名でダウンロードできています）。そして出来立てホヤホヤのUbuntuのVMにsshでログインし以下を実行します。

```
sudo apt-get update
sudo apt-get install libappindicator1 gconf-service libasound2 libgconf-2-4 libnspr4 libxss1 fonts-liberation libnss3 xdg-utils
sudo dpkg -i google-chrome-stable_current_amd64.deb
```

エラーが出たらエラーメッセージを頑張って見ましょう。多分依存ライブラリが足りません。頑張ってそれを含むパッケージを見つけて追加インストールしてから再度チャレンジします。再度チャレンジする場合は、一旦`sudo apt-get remove google-chrome-stable`を実行して、半端にインストールされたchromeを取り除いてから再度`dpkg -i`を実行するのが良いようです（[参考](http://robotics4society.com/2016/06/16/ubuntu-chrome/)）。

無事インストールできたら以下のコマンドで起動できます。

```
/opt/google/chrome/chrome --user-data-dir="./user-data-dir" --headless --disable-gpu --remote-debugging-port=9222 https://www.google.com
```

## 動作確認（しなくてもいいけど）
上記の起動の例では、Remote debugging port 9222で起動していますので、ローカルマシンから```http://localhost:9222```でブラウザを使って接続すれば、GUIで動作確認できますが、そもそもAWS上のインスタンスでheadlessのブラウザ立てようとしている訳なのでVNCとかセットアップしたくないですよね。ポートむやみに開けたくないかもしれないし。ということでsshのポートフォワードでアクセスしてみます。

一回sshを抜けて、以下で入り直します。

```
ssh -i ~/.ssh/mykey.pem -L 9999:localhost:9222 ubuntu@54.xx.yy.zz
```
または、`~/.ssh/config`に以下のエントリを書いておいて、`ssh headless`でログインします。

```
Host headless
  HostName 54.xx.yy.zz
  Port 22
  User ubuntu
  GatewayPorts yes
  LocalForward 9999 localhost:9222
  IdentityFile ~/.ssh/mykey.pem
```
「54.xx.yy.zz」「/.ssh/mykey.pem」にはご自身のホストのIPアドレスやkey fileへのパスに書き換えてください。

この設定でsshにログインしたままブラウザで、```http://localhost:9999```にアクセスするとリモートのUbuntuの9222に転送され、「Inspectable WebContents」（すなわちデバッグ可能な画面一覧）の画面が表示されます。その下に開いている画面（タブ）の数だけリンクがあると思いますので、そのリンクを押すと、headlessのchromeでアクセスしている画面とそのDeveloper Tools画面が表示されます。

## 日本語フォントのインストール

多分、日本語フォントが正しく表示されず、四角（いわゆる豆腐）が表示されていますよね？
AWSのUbuntuイメージは最初から日本語フォントが入っていないようなので、以下でインストールします。

```
sudo apt-get update
sudo apt-get install fonts-ipafont-gothic fonts-ipafont-mincho
```

## node.jsとnpmのインストール
ここは、[こちら](https://qiita.com/seibe/items/36cef7df85fe2cefa3ea)の手順をそのまま実行させていただきました！

```
sudo apt-get update
sudo apt-get install nodejs npm
sudo npm cache clean
sudo npm install n -g
sudo n stable
sudo ln -sf /usr/local/bin/node /usr/bin/node
sudo apt-get purge -y nodejs npm
```
インストールできたかは```node -v```で確認できます。

## yarnのインストール

[公式サイト](https://yarnpkg.com/lang/en/docs/install/#linux-tab)の手順そのままです！

```
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update
sudo apt-get install yarn
```

## expressのインストール

```
yarn add express
```


## puppetterのインストール
```
yarn add puppeteer
```

## サンプルプログラムの実装

以下を適当なファイル名（例えば sample.js）でセーブしてください。

```
const puppeteer = require('puppeteer');
const express = require('express');
const fs = require('fs');
const PORT = 8888;
const DEFAULT_WIDTH = 800;
const DEFAULT_HEIGHT = 600;
const DEFAULT_URL = 'http://www.google.com/'

var app = express();

app.get('/', function(req, res){

  (async() => {
    // クエリパラメータから、w, h, URLを取得（なければデフォルトを使う）
    var w = parseInt(req.query.width ? req.query.width : DEFAULT_WIDTH, 10);
    var h = parseInt(req.query.height ? req.query.height : DEFAULT_HEIGHT, 10);
    var url = req.query.url ? decodeURIComponent(req.query.url) : DEFAULT_URL;

    // ブラウザを起動し、新規ページを生成
    const browser = await puppeteer.launch();
    const page = await browser.newPage();

    // ページサイズをセットし、指定されたURLをロード
    await page.setViewport({width: w, height: h, isMobile: true});
    await page.goto(url, {waitUntil: 'networkidle0'});

    // カレントディレクトリにpage.pngでスクリーンショットをセーブし、ブラウザを終了させる
    await page.screenshot({path: 'page.png', fullPage: false});
    browser.close();
    console.log("browser closed");

    // カレントディレクトリにセーブしたpage.pngをバイナリで返却する
    var img = fs.readFileSync('./page.png');
    res.writeHead(200, {'Content-Type': 'image/png' });
    res.end(img, 'binary');
  })();
});

app.listen(PORT);
console.log(`Server running at http://localhost:${PORT}/`);
```

## 実行

```node sample.js```で実行します。
あとは、手元のブラウザから、
```http://ホスト名:8888/?width=1200&height=400&url=http%3A%2F%2Fwww.yahoo.co.jp%2F```
などと叩いて見てください。ポート番号を空けておく（AWSだとSecurity GroupでTCP/8888のinboundを許可しておく）のを忘れずに。

## おまけ：もう一つのChrome(Chromium)のインストール方法
[こちら](https://www.chromium.org/getting-involved/download-chromium)を参考にした方法です。
まず依存ライブラリをインストールします。unzipは下記のインストーラ（update.sh）内で必要です。

```
sudo apt-get install unzip libgtk-3-0 libnss3-dev libxss1 libgconf-2-4 libasound2
```
次に、latestのchromiumのダウンロードとインストールです。

```
git clone https://github.com/scheib/chromium-latest-linux
cd chromium-latest-linux
./update.sh
```

起動は以下のコマンドで。

```
./latest/chrome --user-data-dir="~/user-data-dir" --headless --disable-gpu --remote-debugging-port=9222 https://www.yahoo.com
```

## 参考サイト
- [Getting Started with Headless Chrome](https://developers.google.com/web/updates/2017/04/headless-chrome)（[日本語のページ](https://developers.google.com/web/updates/2017/04/headless-chrome?hl=ja)より情報量が多いです。）
- [PuppeteerのAPI仕様書](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md)
- [Ubuntuに最新のNode.jsを難なくインストールする](https://qiita.com/seibe/items/36cef7df85fe2cefa3ea)
- [Ubuntu 16.04 にGoogle Chromeをインストール](http://robotics4society.com/2016/06/16/ubuntu-chrome/)