🍆

スクレイピング用途でPlaywrightを使うときに知っておきたいこと

2022/12/30に公開

2024年4月2日: 全文書き直しました!

Playwrightをスクレイピングする目的で使う際に必要な基礎知識や便利なこと、経験則から得た知識などをまとめてみました。

かなりのボリュームになっています。
情報が探しにくいと思いますので、Zennの目次機能を使うと便利です。

今後もこの記事に情報を追加していきます!
分からないことがあればコメント欄にて聞いていただければお答えします。(DMでもOKです。)

※【大前提】スクレイピングの実行は相手サーバーに大きな負荷がかからないように気をつけましょう。

0. プロキシについて

継続的なスクレイピングを行うには欠かせない知識です。

0-1. 必ずプロキシを刺そう!

スクレイピングする際は必ず有料プロキシを利用しましょう。
IPアドレス単位でアクセス制限をかけられることがあります。自宅のIPアドレスがブロックされてしまうと普段利用できなくなってしまいます。捨てIPアドレスとして「プロキシ」というものがあります。

データの流れは下記の通りです。

【リクエスト方向】
自宅 → プロキシサーバー → 相手サイト
【レスポンス方向】
自宅 ← プロキシサーバー ← 相手サイト

このようにプロキシサーバーがデータの送受信の間に入ってくれるため、相手サイトは自宅のIPアドレスを知ることができません。スクレイピングにおいてはプロキシは欠かせないものだとご理解ください。

プロキシリストと呼ばれるものを手に入れると便利です。

0-2. プロキシリストを手に入れるには?

プロキシはどこから入手できるかですが、「プロキシ 有料」などで検索してみてください。かなり多くの業者が出てきますのでお好きなものを選んで頂ければと思います。注意点としては日本リージョンのプロキシがあるかを重要視してください。

というのも、日本のWebサイトでは、サイバーセキュリティの観点から海外IPからアクセスを受け付けない設定にしていることも珍しくありません。

【僕が使ってるプロキシ】
WebShare を使っています。

下記の構成で月2.24ドルで利用できちゃいます。

  • 75個のIPアドレス
  • 250GBの帯域幅
  • API提供あり

様々なプロキシを探してきましたが、これほど安く高品質なプロキシは他に見つかっていません。特にこだわりが無ければ WebShare を一度お試しください。

0-3. WebShareのプロキシリストを使った実装例

僕はPlaywright実行の際にプロキシリストから自動的にランダムで選択される仕組みを実装して使っています。(下記折り畳んであります。)

なお、プロキシはIP認証を使っているのでPlaywright上では認証処理は含まれていません。

実装例を見る

▼プロキシをランダムで取得する関数

utils/functions/proxy.ts
import fs from "fs/promises";
import path from "path";

export const getRandomTheProxy = async () => {
  const proxy = await proxyList();
  const proxyArray = proxy.trim().split(/\n/);
  return proxyArray[Math.floor(Math.random() * proxyArray.length)];
};

const proxyList = (): Promise<string> => {
  // ここでプロキシリストを取得しています
  const fileProxyListPath = path.resolve(__dirname, "./../proxy/proxy-list.txt");
  return new Promise(async (resolve) => {
    const result = await fs.readFile(fileProxyListPath, "utf-8");
    resolve(result);
  });
};

▼Playwrightをスクレイピング用に調整した関数

utils/functions/browser.ts
import "dotenv/config";
import { getRandomTheProxy } from "@utils/functions/proxy";
import { chromium, Browser, BrowserContext, Page, Response } from "playwright";

