puppeteerでスクレイピング

web上の情報を抽出するスクレイピング技術ですが、いままでphantomJSで行っていましたが、chromeがヘッドレスブラウザに対応したとのことで、そのnodeライブラリであるpuppeteerで実践してみました。

## 環境構築
とりあえずお試しということで、dockerで構築しました。
構成はnode.jsのdockerイメージにpuppeteerを追加するかたちです。

下記2サイトの手順を大幅に参考にさせていただきました。
[Docker コンテナ上で Puppeteer を動かす](https://qiita.com/QUANON/items/59c468adfff0278f20bb)
[Puppeteer をDockerコンテナで利用する](https://qiita.com/QUANON/items/59c468adfff0278f20bb)

ディレクトリ構成はこんな感じ。
/
　├ app/
　│　└ script/
　│　   └ app.js
　│　└ data/
　├ docker-compose.yml
　├ Dockerfile
　└ Package.json


```Dockerfile.
FROM node:9.2.0

RUN apt-get update \
 && apt-get install -y \
      gconf-service \
      libasound2 \
      libatk1.0-0 \
      libc6 \
      libcairo2 \
      libcups2 \
      libdbus-1-3 \
      libexpat1 \
      libfontconfig1 \
      libgcc1 \
      libgconf-2-4 \
      libgdk-pixbuf2.0-0 \
      libglib2.0-0 \
      libgtk-3-0 \
      libnspr4 \
      libpango-1.0-0 \
      libpangocairo-1.0-0 \
      libstdc++6 \
      libx11-6 \
      libx11-xcb1 \
      libxcb1 \
      libxcomposite1 \
      libxcursor1 \
      libxdamage1 \
      libxext6 \
      libxfixes3 \
      libxi6 \
      libxrandr2 \
      libxrender1 \
      libxss1 \
      libxtst6 \
      ca-certificates \
      fonts-liberation \
      libappindicator1 \
      libnss3 \
      lsb-release \
      xdg-utils \
      wget

# node関連設定
WORKDIR /usr/src/app 
COPY     package.json /usr/src/app/
RUN npm install

# スクリプト配置用ディレクトリ作成
RUN mkdir -p /usr/src/app/script

# フォント追加
RUN mkdir /noto

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

WORKDIR /noto

RUN apt-get install -y unzip
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

WORKDIR /
RUN rm -rf /noto

ENTRYPOINT ["nodejs","/app/script/app.js"]
```

```package.json
{
    "name": "puppeteer_test",
    "version": "0.1.0",
    "description": "puppeteerの動作テスト",
    "dependencies": {
	"puppeteer"   : "*",
    }
}
```

```docker-compose.yml
version: '2'

services:
    main:
        build: "."
        container_name: "puppeteer_test"
        volumes:
         - "./app/script:/app/script"
```

## puppeteerを使う
### 基本
```/app/script/app.js
const puppeteer = require('puppeteer');

(async() => {
    const browser = await puppeteer.launch({
        args: [
          '--no-sandbox',
          '--disable-setuid-sandbox'
        ]
    });
    const page = await browser.newPage();
    await page.goto('http://example.com'); // 表示したいURL

    /*（何か処理）*/

    browser.close();
})();
```
(何か処理)のところでページの遷移だったり画像キャプチャだったりを書いていく形になります。

### デバイスを指定
puppeteer.DeviceDescriptorsメソッドを最初に実行しておくことでデバイスを指定できます。

```/app/script/app.js
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');

(async() => {
  const browser = await puppeteer.launch({
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox'
    ]
  });
  const page = await browser.newPage();
  const iPhone = devices['iPhone 6']; //デバイスはiPhone6を選択
    
  await page.emulate(iPhone); // デバイス適用
  await page.goto('http://example.com/');

 ...
```

puppeteerでの処理は大まかに、
**ページ全体に対して処理をするpageのメソッド**
と、
**pageメソッドで抽出したエレメントに対して処理をするelementHandleのメソッド**
を使う形に分かれます。

### domの選択
```domSelect.js
// page.$(セレクタ)
let elm= await page.$('#login_button');
```

### フォーカスを当てる
```domSelect.js
// page.focus(セレクタ)
let elm= await page.focus('#login_button');
```

### フォーム入力
テキストはtypeメソッドで入力できます。

```formInputSample1.js
// page.type(セレクタ , 値)
await page.type('input[name="id"]', 'abc');
await page.type('input[name="password"]', '123');
```

<div>もしくはfocusと組み合わせて、</div>

```formInputSample2.js
// let element = page.focus(セレクタ)
// element.type(値)
let idElm = await page.focus('input[name="id"]');
await idElm.type('abc');

let passElm = page.focus('input[name="password"]');
await passElm.type('123');
```

### クリック
clickメソッドでクリックできます。

```formSubmit1.js
// page.click(セレクタ)
page.click('#login_button');
```
<div>もしくは$()メソッドで</div>

```formSubmit2.js
// page.$(セレクタ)
let buttomElm = page.$('#login_button');
await buttomElm.click();
```

### domの選択(複数)
通常のjavaScriptでいうquerySelectorAllにあたるものです。

```domSelect.js
// page.$$(セレクタ)
let items = await page.$$('#list .item');
```

### domの選択（応用）
セレクタ表記のみで書きづらい要素を指定した場合は、組み合わせで対応できます。

```pickup.js
// 3番目のitemクラスのなかのnameクラス(nth-childでもいいですが例として。。)
let items = await page.$$('.item');
let pickup = await items[2].$('.name');
```

### 要素の抽出
page.evaluateメソッドを使うことで通常のJavaScript記述で処理できるため、そのなかでinnerText等を使って抽出できます。

```scraiping.js
const scrapingData = await page.evaluate(() => {
    const dataList = [];
    const nodeList = document.querySelectorAll("td.date");
    nodeList.forEach(_node => {
        dataList.push(_node.innerText);
    })
    return dataList;
});
```

### スクリーンショットを撮る
screenshotメソッドでスクリーンショットを保存できます。簡単。

```screenshotPage.js
await page.screenshot({path: 'screenShotPage.png'});
```

公式ドキュメントによるとdom単位のスクリーンショットもいけるらしい。すごい。。

```screenshotDom.js
let element = page.$('#side_nav');
await element.screenshot({path: 'screenShotDom.png'});
```

### レンダリングを待つ
ページやDOMがレンダリングされるタイミングを待つことができます。

```wait.js
// ページの遷移待ち
await page.waitForNavigation();

// DOMのレンダリング待ち
await page.waitForSelector('#contents');
```

## サンプル
ここまでの使い方をまとめたようなサンプル。
(2019/02/18 編集)　コメントにてログイン処理の不具合をご指摘頂いた内容を反映しました。ありがとうございました。

```sample.js
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const fs = require('fs');

(async() => {
    const browser = await puppeteer.launch({
        args: [
          '--no-sandbox',
          '--disable-setuid-sandbox'
        ]
    });
    const page = await browser.newPage();
    const iPhone = devices['iPhone 6']; // 端末はiPhone6を選択
    
    await page.emulate(iPhone);
    // login
    await page.goto('http://sample-site.com/login'); //sample-site.comへ接続
    await page.screenshot({
        path: 'data/loginPage.png' // スクリーンショットを撮る
    });

    // ログインしているか判定
    const loginChk = await page.evaluate(()=> {
        const node = document.querySelectorAll("#login_button");
        return node.length ? false : true;
    });
    
    // ログインしていなかったらログイン処理
    if (!loginChk) {
        await page.type('input[name="login_id"]', 'abc');
        await page.type('input[name="login_password"]', '123');

        page.click('#login_button'); // waitForNavigationがタイムアウトになってしまうばあいがあるため、ここではawaitは付けない（コメント参照）

        await page.waitForNavigation({
            waitUntil: 'domcontentloaded'
        });
    }

    // ログイン後ページトップ->メニューから2番目のボタンをクリック
    const menuItems= await page.$$('#menu .item'); 
    const menuItem = await menuItems[1].$('button[type=submit]');
    await menuItem.click();

    await page.waitForNavigation({
        waitUntil: 'domcontentloaded'
    });

    // 一覧ページ
    // コンテンツ抽出
    const scrapingData = await page.evaluate(() => {
        const dataList = [];
        const nodeList = document.querySelectorAll("td.date");
        nodeList.forEach(_node => {
            dataList.push(_node.innerText);
        })
        return dataList;
    });

    //結果をファイルに出力
    fs.writeFile('data/result.txt', JSON.stringify(scrapingData),(err) => {
        if (err) throw err;
	   console.log('done');
    });
    
    browser.close();
})();
```


## 公式ドキュメント
ここに記述した諸々は公式ドキュメントを見ていただくとさらに詳しい使い方が載っていますので参考にしてください。
[Puppeteer API](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md)