chromedpで苦戦、Playwrightで楽々クリア:スクレイピングはPlaywright一択か

2023/03/31に公開

とある開発でスクレイピングの実装をすることになった。

以前、selenium、puppeteerを使用して実装した経験があったが、最近はGo言語での開発が増え、その書き味が気に入っていることもあり、今回もGo言語を使うことにした。

Go言語でスクレイピングを行うための主要なライブラリを調べたところ下記が見つかった。

  1. colly:
    collyは、Go言語で書かれた高速で使いやすいWebスクレイピングフレームワーク。
    単純なHTMLページの取得から、リンクのフォロー、CSSセレクタを使用した要素の抽出、並列リクエストの実行など、様々な処理に対応している。

  2. goquery:
    goqueryは、jQueryのような構文でHTMLドキュメントを操作・解析できるGo言語用ライブラリ。
    WebページのDOM要素の検索や抽出を簡単に行うことができるが、リクエストの実行やリンクのフォローなどは行わない。
    そのため、通常は他のライブラリ(httpクライアントなど)と組み合わせて使用される。

  3. chromedp:
    chromedpは、Go言語でChrome DevTools Protocolを使用して、Headless ChromeまたはChromiumブラウザを操作するためのライブラリ。
    jsが多用されているサイトや動的コンテンツを扱う場合に特に有用で、ブラウザの操作を自動化してページをレンダリングし、その後の解析や操作が可能。

selenium、puppeteerと同じHeadless Chromeでスクレイピングしたかったので、chromedpを採用することにした。

しかし、実際に使ってみるとchromedpでは複雑なスクレイピング処理が厳しく、結局はnode.jsのPlaywrightへ移行することになった。

chromedpの実装で躓いたこと

今回、スクレイピングしようとしたサイトでは「aタグをクリックするとonclickイベントが発動し、js内で画面に表示されないフォーム内のhiddenの値を変更、formをsubmitし画面遷移する」仕様であった。

そのため、直接任意のURLを叩いてもエラーになってしまい、トップページから辿る必要がある。

また、やりたいこととしては「トップページから一覧画面に遷移し、さらに各詳細画面に遷移し、情報を取得すること」。

それをchromedpを使って実装すると次のようなコードになった。

var detailInfo []string
tasks := chromedp.Tasks{
    // トップページへアクセス
    chromedp.Navigate(url),

    // 一覧画面へ遷移
    chromedp.WaitVisible(`form#top`, chromedp.ByQuery),
    chromedp.SetValue(`input#pageId`, pageId, chromedp.ByQuery),
    chromedp.Submit(`form#top`, chromedp.ByQuery),

    /**
     * 詳細ページへの遷移情報を全て取得
     */
    chromedp.Evaluate(fmt.Sprintf(`Array.from(document.querySelectorAll('%s')).map(a => a.getAttribute('onclick'))`, "div.items > a"), &detailInfo),
}

if err := chromedp.Run(ctx, tasks); err != nil {
    return "", err
}


/**
 * 詳細画面へのアクセス情報整理
 */


/**
 * 詳細画面の数だけ繰り返す
 */
for _, detailPage := range detailPages {
    tasks = chromedp.Tasks{
        /**
         * 各詳細画面に遷移
         */
        chromedp.WaitVisible(`form#list`, chromedp.ByQuery),
        chromedp.SetValue(`input#detailPageId`, detailPage.Id, chromedp.ByQuery),
        chromedp.SetAttributeValue(`form#list`, "action", "/detail", chromedp.ByQuery),
	chromedp.SetAttributeValue(`form#list`, "target", "_blank", chromedp.ByQuery),
        chromedp.Submit(`form#list`, chromedp.ByQuery),

        /**
         * 詳細画面の情報取得
         */
    }

    if err := chromedp.Run(ctx, tasks); err != nil {
        return "", err
    }
}

上記のようにタスクという単位で処理が実行される。

今回のケースでは1つのタスクでは処理できず、2つのタスクに分けているが、これが原因で詳細画面に遷移する際に403エラーが発生してしまった。

原因を探っていくと1つのタスクに収めるとうまくいくことがわかったが、改善策が見つからずchromedpでの実装は諦め、node.jsのPlaywrightに移行することにした。

Playwrightでは上記の課題を難なくクリアできた。

chromedpで発生した問題も解決方法はあるかもしれないが、タスク単位で実行する形が複雑で連続性があるスクレイピングに向かないのと、そもそもの情報量が少ないことが実装の難易度を上げているため、chromedpでの実装を捨てることに踏み切った。

まとめ

簡単なスクレイピング処理であればchromedpで十分だが、複雑な処理が必要になる場合は、最初からPlaywrightを使ったほうが無難だろう。

もしかしたらGo言語の他のライブラリで実現できたのかも知れないが、そもそもの情報量が圧倒的に違うのでPlaywrightのほうが楽に実装を進められることは間違いない。

Discussion