export const chrome = (
  headless = false
): Promise<{
  browser: Browser;
  context: BrowserContext;
  page: Page;
  waitForResponseResource: (resource: string) => Promise<Response>;
}> => {
  return new Promise(async (resolve) => {
    const proxyURL = await getRandomTheProxy();

    // 帯域幅の節約のため、Webフォントと画像を無効化してます
    const browser = await chromium.launch({
      headless: headless,
      args: ["--blink-settings=imagesEnabled=false", "--disable-remote-fonts"],
      proxy: {
        server: `http://${proxyURL}`,
      },
    });
    const context = await browser.newContext();
    const page = await context.newPage();

    // 指定したリソースのレスポンスが返ってくるまで待機する
    // 非同期が含まれるサイトのスクレイピングをする際に超便利!
    function waitForResponseResource(resource: string) {
      return page.waitForResponse(
        (response) => {
          if (response.url() === resource && response.status() === 200) {
            return true;
          }
          return false;
        },
        { timeout: 60 * 1000 }
      );
    }

    resolve({
      browser,
      context,
      page,
      waitForResponseResource,
    });
  });
};

これらの関数を使ってスクレイピングをするには?

index.ts
import { chrome } from "@utils/functions/browser";

(async () => {
  // await chrome(true) とするとヘッドレスで実行できます
  const { page, browser } = await chrome();

  // ブラウザを閉じる
  await browser.close();
})();

pagebrowser も通常のPlaywrightと同じものです。ただ単に使いやすいようにラップしただけですね!かなり少ない記述でプロキシを適用したり、画像やWebフォントを無効にした状態でのスクレイピングを開始できるので、この書き方オススメです!

よろしければご活用ください。

0-4. プロキシの帯域幅の節約を意識しよう!

有料プロキシを使う場合、低コストなスクレイピングを実施するには帯域幅を強く意識しなければなりません。

帯域幅とはデータ通信量のようなもので送信できるデータ量には制限が設けられています。足りないなら課金すればいいのですが、通常の月250GBから月1000GBにアップすると価格が2倍以上に跳ね上がってしまいます。

なので、可能な限り月250GBで済むように帯域幅を節約することを意識した方がお財布に優しくなります。

僕が取り入れてる節約方法としては、

  • Webフォントを無効にする
  • 画像を無効にする
  • CSSを無効にする
  • 不要なJSファイルは無効にする

としています。
これらをするだけで余裕で90%ぐらいは削減できるので絶対にやるべきです。

上記は実際に削減した例です。(上が削減後、下が削減前)
3.6MBから279kBまで大幅に削減できています!

ちなみにPlaywrightには「ファイル単位でリクエストを投げない」という、めちゃくちゃすごい機能が備わっています。詳しくは下記の記事をご覧ください。

https://zenn.dev/masa5714/articles/d798c4fa5f8b08

画像の非表示とWebフォントの無効化は

const browser = await chromium.launch({
  args: [
    "--blink-settings=imagesEnabled=false",
    "--disable-remote-fonts"
  ],
})

という指定をするだけでできます。他にも様々なオプションがあります。(下記参照)
https://peter.sh/experiments/chromium-command-line-switches/

0-5. プロキシの切り替えをする場合はCookieを削除しよう

Playwirght単体で使う場合は問題ないのですが、Seleniumなどに接続してスクレイピングする場合に気をつけたいのがCookieなどの存在です。

いくらプロキシを使っていたとしても "同一人物" と分かってしまうような痕跡を残さないように注意してください。これらの些細な情報から同一人物と分かってしまうとプロキシリストが一斉にブロックされることがあります。(GAFAMのいずれかのサイトで喰らったことがあります。)

プロキシリストを無駄にしないためにも気をつけましょう。

1. Playwrightのスクレイピング処理

スクレイピングにおいてよく使うものをピックアップして掲載しています。

1-0. 処理の基本

import { chromium } from "playwright";

(async () => {
  const browser = await chromium.launch();
  const context = await browser.newContext();
  const page = await context.newPage();
})();

このように書くことでスクレイピングを開始できます。
ただ、毎度これらを書くのは面倒なので、「0-3. WebShareのプロキシリストを使った実装例」 で紹介した書き方がオススメです。

困ったらとりあえず await をつけるようにしてみてください。

1-1. ページ遷移する

await page.goto("https://www.reirieofficial.com/");

指定したURLにアクセスできます。

1-2. 要素の取得

const element = await page.locator("#hoge");

