動的に生成されたページをpuppeteerでスクレイピングする

# 背景
JSによって動的に生成されたWebページをスクレイピングすべく、mechanize ⇨ phantomJS ⇨ときて、puppeteerに辿り着きました。

###それぞれの問題点
|スクレイピング方法|良い点|問題点|
|---|---|---|
|mechanize|Rubyのgem、簡単に使える|動的に生成されたページに対応していない|
|phantomJS|動的に生成されたページに対応している|公式にメンテナンス停止がアナウンスされている|

# puppeteerとは？
Github ⇨　https://github.com/GoogleChrome/puppeteer
>Puppeteer is a Node library which provides a high-level API to control headless Chrome over the DevTools Protocol. It can also be configured to use full (non-headless) Chrome.

Nodeのライブラリの一つで、Chromeを操作できる。
Chrome DevToolsチームがメンテナンスしているというのも強み。

# puppeteerのインストール
### 1.yarnを使う場合

```zsh:terminal
$ sudo yarn add puppeteer
zsh: command not found: yarn
```
あれ。yarnがそもそもなかった。Homebrew経由でインストール。

```zsh:terminal
$ brew install yarn //nodebrew等を用いてnode.js　のバージョン管理をしていない場合。(yarnと一緒にnode.jsもインストールされる)

$ brew install yarn --without-node //nodebrew等を用いてバージョン管理をしている場合。
```
そして、もう一度トライ。

```zsh:terminal
$ sudo yarn add puppeteer
```
できた。

### 2.npmを使う場合
npmをすでに使用している場合、こっちの方がスマートな印象。

```zsh:terminal
$ npm i puppeteer
```
# 基本的な使い方

1. puppeteerを読み込む。
2. Chromeインスタンスを生成。
3. Pageオブジェクトを生成。
4. 引数に指定したURLのページを読み込む。

```js:test.js
const puppeteer = require("puppeteer"); //・・・１
puppeteer.launch().then(async browser => {　//・・・２
	const page = await browser.newPage(); //・・・3
	await page.goto('https://www.google.com'); //・・・4

	// some actions...

	browser.close();
});

```

# いざ、実践
今回のターゲットはみんな大好き、旅行サイトの[expedia](https://www.expedia.co.jp)。
ホテルの情報をスクレイピングしたい。ただ、検索結果が動的に生成されるっぽく、mechanizeではお手上げ。
puppeteerを手にして立ち向かう。コードはこんな感じ。

やっていることは、
1. expediaにアクセス
2. ホテルの場所を入力するフォームにフォーカスした状態にする
3. 検索ワードを入力
4. 検索ボタンを押す
5. 検索結果のページから、ホテルの名前を取得
6. 結果を表示

```js:scraping.js
const puppeteer = require("puppeteer");

puppeteer.launch().then(async browser => {
	const page = await browser.newPage();
	await page.goto("https://www.expedia.co.jp",{waitUntil:"networkidle"});
	await page.focus("input#hotel-destination-hp-hotel");
	await page.type('ソウル');
	await page.click('button[type="submit"]');
	await page.waitForNavigation({waitUntil:"networkidle"});

	const hotelNames = await page.evaluate(() => {
		const node = document.querySelectorAll("h3.visuallyhidden");
		const array = [];
		for(item of node){
			array.push(item.innerText);
		}
		return array;
	});
	
	console.log(hotelNames);
	browser.close();
});
```

※ `{waitUntil:"networkidle"}`というのは、ページの読み込みが完了する（＝jsによってページ化生成される）まで待つことを指定するオプション。

```zsh:terminal-出力結果
[ 'L7 明洞',
  'ザ・プラザ・ソウル、オートグラフ コレクション',
  'フォー ポインツ バイ シェラトン ソウル ナムサン',
  'ホテル プレジデント',
  'ホテル PJ ミョンドン',
  'フォーシーズンズホテル ソウル',
  'ロイヤル ホテル ソウル',
  'コートヤード・ソウル南大門',
  'パパ ハウス ミョンドン',
  'ラマダ ホテル アンド スイーツ ソウル南大門',
  'ホテル シンシン',
  'ホテル プリンス ソウル',
  'ベスト ウェスタン プレミア ソウル ガーデン ホテル',
  '新羅ステイ西大門',
  'ザ クラシック 500',
  'ロッテ シティ ホテル ミョンドン',
  'ティーマーク グランド ホテル明洞',
  '江南アールヌーボー シティ',
  'ロッテ シティ ホテル 麻浦',
  'ホテル リビエラ',
  'フレイザー プレイス 南大門',
  'ドーミー イン プレミアム ソウル カロスキル',
  'クラウン パーク ホテル ソウル',
  'ラマダ ソウル',
  '新羅ステイ瑞草',
  'イビス アンバサダー ソウル 明洞',
  'BAROATO 2nd',
  'ホテル ナポレ',
  'バンヤン ツリー クラブ & スパ ソウル',
  'ナイン ツリー プレミア ホテル ミョンドン 2',
  'Hotel Peyto Gangnam',
  '新羅ステイ駅三',
  '南山シティ ホテル明洞',
  'サンビー ホテル',
  'ロッテシティホテル金浦 (キンポ) 空港',
  'ベスト ウェスタン プレミア 江南',
  'サボイ ホテル',
  'ホテル スカイパーク セントラル 明洞',
  'ホテル キョンドン',
  '新羅ステイ クロ',
  'ホテル ステイ イン ソウル ステーション',
  'バビエン スイーツ II サービスド レジデンス',
  'G2 ホテル明洞',
  'ステイ B ホテル 明洞',
  'ホテル ベニュー G',
  'ホテル アロパ',
  'シグニエル ソウル',
  'ホテル 28 明洞',
  'グラッド ライブ江南',
  'ドーミー イン ソウル カンナム',
  'ベスト ウェスタン ニュー ソウル ホテル',
  'ホテル アトリウム',
  'グラッド ライブ江南',
  'コンラッド ソウル' ]
```

ついに動的ページに勝利。
あとはこれをデータベースに突っ込むかスプレッドシートに吐き出すかすれば使い勝手良さそう。