Puppeteerでスクレイピング

静的サイトから30ページぐらいのテキストを抽出してjsonにしたかったため、
Puppeteerでスクレイピングをすることにした。

なぜPython3 + Selenium + BeautifulSoupじゃないのか

このブログがフロントエンドの備忘録のため

Puppeteerとは

https://github.com/GoogleChrome/puppeteer
Chrome / Chromiumを動かすNodeのフレームワーク。
デフォルトはHeadless(GUIを持たず、コマンドで操作できる)で、ブラウザを出しての操作も可能。

インストール

てきとうなプロジェクトを作成し、Puppeteerをインストール。

yarn init
yarn add puppeteer

とりあえず立ち上げる

公式に写真を撮るサンプルがあったのでそのまま貼ってみる。

const puppeteer = require('puppeteer')

(async () => {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
  await page.goto('https://example.com')
  await page.screenshot({path: 'example.png'})
  await browser.close()
})()

"test.js"で保存し、Nodeで起動。

node test.js

example.comのスクリーンショットが撮影できる。 これだけだとしょうもない。

example

基本形

Nodeのバージョン

まずPuppeteerはNodeのバージョンが6.4.0以上から対応している。
ただし非同期処理の都合上、async/awaitで書けた方がラクなので、バージョンは7.6以上が推奨されている。

最低限の処理

const browser = await puppeteer.launch()でブラウザを立ち上げ、
const page = await browser.newPage()でページを作成する。
このページを使って色々やる。

処理を終え次第、"browser.close()"で自動的に閉じられる。 雛形はこんな感じになる。

const puppeteer = require('puppeteer')
(async () => {
  const browser = await puppeteer.launch() // 立ち上げ
  const page = await browser.newPage() // ページ作成
  await browser.close() // ブラウザ閉じる
})()

URLの指定

"page.goto(url, options)"を用いる。
urlにはurlを入れ、オプションがあればoptionsを設定。
もっとも良く使われてそうなのが"waitUntil"オプション。
接続してから指定したパラメータのタイミングになるまで処理を待つ。

  • domcontentloaded: DOMContentLoaded(HTMLリソースを読み込んだ)の時
  • networkidle0: 500ms以内にネットワーク接続がなくなった時
  • networkidle2: 500ms以内のネットワーク接続が2つ以下になった場合

デフォルトはload(ページの読み込みが完了)の時。
静的ページからテキスト取るならDOMContentLoadedで問題ない。
動的ページなら、networkidle系を用いないと完璧に取れない可能性がある。

const puppeteer = require('puppeteer')
(async () => {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
   // urlを開き、DOMContentLoadedまで待つ
  await page.goto('https://example.com', {
    waitUntil: 'domcontentloaded'
  })
  // ここに処理を書いていく
  await browser.close()
})()

ページの情報を取得してみる

"page.title()"で、現在ページのタイトルを取得する。
例によってPromiseが返るので、awaitを使う。

console.log(await page.title())

要素を取得する

"page.evaluate(function(){})"を使う。
関数内では普通にJavaScriptでの処理を書くだけ。

example.comの"This domain is..."という概要みたいなのを抜き出してみる。
pタグ2つのうち1番目がその文章なので、"document.getElementsByTagName"の[0]を取る。

const summary = await page.evaluate(() => {
  return document.getElementsByTagName('p')[0].innerText
})
console.log(summary)

また、"page.$(element)"という記述もある。
これはElementHandleというオブジェクトが返る。
そこからまた取リ出すのが面倒なので、"page.$eval(element, function(){})"を用いる。

const summary = await page.$eval('p', selector => {
  return selector.textContent
})
console.log(summary)

$を一つ増やして、page.$$eval(elements, function(){})で一括指定可能。
これも"document.getElementsByTagName"同様配列が返るので、[0]を取リ出す。

const summary = await page.$$eval('p', selector => {
  return selector[0].textContent
})
console.log(summary)

待つ

"page.waitFor(number)"で指定したミリ秒待たせる。
あまりガツガツアクセスすると怒られそうなので入れている。

await page.waitFor(1000)

ページ遷移の場合は、後述する"waitForNavigation()"で待たせることが可能。

移動する

page.click(element)でa要素を設定すると移動できる。
その後、"page.waitForNavigation()"を実行することで、
ページ遷移を検知して待機してくれる。
"waitForNavigation()"が待機するのはURLの変更かリロードとある。
SPAでは試していないけど、下記の方のエントリが参考になる。
MEMO: PuppeteerでSPA(Single Page Application)を操作する時の留意点 - qiita

waitForNavigation()部分でタイムアウトが発生し上手く行かなかったので、
Promise.allの中に入れた。

await Promise.all([
  page.click('body > div > p:nth-child(3) > a'),
  page.waitForNavigation()
])
console.log(await page.title())

example.comのリンク先はIANAのサイトになるので、
"IANA — IANA-managed Reserved Domains"というタイトルがconsole.log()で表示される。

page.waitForNavigation()にもいくつかのオプションが設定可能で、その中には"waitUntil"もあるので、設定した方が良い。

viewportの変更

画面サイズのデフォルトは800x600だけど変えられる。
puppeteer.launch()に"defaultViewport: {}"を入れる。

const puppeteer = require('puppeteer')

(async () => {
  const browser = await puppeteer.launch({
    defaultViewport: {
      width: 200,
      height: 200
    }
  })
  const page = await browser.newPage()
  await page.goto('https://example.com')
  await page.screenshot({path: 'example_mini.png'})
  await browser.close()
})()

200 x 200で撮影。

example mini

"isMobile"(モバイル端末かどうか)、"hasTouch"(タッチをサポートするか)などのオプションもあった。

headlessの設定

デフォルトはheadlessでの動作だけど、無効化できる。
puppeteer.launch()の引数に"headless: false"を入れることでブラウザを開く。

const browser = await puppeteer.launch({ headless:false })

実行するとChroniumが開く。

また、"devTools: true"を入れても、自動的にブラウザが開く模様。
これは起動時にブラウザの開発者ツールを開いてくれる、開発者というか解析者向けのオプション。

const browser = await puppeteer.launch({ devTools:true })

また、"slowMo"オプションを指定することで、指定したミリ秒処理を遅延させることができる。

const browser = await puppeteer.launch({
  headless: false,
  slowMo: 500
})

とりあえずjsonを作成できた。