JSの document.querySelectorAll() とほぼ同じだとイメージすると分かりやすいかと思います。対象箇所が複数あれば全て取得されます。

1-3. 複数要素のループ処理(要素ごとに処理する)

const elements = await page.locator(".article");
const count = await elements.count();
for (let i = 0; i < count; i++) {
  const text = await elements.nth(i).innerText();
  console.log(text);
}

この例では要素ごとにテキストを取得しています。

1-4. テキストを取得する

const element = await page.locator("#hoge");
const text = await element.innerText();

.innerText() でテキストを取得できます。
テキスト取得時には .trim() しておくことをオススメします。

1-5. 要素の有無を判定する

const element = await page.locator("#hoge");

if (await element.count() > 0) {
  console.log("要素がありました!");
} else {
  console.log("要素がありませんでした...。");
}

.count() で要素を数えて 1つ以上 あるかで判定できます。

1-6. テキストボックスにテキストを入力する

const inputElement = await page.locator("#input-hoge").fill("こんにちは");

.fill() でテキストを入力できます。

ラジオボタンやセレクトボックスについては下記をご覧ください。

https://playwright.dev/docs/input

1-7. マウス操作

カーソルを移動(実際にはカーソルは表示されない)
await page.mouse.move(100, 100);

x方向に100px、y方向に100pxの位置に移動します。

左クリックをする
await page.mouse.click(100, 100)

x方向に100px、y方向に100pxの位置でクリックします

マウス座標を計測する際には下記のブックマークレットが便利でした!

https://gist.github.com/darrenscerri/9019209

1-8. ページスクロール

await page.mouse.wheel(0, 200);

マウスホイール操作(スクロールできる)もできます。
x方向に0px、y方向に200px ページを進めます。

なお、マウスホイールと同じ動きをするので、マウスカーソルの位置を移動させる必要がある場合もあるので注意してください。

1-9. キーボード操作

Enterキーを押す
await page.keyboard.press("Enter");

Enterキーを押したときと同じ動きをします。
フォームなどでEnterで送信する場合は事前に await page.locator("#hoge").fill("こんにちは") などでinput要素などを選択状態にしておきましょう。

1-10. クリック先の別ウィンドウをスクレイピングするには?

const [popup] = await Promise.all([
  page.waitForEvent("popup"),
  await page.locator("#button").click(), // ここでポップアップが開く
]);

// 別ウィンドウで開いたページのロード完了を待機
await popup.waitForLoadState();

// 別ウィンドウの中の要素のテキストを取得
await popup.locator("#hoge");

// 別ウィンドウで開いたものだけを閉じる
await popup.close();

page ではなく popup を使ってスクレイピングしていく形になります。

1-11. JSによるページ遷移を待機する

通常、 await page.locator("#link").click() でページ遷移したら、ページ表示されるまで自動的に待機してくれます。しかし、JSによるページ遷移は待ってくれません。(例:location.hrefなどによる遷移)

そんなときは .waitForURL() を使います。(正規表現でも指定可)

https://playwright.dev/docs/api/class-page#page-wait-for-url

もしくは特定のリソースのレスポンスが返ってくるまで待機するのも良いでしょう。

0-3. WebShareのプロキシリストを使った実装例」 で紹介した関数に waitForResponseResource() というものを含めてあります。

await waitForResponseResource("https://example.jp/assets/app.js") と指定すれば、 https://example.jp/assets/app.js のレスポンスが返ってくるまで待機してくれるようになります。DOM上に変化の表れない非同期通信などに活用すると便利だと思います。

2. スクレイピングで知っておきたいこと

2-1. データの加工はスクレイピング後で!

スクレイピング処理をしながら同時にデータ加工をしたくなるかもしれませんが、これは避けるべきです。なぜならオリジナルデータが残っておらず後から別の形式に加工したくなったら、再度スクレイピングしなければなりません。

オリジナルデータが残っていれば何度も何度も加工をトライできます。

このルールは守った方が絶対にいいです!
※ただし .trim() などのオリジナルデータを傷つけない程度であればやっておきましょう。

