unhurried

コンピュータ関連ネタがほとんど、ときどき趣味も…

puppeteerを使ったスクレイピング

puppeteerを使ってスクレイピングを実装したときにはまったポイントをまとめています。

ナビゲーション完了の検知方法

あるURLにアクセスしたとき、もしくはボタンをクリックしたときなどにナビゲーションの完了を検知するには、単純にはDomContentLoadedイベントを検知すれば良い。

yield page.goto('https://example.com/')
yield page.waitForNavigation({ waitUntil: 'domcontentloaded' })

ところが、Webサイトによっては一度クッションとなるURLに遷移(HTTPステータスコード200が返却される)して、JavaScriptの処理で別のURLへ再度遷移する、という振る舞いをするものがある。この場合には、DomContentLoadedイベントを検知するだけでは最初のクッションページでのイベントを検知してしまい、上手くいかない。

この対応としては、以下の3つの方法がある。

(1) 一定時間ネットワーク通信のないことで完了を判定する。
yield page.goto('https://example.com/')
yield page.waitForNavigation({ waitUntil: 'networkidle0' })

一定時間ネットワーク通信がないことを表す2種類のイベントが定義されている。

  • networkidle0:コネクション数が0個である状態が500ミリ秒続いたとき
  • networkidle2:コネクション数が2個以下である状態が500ミリ秒続いたとき
(2) URLとDOM構築状態を組み合わせて判定する。

より確実にかつ高速化したい場合には、URLが目的のページになっていることと、DOM構築完了状態となっていることを組み合わせて判定することができる。

yield page.waitForFunction(() => {
    console.log(window.location.href)
    const locationCondition = window.location.href === 'https://example.com/target/'
    const readyStateCondition = window.document.readyState === 'interactive' || window.document.readyState === 'complete'
    return locationCondition && readyStateCondition
})
(3) 目的のページにしか存在しないDOM要素の出現を検知する。

もう一つの方法として、目的のページにしか存在しないDOM要素が出現したことを検知して、読み込み完了とすることもできる。

yield page.waitForSelector('input[name="xxx"]')

Cookieのクリア

スクレイピングを行う際には、以前の操作の影響を受けないために、毎回Cookieをクリアした状態で行うのがよい。ところが、現在のpuppeteerのAPIではCookieのクリアが簡単にできないため、毎回ブラウザのインスタンスを新しく生成する必要がある。

この問題はIssueとして登録されているため、将来的にAPIが実装されるかもしれない。

不要なリクエスト送信を防ぐ(高速化)

はまりどころではないが、可能な限りスクレイピングを高速化したい場合に、不要なリクエスト送信を防ぐ方法ががある。例えば、以下のコードではexample.comドメイン以外のリソースと、拡張子がpngのファイルへのリクエストを抑制できる。

yield page.setRequestInterception(true)
page.on('request', interceptedRequest => {
    const reqUrl = url.parse(interceptedRequest.url())
    const reqExt = path.extname(reqUrl.pathname)
    const reqHost = reqUrl.hostname
    // Resources from 3rd party domains
    if (reqHost !== 'example.com') {
        interceptedRequest.abort();
    // Images
    } else if (reqExt === '.png') {
        interceptedRequest.abort();
    } else {
        interceptedRequest.continue();
    }
});