Playwright + Selenium Gridでプロキシを刺してスクレイピングする
※この記事はスクラップで書いた内容を記事としてまとめ直したものです。
小規模なスクレイピングであればPlaywrightをそのまま使えば問題ありませんが、それなりの規模でスクレイピングを実施するとなると、Selenium Gridに頼りたくなることでしょう。また、規模が大きくなると必然的にプロキシを刺しながら実施することになります。
複数のスクレイピングを並列で実行することもできます。
Selenium 4ではDynamic Gridと呼ばれる作業ノードを作成したり破棄したりを自動でやってくれる超絶便利な機能が備わっています。この記事はDynamic Gridを使っています。
それなりの規模のスクレイピングを実施するためのアイデアの種となりうる情報を残しておきたいと思います。(日本語のスクレイピング情報がかなり少ないので皆さんもぜひ書いてください!!)
0. 必要なものを準備する
- docker-selenium( zip形式でダウンロードして展開しておいてください。 )
 - Docker(docker-composeを使える状態にしておくこと。)
 - 有料プロキシ( 安価で高品質な WebShareがオススメです! )
 
1. プロジェクトフォルダを作成して必要なファイル群を格納する
まずはご自身のスクレイピング用のプロジェクトフォルダを作成しておきましょう。
この記事では例として sample-scraping というフォルダ名にしておきます。
docker-selenium でダウンロードしたものから下記のファイルとフォルダをプロジェクトフォルダに格納してください。
- 【フォルダ】NodeDocker
 - 【ファイル】docker-compose-v3-dynamic-grid.yml
 
次に docker-compose-v3-dynamic-grid.yml を docker-compose.yml というファイル名に変更しておきましょう。
ここまでできると下記のディレクトリ構造になります。
sample-scraping/
├─ 📄 docker-compose.yml
├─ 📁 NodeDocker/
2. docker-compose.yml に記述を追加する
次に、WebDriverから ChromeDevTools(CDT)の操作を可能にするため、必要な記述を追記しておきましょう。(この記述を追加しないとブラウザの立ち上げができたとしてもブラウザ操作が実行されません。僕はここで時間めちゃくちゃ溶かしました...。)
node-docker サービスの環境変数として - SE_NODE_GRID_URL=http://localhost:4444 を追加してあげる必要があります。
追記した状態のdocker-compose.yml の全文は下記の通りです。
version: "3"
services:
  node-docker:
    image: selenium/node-docker:4.17.0-20240123
    volumes:
      - ./assets:/opt/selenium/assets
      - ./NodeDocker/config.toml:/opt/bin/config.toml
      - /var/run/docker.sock:/var/run/docker.sock
    depends_on:
      - selenium-hub
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SE_NODE_GRID_URL=http://localhost:4444
  selenium-hub:
    image: selenium/hub:4.17.0-20240123
    container_name: selenium-hub
    ports:
      - "4442:4442"
      - "4443:4443"
      - "4444:4444"
これでDockerコンテナを立ち上げる準備ができました!
3. Dockerコンテナを立ち上げてSelenium Gridを確認する。
それでは、Dockerコンテナを立ち上げてみましょう。
docker-compose up -d
これで node-docker と selenium-hub の2つのコンテナが立ち上がります。
コマンドを実行してから1分ほど待ってから下記にブラウザでアクセスします。
http://localhost:4444/

上記の画面が表示されていれば成功です!
画面について簡単に説明すると、
- Sessions -> ブラウザ操作実行中の数
 - Max. Concurrency -> 最大同時実行可能数(恐らくCPUスレッド数に応じて変化するものと思います。論理プロセッサ数)
 - 【サイドバー】Sessions -> 実行中の状態をチェックできます。(各ノードの状態を確認する際に聞かれるパスワードは 
secretがデフォルトです。) 
4. PlaywrightでSelenium Gridに接続してスクレイピングしてみよう
ここからはPlaywright側のコードの記述を書いていきます。
.env ファイルに下記を追記します。
SELENIUM_REMOTE_URL=http://localhost:4444/
次にスクレイピングのコードを書いていきます。
import "dotenv/config";
import { chromium } from "playwright";
(async () => {
  const browser = await chromium.launch({
    headless: false,
  });
  const context = await browser.newContext();
  const page = await context.newPage();
  await page.goto("https://yahoo.co.jp");
  const hoge = await context.newPage();
  // await browser.close();
})();
await browser.close() をコメントアウトしているのは、実行状況を目視でチェックするためにあえて終了しないようにしています。
それでは、スクレイピングコードを実行してみましょう。
npx ts-node index.ts
index.tsを実行すると、Selenium Gridに接続され...