2-2. 要素の取得は事前にChromeの開発者ツールのConsoleタブで確認しておこう!

await page.locator(".hoge") などといきなり書くのもオススメできません。目視では問題なさそうでも意外と「複数要素あるんかい!」となる場面は少なくありません。思い込みでコードを書くのではなく、ChromeのConsoleタブから何個取得されそうかを調査しておきましょう。

document.querySelectorAll(".hoge");

これを行っておくだけでもかなり効率的になります。(セレクタが間違っていて要素取得できてなかったというミスも減らせます。)

2-3. 予想外の動きをしたらひたすら throw new Error() しておこう!

スクレイピングは予期せぬエラーが当たり前に発生します。
一発で完璧なスクレイピングはできません。

自分が想定していない動きや要素数を検出したら全て throw new Error("◯◯でエラーが発生しました。") としておきましょう。そのときは処理が止まってしまいますが、コードを修正して既知の問題にしておきましょう。

例外処理を使いまくって継続的なスクレイピングが可能なコードを作り上げていきましょう!

2-4. パフォーマンスを犠牲にしてでも書き込みは都度行おう!

データ毎にファイルやDBに書き込むのはパフォーマンスの観点から避けたいかもしれません。しかし、スクレイピングにおいては都度書き込むようにしましょう。後でまとめて書き出すために配列で保持しておくなどはやめましょう。

なぜならスクレイピングは当たり前に処理が失敗します。プロキシ、サーバーレスポンス、IPブロック、サイトの更新など要因は様々。処理が止まってしまうと適切な書き方をしていなければ、これまでスクレイピングしたデータを失ってしまいます。

何度もやり直していては永遠に終わらないので、"失敗する前提" で書き出しをするようにしましょう。

3. CSVでデータを取り扱う

3-1. CSVとしてデータを書き出す

https://www.npmjs.com/package/csv-writer

csv-writer というパッケージが使いやすくてオススメです。
若干使いにくさも感じていて、そのあたりをカバーする関数を書いておきます。

※下記折り畳んであります。

csv-writerを快適に使うための関数

▼ 新規ファイル作成

_functions.ts
import fs from "fs/promises";

export async function createNewFileCSV(outputFilePath: string, columnNames: string[]) {
  await fs.writeFile(outputFilePath, columnNames.join(",") + "\n", "utf-8");
}

▼ csv-writerのセットアップ

_functions.ts
export function createCsvWriter(outputFilePath: string, columnNames: string[], append = false) {
  const header: {
    id: string;
    title: string;
  }[] = [];

  columnNames.map((theColumnName) => {
    header.push({ id: theColumnName, title: theColumnName });
  });

  return createObjectCsvWriter({
    path: outputFilePath,
    header: header,
    append: append,
  });
}
使い方

CSVファイルを新規作成してデータをCSVで書き出す

import path from "path";
import { createNewFileCSV, createCsvWriter } from "./_functions";

// CSVの項目名を定義する
const csvColumnNames = ["prefecture", "city", "town"];
// 出力先を決定する
const outputFilePath = path.resolve(__dirname, "./sample.csv");

(async () => {
  // CSVファイルを新規作成する
  await createNewFileCSV(outputFilePath, csvColumnNames);

  // 今回書き込むデータ
  const results = [
    {
      prefecture: "愛知県",
      city: "名古屋市中区",
      town: "上前津"
    }
  ]

  // csv-writerを準備する
  const csvWriter = createCsvWriter(outputFilePath, csvColumnNames, true);
  // csv-writerで書き込みを実行する
  await csvWriter.writeRecords(results);
})();

カラムの中にJSON形式でデータを格納したい場合は、 JSON.stringify() で文字列に変換することで実現可能です。

3-2. CSVデータをJSONとして呼び出す

CSVファイルを読み込んでJSONとして使う方法も紹介します。

https://www.npmjs.com/package/csvtojson

csvtojson というパッケージが使いやすいです。

import path from "path";
import csv from "csvtojson";

const inputFilePath = path.resolve(__dirname, "./sample.csv");

