Headless Chromeを試してみる

## この記事の内容は古いです
時々いいねされて気になったのですが、いま(2019/10現在)からだと `puppeteer` を使うのがいいと思います
https://qiita.com/tags/puppeteer


# Headlessブラウザとは
`JavaScript`や`SPA`の普及により`Ajax`など動的なページが増え、Webサイトをスクレイピングする際に`curl`などでHTMLを取得しても取得したいデータが見つからないことが多々ある。
そのためページ内のデータを取得したい場合は`Headless`(画面に表示されない)ブラウザを裏で実行し、普通のブラウザと同じよう`JavaScript`を実行してページの内容を参照する必要がある。

# Headlessブラウザにはどんなものがあるか
* `Phantom.js`
* `Nightmare.js` など

いままでは`Phantom.js`と、それを自動化する`Casper.js`を何度か使っていたが、Chromeが公式でHeadless対応したため`Phantom.js`のメンテナが辞任することを発表し一部で話題になり`Headless Chrome`への乗り換えを決意した。

# Headless Chrome でスクレイピング
今回は`macOS`環境の`Node.js`からQiitaの投稿内容を取得してみる。

## 環境のインストール
`Node.js`は既にインストール済みと想定して、
`$ npm install chromy`
ブラウザは`Google Chrome`か`Google Chrome Canary`を予めインストールしておく。

## 実際のコード 

以下のコードはQiitaのプロフィール画面から記事のURLを取得し、
各記事に設定されているタイトル、タグ、投稿日時を取得する。


```JavaScript:app.js
const Chromy = require('chromy')

async function scrap() {
  let chromy = new Chromy()

  await chromy.blockUrls(['*.ttf', '*.gif', '*.png', '*.jpg', '*.jpeg', '*.webp'])
  await chromy.goto('http://qiita.com/Ria0130')
  const urls = await chromy.evaluate(() => {
    let urls = []
    document.querySelectorAll("#main .tableList .media .ItemLink__title a").forEach(item => {
      urls.push('http://qiita.com/' + item.getAttribute('href'));
    })
    return urls
  })


  for(let url of urls){
    await chromy.goto(url)
    let json = await chromy.evaluate(() => {

      let title = document.querySelector("h1.ArticleMainHeader__title").innerHTML

      let tags = []
      document.querySelectorAll("#main .TagList a span").forEach(elem => {
        tags.push(elem.innerHTML)
      })

      let date = document.querySelector(".container .ArticleAsideHeader__date span time").getAttribute('datetime')

      return {
        title: title,
        tags: tags,
        date: date,
      }

    })
    console.log(json)
  }

  await chromy.close()

}

scrap()
```

# 解説
```JavaScript:app.js
async function scrap() {
```
`記事一覧`→`記事A`→`記事B`→`記事C`と逐次処理が必要になるので`async/await`を使用した。
`async/await`については`promise`を書くよりも数段楽だったので後日記事にまとめたい。

---

```JavaScript:app.js
  await chromy.blockUrls(['*.ttf', '*.gif', '*.png', '*.jpg', '*.jpeg', '*.webp'])
```
ブロックするURLを指定。ここでは画像等のファイルを開かずにスキップするよう指定している。

---

```JavaScript:app.js
  await chromy.goto('http://qiita.com/Ria0130')
  const urls = await chromy.evaluate(() => {
    let urls = []
    document.querySelectorAll("#main .tableList .media .ItemLink__title a").forEach(item => {
      urls.push('http://qiita.com/' + item.getAttribute('href'));
    })
    return urls
  })
```
`.goto()`で`http://qiita.com/Ria0130`を開き、
`evaluate()`でページ内容の操作を行っている。

---

```JavaScript:app.js
  for(let url of urls){
    await chromy.goto(url)
```
その場でブラウザを閉じずに次(記事)のURLを開き同等の処理をforで回す。

---

```JavaScript:app.js
await chromy.close()
```
`.close()`しないとブラウザのプロセスが残り続け、macOSの場合だとDockにブラウザアイコンが表示されたままになる。

# 感想
`Node.js`からだと`JavaScript`なのもあって非同期処理にさえ気をつければ簡単なスクレイピングは容易にできた。
ただ今回ページ内で`JavaScript`のクリックイベント等動的な部分は試していないので難しいことをしようとするとどうなるかはまだわからない。

`Headless Chrome` + `Python` + `ChromeDriver` + `Selenium` で試したときは
`ChromeDriver`が原因の可能性もあるが、ウインドウ(タブ)の切り替えやファイルのダウンロードが上手く行かなかったので、普段のブラウザですることをなんでも出来るわけではなさそう。
この辺りは今後バージョンアップで改善してくれると嬉しいな。