Playwrightで特定の通信だけを抽出したい【Flutter on Webと闘う】
上記記事の続編です。
Flutter on Webで制作されたサイトのうち、CanvasKitを用いて描画しているものはDOMではなくCanvasで要素が再現されています。そのため、Playwrightでスクレイピングするのは至難の業です。
DOMから攻めれないならネットワークから攻めてしまおう、というのが今回の記事です。
その前に...。PlaywrightはShadow DOMにも強い!
Flutter on WebのCanvasKitで描画されるサイトには Shadow DOM が含まれています。Shadow DOMは一般的にスクレイピングをする人間には嫌われる存在です。
今まで php-webdriver で Selenium を動かしてきたので衝撃を受けました。Seleniumではshadowrootを展開する必要があってShadow DOMの扱いが面倒で、かなり苦戦していました。(こんな感じ)しかし、Playwrightは全くそんなことありません。
通常の要素を取得するような感覚で特別な記述もなく、 locator()
で取得すれば良いのです。Playwrightがあまりにも良すぎる!!
Selenium使ってるお兄さんたち、Playwright使うとかなり衝撃受けると思いますよ。
本題。ネットワークの中身を監視する
page.on("response", async (response) => {
const responseURL = response.url();
if (responseURL.match(/hoge\.com/g) && responseURL.match(/目的のパス/g)) {
const responseHeaders = await response.allHeaders();
if (responseHeaders["content-type"] === "application/grpc-web+proto") {
console.log(await response.text());
}
}
});
こんな感じで pageに対して response イベントを張り、レスポンスのデータを確認できます。レスポンスを全て受け取ってしまうと多すぎて大変なので、不要なものは適当にフィルタにかけて必要なものだけを抽出します。
今回は
- hoge.comドメインの「目的のパス」というURLが含まれる通信だけに絞り込む
- さらにレスポンスヘッダーのcontent-typeがgrpcっぽいものだけに絞り込む
- 絞り込んだ通信の中身をconsole.logで確認。
という仕様にしています。
返ってきた中身を console.log で確認すると、gRPCのデータをデサニタイズした状態で中身を見せてくれます。
あとは不要な文字列を正規表現で抽出することでいい感じに使えるかと思います。
通信を発生させるために click(x, y)しないといけないときに活用したいChrome拡張機能
場合によっては対象要素を一度クリックしなければならないことがあるかと思います。その場合はPlaywrightの page.mouse.click(x, y)
を使って座標を直接指定することでしょう。そこで使う座標を調べなければいけないのですが、座標を調べるときは下記のChrome拡張機能が便利です。
※この拡張機能は表示に大きな影響を与えるようなので、取り扱いにはかなり注意した方が良さそうです。サイトの不具合だと思って調査に時間をかけましたが、原因はコイツだったりしました...w
謎の文字が邪魔なので消す(バイナリ?)
gRPCを text() で取得すると、こんな謎の文字が発生します。
これを消すには下記のように置換すればOKでした。(テキストデータとして掲載したかったのですが、Zennの表示上では除去するような挙動があったため掲載できませんでした...。)
const text = "尼崎";
const result = text.replace(/[^\u4E00-\u9FFF\u3040-\u309Fー\u3040-\u309F\u30A0-\u30FF。-゚a-zA-Z0-90-9A-Za-zⅠ-Ⅹ(\r\n|\n|\r| | )]/g, "");
-> 出力結果「尼崎」
- 漢字(全角)
- ひらがな(全角)
- カタカナ(半角・全角)
- 英字(半角・全角)
- 数字(半角・全角)
- ローマ数字(I~X)
- 改行コード
- 空白(半角・全角)
以外の文字は全て削除するという仕様です。余計な記号なども全部消しているような感じです。とはいえ、極力オリジナルと同等の状態は保持しておきたいので、改行コードや空白も残しています。
未検証:ラテン文字かも?
[A-Za-zÄÖÜäöüß -]{2,}
とか試さなきゃ。
レスポンスの返却状況とその他処理を分ける(何かの処理の中でレスポンスが返ってきた後に処理を行う)
説明下手ですみません。
レスポンスは page.on("response", async (response) => { ~~~
みたいに通常の処理の流れとは少し分離して書く必要があります。ちょっと扱いにくい部分もあるので、ちゃんとレスポンスが返ってきたことを確認してから処理を進められるように関数を作ります。
let responseStandby = "waiting";
const waitForResponse = (waitingTime = 30) => {
return new Promise(function (resolve, reject) {
let timer, timeout;
// タイムアウトを設定(デフォルト: 30秒、この時間までに要素出力を確認できなければエラー扱いになる)
timeout = setTimeout(function () {
clearInterval(timer);
reject(new Error("レスポンスが返ってきてないかもね。"));
}, waitingTime * 1000);
// 250ミリ秒毎にチェックを実行
timer = setInterval(function () {
if (responseStandby === "ready") {
clearInterval(timer);
clearTimeout(timeout);
resolve(true);
}
}, 250); // ここを変更すれば監視間隔を調整できます。
});
};
(async () => {
page.on("response", async (response) => {
if (await responseFilter(response)) { // 条件に合致するレスポンスが返ってきた
responseStandby = "ready"; // レスポンスが返ってきたというフラグを立てる。
}
});
// 本来はforなどで適切なループ処理をしていると思ってください。例として無限ループ。
while (true) {
responseStandby = "waiting"; // 繰り返し処理などで必ず初期値に戻す
await page.mouse.click(100, 100); // ボタンをクリックして通信を開始する。
await waitForResponse(); // レスポンスが返ってくるまで待つ。(waiting -> ready フラグに変化を待っているだけ。)
console.log("レスポンス返ってきたっぽいよ!")
}
})();
フラグをあちこちの関数で共有してフラグが変化するのを力技で監視しているだけのコードです。たぶんもっといい方法あるんだと思う。とりあえず動けばいいって人向け。
※ワイ氏、この関数好きで色んなところで多用してます。脳死でちゃんと動いてくれるのでめちゃくちゃ楽!(この要素いきなり出てくるけどタイミング分からんなぁ。せや!要素出現を待って要素取得して無理やり処理を追加するやで!的なノリで。テンプレートカスタマイズ系で使いやすい)
Flutter on WebのCanvasKitは強敵ですが、継続的なスクレイピングも比較的いける可能性が出てきました。大きな前進!スクレイパーたちの参考になれば幸いです。
ドキュメント
※もちろん、リクエストの監視もできます。しかし今回は触ってないので記事には含んでいません。
Discussion