(async () => {
  const data: {
    prefecture: string;
    city: string;
    town: string;
  }[] = await csv({
    noheader: false, // カラム名の有無(false -> カラム名あり)
  }).fromFile(inputFilePath);

  // 1行ずつ処理していく
  for (let i = 0; i < data.length; i++) {
    console.log(data[i].town);
  }
})();

JSON.stringify() で格納されたデータは JSON.parse() でJSONに戻すことができます。

ブラウザを表示せずにスクレイピングしたい!

ブラウザを表示せずにバックグラウンドでスクレイピングしたいと考えていませんか?表示せずに使えるブラウザを「ヘッドレスブラウザ」と言います。Google Chromeもヘッドレスモードに対応しています。

※画面に表示されないだけでいつものブラウザと一緒です。JSもCSSも全て処理してくれています。目に見えないだけです。

await chromium.launch({
  headless: true
});

headlessオプションを true にするだけでヘッドレスモードで実行してくれます。

何も表示されずに動くため何をやってくれているか全く分かりません。未完成のスクレイピングコードの状態のときはヘッドレスモードを無効化して表示しながらコードを書いていきましょう!

ほぼ完璧に動くコードが書けてから有効化しましょう!

位置情報やマイクなどの許可を求めるポップアップを出ないようにする

await chromium.launch({
  args: ["--deny-permission-prompts"]
});

argsのオプションとして --deny-permission-prompts を付与してあげるだけで実現できます。逆に許可したい場合は、 .grantPermissions() を使うことでできます。

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

スクレイピングの高速化

PlaywrightでのスクレイピングはJSやCSSや画像などのデータが含まれるため、ページ表示に時間がかかってしまいます。これがスクレイピング速度を遅らせる原因となっています。

ページ表示速度を爆速にするための手段として、 .route() という機能があります。"阿部寛のホームページ" 級に軽くできます。

https://zenn.dev/masa5714/articles/d798c4fa5f8b08

https://zenn.dev/masa5714/articles/c8973bd32b5b30

Canvasで作られたサイトのスクレイピング

最近は Flutter で作られたサイトも増えてきました。
中には CanvasKit を採用しているものもあります。

CanvasはHTML要素と異なりデータアクセスができません。
これは困った。

でも実はCanvasで作られていてもスクレイピングする術が残されています。
.waitForResponse() を用いてNetworkからfetchデータを盗み見してしまうという方法です。僕は今のところ、これが最適解だと思っています。

下記に記事を書きましたのでご覧ください。
https://zenn.dev/masa5714/articles/639b1cfc246abe

他に良い方法があればコメント欄で教えてください!

CAPTCHA対策

CAPTCHAが実装されてるサイトのスクレイピングは不可能です。
というか、スクレイピングやスパム対策として作られたものなので、これを突破しようとするのは不適切です。(スクレイピングが適切かどうかは置いといて...。)

中にはCAPTCHAを突破するためのサービス(2Captcha)が提供されていますが、これを使うのはスクレイパーとして、なんか許せないです。こんなサービスを使うぐらいなら諦めてください。

ただし、人力と機械のハイブリッドなスクレイピングであれば個人的に良しとしています。(都合良すぎ!!)

ということで、CAPTCHAが出現したときに自分がCAPTCHA突破するのを待機する処理を紹介します。

const waitForCAPTCHA = (page) => {
  return new Promise(async (resolve) => {
    const counter = setInterval(async function () {
      const path = await page.url();
      if (path.match(/google\.com\/sorry\/index/)) {
        console.log("停止中");
      } else {
        console.log("続行します");
        clearInterval(counter);
        resolve("");
      }
    }, 500);
  });
};

// 使う
(async () => {
  // ...中略 const page
  await waitForCAPTCHA(page);

  console.log("CAPTCHAを突破したので再開する");
})();

これは google.com/sorry/index が含まれるURLに到達した場合に処理が停止されるコードです。CAPTCHAを突破すると処理が再開されます。

成功するまで規定回数までリトライするには?