Session数が1カウントされ... 左側のSessionタブをクリックしてみると、

何やらカメラっぽいアイコンが追加されました。
これをクリックしてみましょう。
パスワードを聞かれるので secret という文字列を入力して ACCEPT します。

命令どおり、Yahoo!JAPANを開いて新規タブを開いてくれていることでしょう。
無事にPlaywright + Selenium Gridの環境が整いました!
await browser.close(); しない場合は5分後に自動的に作業ノードが破棄されます。5分待てない場合はDocker Desktopから自動的に作成された作業ノードのコンテナを破棄してください。(Selenium 4から備わっているDynamic Gridの内部処理として、作業ノードの立ち上げをDockerコンテナで行っているらしいです。リクエスト飛ばすだけで勝手にお世話してくれるの最高すぎ!!)
5. プロキシを刺してスクレイピングしたい!
Playwrightを単体で使う場合、特に気にする必要はありませんが、
Selenium Gridを経由するとプロキシを刺したときに問題が発生します。
その問題とは、「ベーシック認証が発生してしまう」 です。
これがかなり厄介でコードレベルで対応しようとすると、動的に拡張機能を生成してそれを適用する必要があるのです。(http://hoge:fuga@111.111.111.111/ みたいな書き方でも突破できない。)コードも複雑になる上、都度破棄するなどの管理も大変なので動的生成は極力避けたいところです。
ではどうするかと言うと、「IP認証機能」を使いましょう!
有料のプロキシサービスでは接続元のIPアドレスを信頼して認証を通してくれる機能が適用されていることがあります。僕が気に入って使ってる WebShare にも備わっています。

上記のようにIPアドレスを指定しておくだけで使えてかなり便利です。
また、WebShare ではAPIが提供されてるので、プログラミングで色々できる点も最高にGOODです。
IP認証の設定をした状態にしておけば、Playwrightでは下記のように書くだけでプロキシに接続できます。
  const browser = await chromium.launch({
    proxy: {
      server: "http://hoge:port",
    },
  });
かなりシンプルに使えます。
ちなみに僕は毎度色々書くの面倒なので下記のようにして、プロキシをランダムでピックするようにしています。
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);
  });
};
import "dotenv/config";
import { getRandomTheProxy } from "@utils/functions/proxy";
import { chromium, Browser, BrowserContext, Page } from "playwright";
export const chrome = (): Promise<{
  browser: Browser;
  context: BrowserContext;
  page: Page;
}> => {
  return new Promise(async (resolve) => {
    const proxyURL = await getRandomTheProxy();
    const browser = await chromium.launch({
      headless: false,
      args: ["--blink-settings=imagesEnabled=false", "--disable-remote-fonts"],
      proxy: {
        server: `http://${proxyURL}`,
      },
    });
    const context = await browser.newContext();
    const page = await context.newPage();
    resolve({
      browser,
      context,
      page,
    });
  });
};
実行ファイルとしては下記のように書いて使います。
すごくシンプルになります。
import { chrome } from "@utils/functions/browser";
(async () => {
  const { browser, context, page } = await chrome();
  await page.goto("https://yahoo.co.jp/");
  await browser.close();
})();
おまけ:プロキシを使うときは帯域幅の節約を意識しよう!
有料プロキシでは帯域幅に応じて課金される仕組みが採用されています。
帯域幅は低コストでスクレイピングする際に最も気をつけたい点です。
帯域幅を節約するコツとして、
- Webフォントの受信を無効にする
 - 画像の受信を無効にする
 
が簡単かつ効果的です。
先ほど挙げたコードの中では下記の部分です。
    const browser = await chromium.launch({
      args: ["--blink-settings=imagesEnabled=false", "--disable-remote-fonts"],
    });
立ち上げの際に渡せる argsで --blink-settings=imagesEnabled=false が画像の無効化、 --disable-remote-fonts がWebフォントの無効化です。
僕が気付けていないもっと効果的な設定があるかもしれません。
詳しくは下記を参照してください。
また、スクレイピングに関して有益な情報があればコメント欄にガンガン投稿していってください!ご自身が書いた記事の宣伝URLも大歓迎です!
Discussion