JavaScript
Node.js
Chrome

Headless Chromeを試してみる

More than 1 year has passed since last update.

Headlessブラウザとは

JavaScriptSPAの普及により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 ChromeGoogle Chrome Canaryを予めインストールしておく。

実際のコード

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

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()

解説

app.js
async function scrap() {

記事一覧記事A記事B記事Cと逐次処理が必要になるのでasync/awaitを使用した。
async/awaitについてはpromiseを書くよりも数段楽だったので後日記事にまとめたい。


app.js
  await chromy.blockUrls(['*.ttf', '*.gif', '*.png', '*.jpg', '*.jpeg', '*.webp'])

ブロックするURLを指定。ここでは画像等のファイルを開かずにスキップするよう指定している。


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()でページ内容の操作を行っている。


app.js
  for(let url of urls){
    await chromy.goto(url)

その場でブラウザを閉じずに次(記事)のURLを開き同等の処理をforで回す。


app.js
await chromy.close()

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

感想

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

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