スクレイピングは失敗が当たり前です。
データを受け取るまでに時間がかかったり、処理をしてるコンピュータ側の処理が追いつかないなど様々な要因で失敗することが多々あります。そんなとき、再実行のたびにコマンドを打っていては疲れてしまいます。そこで規定回数までリトライする方法をご紹介します。

※結構長くなってしまうので下記に折り畳んであります。

規定回数までリトライをする例
import { chrome } from "@utils/functions/browser";

(async () => {
  let { page, browser, waitForResponseResource } = await chrome(true);
  let retryCount = 0; // リトライが発生でインクリメント
  const configRetryNum = 10; // リトライの規定回数を定義
  let browserRefresh = false; // 再起動後にブラウザ起動の無限ループに入らないように対策

  // スクレイピングするデータを用意する
  const searchKeywords = ["REIRIE", "金子理江", "黒宮れい"];
  let i = 0;

  while (true) {
    try {
      if (retryCount >= configRetryNum) throw new Error(`Error: リトライ上限に達しました。`);

      // プロキシの帯域幅を節約するための記述
      await page.route(/(\.js.*|\.css.*)/, (route) => {
        return route.abort();
      });

      // ※for文のiは一番最後にインクリメントされるよ
      for (; i<searchKeywords.length; i++) {
        if ((i + 1) % 20 === 0 && !browserRefresh) {
          browserRefresh = true;
          throw new Error(`【メモリ解放】route処理が原因で重くなる問題を解決するためPlaywrightを再起動したい`);
        }
        browserRefresh = false;

        // 【ここにスクレイピング処理を書いていく】
        // 例:データを用いてスクレイピングしていく。
        await page.goto(`https://example.com/search/${searchKeywords[i]}`);

        // スクレイピング処理成功したのでリトライ数をリセット
        retryCount = 0;
      }

      // 全てのデータのスクレイピングが完了したらここまで到達する
      await browser.close();
      break; // whileループを抜ける
    } catch (error) {
      await browser.close();

      // 処理中にデータ関連で未知の挙動に遭遇したとき処理を中断する
      // 下記はエラーメッセージに「要調査」の文言が含まれていたら発火する
      if (typeof error === "string" && error.match(/要調査/)) {
        console.log(error);
        break; // whileループを抜けて処理を中断
      }

      // エラーメッセージを出力する
      console.log(error);

      if (retryCount >= configRetryNum) {
        console.log(`リトライが ${configRetryNum} 回連続で発生したため処理を強制終了します。`);
        break; // whileループを抜けて処理を中断
      }

      // ここまで到達できたらただの失敗。
      // リトライに向けて準備を行う
      retryCount = retryCount + 1;
      // 冒頭で呼び出してる関数と同じものを全て上書きする必要がある
      ({ page, browser, waitForResponseResource } = await chrome(true));
    }
  }
})();

スクレイピング予定のデータをメモリに保持しておき、
それをループして失敗したり、予定にない動きをしたらとにかく throw して例外処理へ。例外処理の中でブラウザ再起動の処理を行っています。

エラーメッセージの中に「要調査」とつければ未知の挙動を調査するために停止してくれるようになります。

全ての処理が終わったタイミングや任意のタイミングで break を使って無限ループから抜けて処理を完了するという流れです。仕組み自体はシンプルですがコードがややこしくて辛いですね。

例外エラーはデスクトップ通知が来るようにしておくと便利!(超簡単に導入できます)

VPSを用意するほどでも無い規模(3日以内で終わるようなもの)なら、デスクトップ通知が来るようにしておくとすごく便利です。例えば例外エラーを検出したら通知が来たり、処理が完了したら通知が来るようにするなど...。

node-notifier を使えば数行で導入できてしまいます。

import notifier from "node-notifier";

notifier.notify({
  title: "エラー - Playwright",
  message: "hogeを検出しました。",
  sound: true, // OS標準の通知音を鳴らしてくれる
  wait: false, // 通知の確認を待たずに終了する
});

