Headless Chromeをいい感じに扱ってくれるpuppeteerで特定ドメインのウェブページのPDFをパシパシ取ってみた

## はじめに
この記事は [Wanoグループ Advent Calendar 2017](https://qiita.com/advent-calendar/2017/wano-group) の4日目の記事になります．

先週友人とハワイへ旅行していて，最初の成田行きのバスに乗っている時にslackで「4日目の記事書いてよ」みたいなことを言われたのでハワイっぽい記事を書きたいなと思っていたのですが，
全くネタが浮かばなかったのでおとなしく最近触った [puppeteer](https://github.com/GoogleChrome/puppeteer) の記事を書こうと思います．

## puppeteerとは
[puppeteer](https://github.com/GoogleChrome/puppeteer) とは，Headless ChromeをNode.jsで動かすためのAPIライブラリです．

Google謹製のライブラリなのでChromeの仕様変更などへの追従も早そうです．
今回はこれを使ってみました．

### （補題）そもそもHeadless Chromeってなんやねん
Headless Chromeとはヘッドレスブラウザの一種です．ヘッドレスブラウザとはざっくり言うとGUIなしで動くブラウザのことです．

自動テストを走らせるときなどは当然GUIは不要なので，不要なGUIを削り動作が軽いヘッドブラウザが求められるわけです．
Seleniumなどが有名ですが，Chromeがヘッドレス対応したことで一気に話題になっていたような気がします．

今回はそもそもHeadless Chromeを使ってみようというところから始まったので，とりあえず [公式](https://developers.google.com/web/updates/2017/04/headless-chrome?hl=ja) を見てみたわけですが，ウッワ面倒くさそう〜といった感情が沸き起こってしまい無理でした．

このままでは何もできず良くないのでなんかいい感じにしてくれるライブラリないかなー，と探していたらちゃんとGoogleが公開してました．気が利いていますね．

## 導入
puppeteerはNode.jsで動くので，Nodeが入っていない人は入れましょう．

**注意点として，puppeteer自体はv6.4.0以降で動くようですが，この記事では `async/await` を使用するので，v7.6.0以降でないと動きません．**

入れたら以下のコマンドでpuppeteerを導入できます．最近はライブラリの管理が楽ちんでいいですね．

```
$ yarn add puppeteer
# or "npm i puppeteer"
```

yarnとnpmの違いについては https://qiita.com/endam/items/c77396705e2a158f1952 などを参照してください．

puppeteerを導入できたら実際に使ってみましょう．

## 単一ページをPDFに保存する
```JavaScript
const puppeteer = require('puppeteer');
const fs = require('fs');

(async (url = "http://example.com") => {

  let browser = await puppeteer.launch(); //ブラウザを起動
  let page = await browser.newPage(); //タブを開く
  console.log("get " + url);
  let response = await page.goto(url); //開いたページでURLを開く

  //status codeの確認
  let ok = response.ok; //status codeが200番台ならtrueを返す
  if (!ok) {
    fs.appendFileSync("./error.log", url + ": " + response.status);
  }

  await page.pdf({path: "./example.pdf", format: 'A4'}); //ページの内容をPDFに保存

  browser.close();
})();

```

ブラウザの起動やタブを開くといったあたりは，普段ブラウザを使っている感じをイメージしてもらうとつかみやすいかなと思います．

`await page.goto(url)` でウェブページを開いたあとは，そのタブに対してステータスコードの確認，PDFの保存，pngでのスクショなどができます．

また，HeadlessとはいえChromeなので，SPAなどのJavaScriptゴリゴリみたいなウェブサイトもキーボードやマウスなどのイベントを発行することで操作することができます．
（なんならそういう操作が色々できるのがHeadless Chromeの強みだと思います……）
詳しくは [API](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md) をご覧ください．

このように，puppeteerを用いるとあまり難しいことを考えずにHeadless Chromeを扱えるので非常にいい感じです．

今回は特定ドメイン下のウェブページをクローリングしてPDFをパシパシ取っていきたいので，次はURLを拾い集めていこうと思います．

## ウェブページ下のURLを拾い集めていく
```JavaScript
const puppeteer = require('puppeteer');

const domain = "http://";

(async (url = "http://example.com") => {

  let browser = await puppeteer.launch();
  let page = await browser.newPage();
  console.log("get " + url);
  let response = await page.goto(url);

  //status codeの確認
  let ok = response.ok;
  if (!ok) {
    fs.appendFileSync("./error.log", url + ": " + response.status);
  }

  //PDFへの保存
  await page.pdf({path: "./example.pdf", format: 'A4'});

  //ページ内のリンクを抽出し配列に格納
  let links = await page.$$eval('a', as => as.map( (v) => v.href ));
  console.log(links);

  browser.close();
})();
```

`let links = await page.$$eval('a', as => as.map( (v) => v.href ));` の部分でURLを拾い集めています．

`page.$$eval()` については [ここ](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pageevalselector-pagefunction-args) を読んでもらえば早いのですが，ざっくり言うと

- 第一引数でページ内から要素を抽出するselectorを指定し，
- 第二引数で抽出した要素を処理する関数を定義する

ことができます．今回はURLをかき集めたかったので `<a>` タグを拾い集めて `href` を取り出しました．

ちなみに，JavaScriptのアロー関数あたりの書き方に慣れていない僕のような人向けには [このページ](https://qiita.com/may88seiji/items/4a49c7c78b55d75d693b) などが分かりやすくて良かったです（が，まあ自分で調べたほうが幸せになれると思います）．

URLを拾い集められたので，これをクローリングしてPDFをパシパシ撮っていけばクエストクリアです．

## 拾い集めたURLをクローリングしていく
ざっくり言えば今までの処理を関数に定義してURLを配列に格納し，定義した関数を再帰的に呼び出せばだいたいできます．

できるんですが，ちょっと見直したい部分もあったりするので続きはそのうち書く……で今は許してください．
以降の話ではpuppeteerの使い方的な要素はもうないと思います．

## まとめ
- puppeteerを使うと本当に楽にheadless chromeを扱えて良いので使おう
- 次はもっと凝ったことをしたい
- 旅行直後のエクストリーム執筆はしんどい
  - それを言い訳にしないように頑張っていきたいと思います

最後中途半端に終わってしまって申し訳ないです．
また，間違ってるところ，改善点などあれば（優しく）教えてください．

以上で終わりです．ありがとうございました．