🍆

HTMLタグからは取得できないデータをスクレイピングする方法【Playwright】

2024/09/10に公開

スクレイピングしたいデータがあっても、HTML上に出力されないためにデータ取得を断念していませんか?

例えば、Spotifyの歌詞表示機能。
歌詞はHTML上に出力されますが、歌詞行ごとの再生時間は出力されませんが、レスポンスデータの中にstartTimeMsが含まれています。このようなデータをスクレイピングするには、 .route() を使うことで解決できます。

解決のアプローチ

この問題解決のアプローチをご紹介します。

.route() を使うと解決できる!

HTMLには出力されていなくても、ブラウザ上で処理されている以上、サーバーから渡されたデータに処理に必要なデータが含まれていることが想像できます。つまり、レスポンスデータを監視できれば解決できるということになります。

そこで使うのが Playwright の .route() という機能です。

https://playwright.dev/docs/api/class-route

.route() でできることはざっくりと、

  • リクエストの実行タイミングを任意で制御できる。
  • リクエストを破棄することもできてしまう。(レスポンスが返ってこない状態を作れる)

というものすごい機能を持っています。
スクレイパーにとってはかなり重宝する機能です。仮にこの機能が廃止されてしまうとスクレイピングの実現の幅が極めて狭まってしまうと言ってしまっても差し支えないぐらいのものだと考えています。

.route() での実装例

ここでは例としてデータが https://example.com/api/lyric/reirie/バタフライ・キス からのレスポンスだと仮定して説明します。

下記の記述をリクエストが発生する前の箇所に記述します。

await page.route(/\/api\/lyric\/reirie\/バタフライ・キス.*/, async (route) => {
  const response = await route.fetch();
  const headers = response.headers();

  if (headers["content-type"].match(/application\/json\;/)) {
    const text = await response.text();
    console.log(text);
  }

  route.continue();
});

たったこれだけの記述でレスポンスデータを監視できます。
各行について見ていきましょう。

/\/api\/lyric\/reirie\/バタフライ・キス.*/
「/api/lyric/reirie/バタフライ・キス」が含まれるURLに絞って反応するようにしています。ここは正規表現の形式で指定もできますし、そのまま文字列で決め打ち指定ができます。末尾に .* としているのは、キャッシュバスターや予期せぬパラメータが続く可能性を想定しているためです。一応付けておいた方が漏れてしまう可能性を大幅に減らすことができます。

const response = await route.fetch();
反応したリクエスト予定のものを実際にこのタイミングでリクエストを投げます。データが返ってくると response 変数に格納されます。(POSTの場合でも何もせずとも form-data など全ての情報をそのまま保ったままリクエストしてくれます!)

const headers = response.headers();
レスポンスヘッダーを格納します。

if (headers["content-type"].match(/application/json;/))
レスポンスヘッダーの content-type(データ形式) がJSONのものに限り処理を続行するようにします。想定外のデータ形式の場合は無視するようにすべきでしょう。

const text = await response.text();
レスポンスデータを取得します。
このデータがスクレイピングで欲しかったデータです。

route.continue();
このリクエストに関する処理が終わったことを Playwright に伝えるものだとお考えください。リクエストを破棄する場合には route.abort() を使うのですが、今回のようなケースでは基本的に .continue() を使うことになるかと思います。

※JSON形式のデータでは試したことが無いので分かりませんが、取得したデータはおそらく JSON.parse() などの処理をすることになるかと思います。このあたりはAIさんに聞いてください。

必ずメモリ解放を行おう!

.route() はとても便利なのですが、少し問題があります。
このままにしておくとメモリが専有されてしまいコンピュータの処理がかなり遅くなることがあります。(少量のリクエスト制御であれば致命的に重くなることはありませんが。数が多くなるとかなり重くなります。)そこでメモリ解放が必要です。

メモリ解放するために await page.unrouteAll(); でrouteを破棄するようにしてください。
レスポンスデータを取得した後に記述するだけでOKです。例えば次のページに遷移する前など。

メモリを圧迫しすぎて止まっていたということが過去にありましたので、忘れずに必ず記述しておきましょう!

おしまい

.route() を使うだけでスクレイピングの幅がかなり広がります。
ぜひ取り入れてみましょう!

Discussion