Webサイトのスクリーンショット取得、CLIのやつと、WEB APIで取得できるコンテナとを作ってみた

# 概要

このエントリでは、Webサイトのスクリーンショットを以下2つの方法で取得するための仕組みを作ってみたので、ご紹介します。

- CLI
- WEB API

ざくっと動かす方法と、作りの上で気づいたところについて記載します。

## 対象

- [GoogleChrome/Pupeteer](https://github.com/GoogleChrome/puppeteer) (=ヘッドレスで動作できてスクリプトから操作できる素敵なブラウザを操るためのプロジェクト)は知っているし大体使い方イメージできるので、あとは動いている何かを使いたい方
- チャットサービスなどでURLを貼り付けると、そのすぐ下あたりにWebのサムネイルとちょっとした説明なんかが出てくるけれども、あれ作るとしたらどうやるんだっけ、と思ったことがある方

## 前提

- CLIとして使いたいときは、新し目の、macOS(MojaveとかHigh Sierra)/Linux(で、新し目のNodeとChromiumを動かすのに不自由しないもの)/Windows(10 Proでは試しました。7でもいけると思いますが試してません)が使えること
- コンテナとして使いたいときは、Dockerコンテナを動かす環境があること

## 以下を前提としています

- Node 10.x系の新しいもの
- npm 5.2以降 (npxを使わないのであれば古くても問題ないです)

## 関連サイト

- このエントリで扱っている内容に関するGitHubのリポジトリ [https://github.com/hrkt/take-ss-pupeteer](https://github.com/hrkt/take-ss-pupeteer)
- 上記をコンテナにしたもの[https://cloud.docker.com/u/hrkt/repository/docker/hrkt/take-ss-pupeteer](https://cloud.docker.com/u/hrkt/repository/docker/hrkt/take-ss-pupeteer)
- 元にしたコンテナの話：「[コマンドをサクッとREST風にHTTPで呼び出すサンプルをpandocに使ってコンテナを作ってみた](https://qiita.com/hrkt/items/f2872520efcb1e506019)」


# TL;DR

## CLI

### インストール

```console
git clone https://github.com/hrkt/take-ss-pupeteer
nom install
```

### 実行

```console
npx ./src/take-ss -o example.com.png http://www.example.com
```

数秒後、実行したカレントディレクトリ以下に、example.com.pngというPNGファイルができているはずです。

v0.1.1でのオプションは以下の通り。ブラウザの表示は、サムネイル

```console
Usage: take-ss [options]

Options:
  -v, --version                     output the version number
  -vw, --viewport-width <int>       Viewport(width) of the screenshot (default: 1200)
  -vh, --viewport-height <int>      Viewport(height) of the screenshot (default: 800)
  -o, --output-filename <filename>  filename of the screenshot (default: "out.png")
  -j, --output-as-json              output result as JSON, base64 encoded image and meta data (default: false)
  -h, --help                        output usage information
```

## WEB API

### 起動

```console
docker run -p 8080:8080 hrkt/take-ss-pupeteer:latest
```

### curlでアクセス

```console
curl "http://localhost:8080/api/takess?http://www.example.com"
```

#### 出力

数秒後、以下のようなJSONが返却されます。

```
{"base64Img": "xxxxxxxxxx(base64にエンコードされた文字列)===",
"description":"(METAタグ内のdescription)",
"title":"TITLEタグ内のタイトル"
}
```

base64Imgは、HTML内で下記のように用いるイメージです。

```
<img src="data:image/png;base64,(ここに貼り付け)">
```

###補足

サーバで動くときには、レスポンスとしてJSONイメージを返すだけとし、サーバ条に画像ファイルを作る機能は動かないようにしています。(Dockerfileから環境変数を渡して、スクリプト内でファイル保存モードを無効化)

（なんちゃって記事ではありますが、万が一そのまま使ってしまう場合を恐れ、一応、ファイルで膨れ上がったり、イヂワルなファイル名で目的外のファイルを上書きしたりはしにくいようにしてます）

# 各論: CLI

## 前提

- ヘッドレスでWebをキャプチャする方法はいろいろありますが、2019年の時点で開発元が手厚くて筋が良さそうなPupeteerを使っています。
- Pupeteerは、Nodeで動かす前提だったので、Nodeを使っています。

## 使ったもの

- CLIとして動かすため、コマンドラインのオプション処理が必要ですが、[Commander.js](https://github.com/tj/commander.js/)を使いました。必要なだけの機能が見通しよく提供されていることと、検索してみてパッとみた感じ、使われてそうなものだったので。

## 作り

- [ソースを見ていただければ](https://github.com/hrkt/take-ss-pupeteer/blob/master/src/take-ss.js)わかりますが、1つのJavaScriptスクリプトで作ってあり、そんなたいしたことはしていません。
- PupeteerのTutorialと比べると、以下のいくつかのところでは工夫しました。

brwoserオブジェクトは、try-catch-finallyでcloseするようにしてます。
これは、実際使ってみると様々な理由でブラウザがクラッシュしますが、その時もそこそこ礼儀正しく終了できるようにするためです。

```
} finally {
     await browser.close()
}
```

Pupeteerをlaunchするときに、Chromiumのオプションをいくつか渡しますが、エントリ執筆時点では、下記のようなものを渡しています。最後の「'--disable-dev-shm-usage'」は、とあるバージョンまではコンテナ内で動かしていると共有メモリが足りなくて落ちちゃっているケースもあったんだけれども、それはファイルとして作ることにして回避することにする、というオプションです。（コンテナでなく普通に手元OSで動かす分には出会わないと思います）

```
args: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage']
```



# 各論: REST風コンテナ

どうせだったら、コマンドラインで動かすだけでなく API的に動いてもいいよね、という発想の元、筆者の別エントリの「[コマンドをサクッとREST風にHTTPで呼び出すサンプルをpandocに使ってコンテナを作ってみた](https://qiita.com/hrkt/items/f2872520efcb1e506019)」を使いました。

基本的な話は上記エントリに書いた通りなのですが、コンテナ内でPupeteerを動かすため、以下を実施しています。

## とりあえずDockerのベースを使う

```dockerfile
FROM hrkt/command-as-a-service:latest
```

基本的には、このコンテナをFROMにするだけで、任意のパスのコマンドをREST API風に呼び出せます。

## AlpineでChromiumを動かすため、いろいろ入れる。

これは、Pupeteerサイトにある「[Troubleshooting](https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md)」で開設されているものをそのまま使ってます。
(pupeteerパッケージが自動で落としてくれたり、pupeteer-coreパッケージと自分でapk add chromiumしたものでは、2019年9月時点ではうまくいきませんでした。)

```dockerfile

RUN apk add --no-cache \
      udev \
      chromium \
      nss \
      freetype \
      freetype-dev \
      harfbuzz \
      ca-certificates \
      ttf-freefont \
      nodejs \
      yarn \
      nodejs-npm
```

## 上記だけだと、日本語フォントが豆腐になってしまうので、noto(=no-豆腐)フォントを入れる。
これは、「[Dockerを使ってHeadless Chromeを動かしてみる](https://qiita.com/dd511805/items/dfe03c5486bf1421875a)」の内容を使わせていただいてます。

```dockerfile


RUN mkdir /noto
ADD https://noto-website.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip /noto

WORKDIR /noto

RUN unzip NotoSansCJKjp-hinted.zip && \
    mkdir -p /usr/share/fonts/noto && \
    cp *.otf /usr/share/fonts/noto && \
    chmod 644 -R /usr/share/fonts/noto/ && \
    fc-cache -fv

RUN rm -rf /noto
```

# まとめ

Webサイトのスクリーンショットを撮るための、CLIのツールと、それをなんちゃってWEB APIにするための方法についてご紹介しました。

## 所感

Pupeteer、サイトのTutorialの内容だけ見るとお手軽感満載で最高ですが、ちょっと動かしてみて、コンテナに入れてみて、手近のサイトいくつかを動かしてみるだけでも、いくつか工夫することがありました。