すごく簡単に導入できるのでぜひ!
これをつけておけば、仮想デスクトップにしておいて裏側で処理させておけば、スクレイピングしてる感覚を捨てて日常生活を送れます!

extra. 付録

分類できないおまけ情報を掲載しています。

extra-1. Googleの検索結果のリンクが取得できるCSSセレクタ

Googleの検索結果ではclass名が変わってしまいます。継続的なスクレイピングをするならば、class名に頼らないCSSセレクタが必要です。

document.querySelectorAll('a[href^=http]:not(a[href*=google],a[class]):has(h1,h2,h3,h4,h5,h6):not(:is([jsslot],ul) a[href^=http]:not(a[href*=google],a[class]):has(h1,h2,h3,h4,h5,h6))')

僕が作ったこのセレクタを使ってみてください。
開発者ツールのConsoleで上記を実行してみると、検索結果のリンクだけが取得できるかと思います。(スポンサー、ニュース、マップ、目次リンクや画像リンクなど余計なものが除外されている。)

我ながら力作のセクレタです。

extra-2. 正規表現が苦手な人に使って欲しい Regex101

スクレイピングでは正規表現をかなり使います。
でも正規表現難しくて苦手じゃないですか?僕は超苦手です。

そこで、

https://regex101.com/

がオススメです。
ブラウザ上で手軽にテストできるのでちゃんと動く正規表現が作りやすくなります。このツールのおかげで比較的楽に正規表現に向き合えています。

extra-3. 並列処理でスクレイピングしたい

単体のPlaywrightでは並列でスクレイピングは難しいです。
最も簡単な方法は Selenium Grid に接続するという手段です。

興味がある方は下記をご覧ください。

https://zenn.dev/masa5714/articles/01d8f89fff1b68


調査中

まだ実践してなくて調査中の内容を掲載しています。
実践後に記事に追加します。

調査中-1. 任意のリクエストを作成して発火する

まだ調査中で試してないので何も分かりませんが、CORSを発生させずに任意のリクエストを作成して実行できる機能もありそうです。(UI上にボタンが無くても任意のURLに form-data を添えてリクエストするようなことができそう?)

https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-fetch

試して動きそうであれば詳しく追記しておきます。

調査中-2. 隣接する要素を取得する

下記のようにそれぞれが囲われておらず、なおかつ隣接する要素が増減するケースに対応する方法を確立させたい。

Aパターン
<div class="list">
  <h1>REIRIE</h1>
  <p class="name">金子理江</p>
  <p class="instagram">https://www.instagram.com/bite_me_3/</p>
  <p class="name">黒宮れい</p>
  <p class="instagram">https://www.instagram.com/suicide_u/</p>
</div>
Bパターン
<div class="list">
  <h1>REIRIE</h1>
  <p class="name">金子理江</p>
  <p class="name">黒宮れい</p>
  <p class="instagram">https://www.instagram.com/suicide_u/</p>
</div>

Aパターンは .name も .instagram も存在してるので、

const nameElements = await page.locator('.name');
const instagramElements = await page.locator('.instagram');

for (let i = 0; i < await nameElements.count(); i++) {
  const nameText = (nameElements.nth(i).innerText()).trim();
  const instagramText = (instagramElements.nth(i).innerText()).trim();
}

とできる。
しかし、Bパターンでは .instagram が一部抜けているため for 文で回してしまうとズレてしまい、不適切なデータ取得となってしまう。

現時点の解決策としては .evaluate() を用いてChrome側でJSを走らせて、要素を囲って使いやすい形に変更するのが良さそうです。jQueryの .wrapAll() 相当の処理を行ってから await page.locator('.hoge') で要素を取得するという流れ。

つまり、

Bパターン
<div class="list">
  <h1>REIRIE</h1>
  <div class="profile">
    <p class="name">金子理江</p>
  </div>
  <div class="profile">
    <p class="name">黒宮れい</p>
    <p class="instagram">https://www.instagram.com/suicide_u/</p>
  </div>
</div>

となるように頑張ってJSで操作するということです。

Discussion