Claude Media
Claude CodeでPuppeteerのスクレイピングを書かせる — waitForSelectorと件数の検算

Claude CodeでPuppeteerのスクレイピングを書かせる — waitForSelectorと件数の検算

Puppeteerのスクレイピングスクリプトを書かせるとき、nodeの許可の切り方、waitForSelectorの待機条件、取得件数の検算をどう組むか。手元の再現出力つきでまとめます。

Claude CodeでPuppeteerのスクレイピングを書かせる — waitForSelectorと件数の検算

Puppeteerのスクレイピングは、書かせること自体は難しくありません。つまずくのは3か所です。node を実行する許可をどこまで開けるか。JavaScriptで描画される要素をどう待つか。取れた結果が本当に全件かを誰が確かめるか。

この記事は、その3点を順に決めていく手順です。待機と検算は、手元のローカルページで動かした出力を載せます。

許可は「実行するコマンド」の単位で切る

Puppeteerのスクリプトは node scripts/scrape.mjs のように実行します。Claude Codeの権限ルールはコマンド文字列を見るので、ここを具体的に書くのが出発点です。

* は任意の文字列に一致し、末尾の * は引数なしの実行にも一致します。Bash(npm run *) を許可して Bash(git push *) を拒否する例が、権限の設定ページに載っています。同じ形をスクレイピング用に写すと次のようになります。

{
  "permissions": {
    "allow": [
      "Bash(node scripts/scrape.mjs *)",
      "Bash(npm run scrape *)"
    ],
    "deny": [
      "Read(./.env)"
    ]
  }
}

npxは包んで許可しない

ブラウザの取得で npx puppeteer browsers install を使う場面があります。npx や docker exec のような環境ランナーは、timeout や nice と違って自動では外されません。つまり Bash(npx *) と書くと、npx の後ろに来る任意のコマンドを許可します。

安全に書くなら、ランナーとその中のコマンドの両方を含む具体的なルールを、コマンドごとに1つずつ置きます。ブラウザの取得は初回だけの作業なので、ルールを足さずに都度確認で通すのが手堅い運用です。

許可しても守れないもの

ここに落とし穴があります。許可ルールが見るのはコマンド文字列で、スクリプトの中身ではありません。node scripts/scrape.mjs を許可した時点で、そのファイルを書き換えられれば、中のコードも許可なしで走ります。

読み取りの拒否ルールも同じ制約を受けます。ReadとEditの拒否ルールは、ファイル名を指定せずに読む場合や、PythonやNodeのスクリプトがファイルを自分で開く場合には効きません。.env を守りたいなら、OSレベルで効くsandboxを併用します。権限ルールとsandboxの併用は、多層防御という位置づけです。

スクレイピングでは、通信先も絞れます。sandboxのネットワーク許可リストは、権限ルールのドメインと合算されて最終的な設定になります。接続でつまずいたときはsandboxから繋がらないときの切り分けが参考になります。Linuxで同梱Chromeの起動時に「No usable sandbox!」が出たなら、allowNestedUserNamespacesの記事に3つの直し方があります。

Puppeteerのブラウザ取得がブロックされたときの直し方

npm i puppeteer はインストール時に互換のChromeを取得します。ブラウザ不要なら puppeteer-core を使います。

つまずく点が1つあります。npm、pnpm、Yarn、Bunなど最近のパッケージマネージャは、依存パッケージのインストールスクリプトを既定でブロックします。ブロックされるとブラウザが取得されず、実行時にエラーになります。その場合は次のコマンドで手動取得します。

npx puppeteer browsers install

Claudeに頼むときは、ここで止まる可能性を先に伝えておくと手戻りが減ります。

waitForSelectorで「描画後」まで待つ

スクレイピングで起きやすい失敗の1つは、要素が描画される前に取りに行くことです。page.waitForSelector は、セレクタが現れるまで待つメソッドです。

オプション意味既定値
visible意味DOMにあり、かつ display: none や visibility: hidden でない既定値false
hidden意味DOMにない、または非表示になるまで待つ既定値false
timeout意味待つ上限(ミリ秒)。0 で無効既定値30000

セレクタが現れないままタイムアウトすると、例外が投げられます。hidden: true で対象がDOMに無かった場合は null を返します。待ち時間の既定は page.setDefaultTimeout() で変えられます。

手元で確かめた差

800ミリ秒後に li.item を5件描画するローカルページを用意しました。Node v20.18.0、Puppeteer 24.43.1で試した結果です(実在のサイトではなく、手元の簡易サーバーに対する実行です)。

取り方結果
page.goto の直後に page.$$('.item')結果0件
waitForSelector('.item', { visible: true }) の後に取得結果全件

goto が返っても、後から描画される要素は存在しません。存在しないセレクタを待つと、TimeoutError: Waiting for selector `.nothing` failed で止まります。待機の失敗は、黙って空配列を返さず、例外で落ちる形になります。

固定の setTimeout で待つ書き方は、遅い回で取り逃し、速い回で無駄に待ちます。Claudeに書かせるときは「待機は waitForSelector で、timeout を明示すること」を指示に含めます。

非表示の要素と、増え続けるリスト

waitForSelector の既定は「DOMに存在すればよい」です。display:none の要素を置いたページで試すと、既定では約34ミリ秒で解決しました。visible: true を付けると、同じ要素に対して TimeoutError になりました(待ち上限は800ミリ秒に設定)。画面に出ていないテンプレート用の要素を拾いたくないときは、visible: true を付けます。

もう1つは、読み込みが続くリストです。waitForSelector はセレクタが現れた時点で戻るので、「1件目が出た」ことしか保証しません。200ミリ秒ごとに行が1つずつ増えて計6行になるページで、page.waitForFunction(() => document.querySelectorAll('.row').length >= 6) を使うと、戻った時点で6件そろっていました。期待件数が分かる場面では、件数を条件にした待機のほうが、後段の検算と噛み合います。

期待件数が分からないときは、件数が連続して変わらなくなるまで待つ書き方になります。無限スクロールのページは件数が止まるタイミングがサイトごとに違うので、スクロール回数の上限と打ち切り条件をスクリプトに入れます。

取得件数の検算をスクリプトに入れる

待てば取れる、とは限りません。ページには「全5件」と表示されているのに、4件しか描画されない状態は、待機を入れても起きます。

検算は、画面上の件数と取得件数を突き合わせるのが最も安上がりです。次のスクリプトは、手元で動かしたものです。

import puppeteer from 'puppeteer';
 
const [url] = process.argv.slice(2);
const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.goto(url);
  await page.waitForSelector('.item', { visible: true, timeout: 5000 });
 
  const items = await page.$$eval('.item', els => els.map(el => ({
    title: el.querySelector('.title')?.textContent?.trim() ?? null,
    price: Number(el.querySelector('.price')?.textContent?.replace(/\D/g, '')),
  })));
 
  const text = await page.$eval('#total', e => e.textContent);
  const expected = Number(text.match(/\d+/)[0]);
  console.error(`取得: ${items.length} 件 / 表示上の件数: ${expected}`);
  if (items.length !== expected) {
    throw new Error(`件数不一致: ${items.length} != ${expected}`);
  }
  if (items.some(i => !i.title || Number.isNaN(i.price))) {
    throw new Error('欠損あり');
  }
  console.log('検算OK');
  console.log(JSON.stringify(items.slice(0, 2)));
} catch (e) {
  console.error(`FAIL ${e.message}`);
  process.exitCode = 1;
} finally {
  await browser.close();
}

ローカルページを4件だけ描画する設定にして実行すると、次のように止まりました。

$ node scripts/scrape.mjs http://localhost:8080/
取得: 4 件 / 表示上の件数: 5
FAIL 件数不一致: 4 != 5

終了コードは1になるので、Claudeは「成功した」と報告できません。5件に直して実行し直すと「検算OK」と最初の2件が出て、終了コードは0で終わります。検算なしでスクリプトを書かせると、4件の欠けた結果がそのままJSONに保存されます。

文字化けも検算で見つかる

実行中に、別の問題も見つかりました。最初に動かしたときは、サーバーが文字コードを返さず、取得したタイトルが 譛ャ1 のように文字化けしました。件数も価格も合っていたので、件数の検算だけでは通ります。

HTMLに <meta charset="utf-8"> を足すと 本1 になりました。日本語のサイトを対象にするときは、検算に「タイトルが空でない」だけでなく、「サンプル1件を目で読む」ステップを足します。

落とし穴

件数が合っていても中身が壊れていることがある

検算は件数と欠損の有無しか見ません。文字化けや、別の要素を拾った誤取得は通ります。最初の1〜2件を出力させて、Claudeではなく自分の目で読む工程を残します。

CLAUDE.mdに書く固定ルール

毎回プロンプトに書くのは手間なので、CLAUDE.mdに固定します。次は、この記事の内容を写した断片の例です。

## Puppeteer の固定ルール
- 実行は `node scripts/scrape.mjs <url>` に限る(npx は使わない)
- 要素の待機は waitForSelector で行い、timeout を必ず明示する
- 取得後に、画面上の件数との一致と、必須フィールドの欠損を検査する
- 検算に失敗したら終了コード 1 で終わり、結果を保存しない
- 1回の実行で最初の2件を標準出力に出して、中身を確認できる形にする

アクセス先の作法(robots.txtやリクエスト間隔)は別の話です。そちらはScrapyでのrobotsとDOWNLOAD_DELAYの扱いで整理しています。Puppeteerで同じ配慮をするなら、リクエスト間隔をスクリプト側に実装する必要があります。

依頼文の例と、失敗したときの回し方

固定ルールを置いたうえで、依頼は対象と合格条件だけに絞ります。次はその一例です。

scripts/scrape.mjs を作って、<対象URL> の商品一覧を取得してください。
- 取得項目は title と price
- 画面上の「全N件」と取得件数が一致しなければ失敗にする
- 実行して、終了コードと最初の2件を見せてください

実行して 件数不一致 で落ちたら、その出力をそのままClaudeに返します。原因の候補は、待機条件が早すぎる、セレクタが別の要素も拾っている、ページ分割の見落とし、の3つです。どれかを決め打ちせず、「どの待機条件に変えたか」と「変更後の件数」を報告させると、直した根拠が残ります。

PuppeteerのMCPで動かす場合は許可の書き方が変わる

Puppeteerベースのブラウザ自動化・デバッグ用MCPサーバーとして、chrome-devtools-mcp があります。この経路では、スクリプトではなくMCPツールを呼びます。

権限ルールは、mcp__<server>__* でサーバー内の全ツール、mcp__<server>__<tool> で単独のツールを指します。たとえば mcp__puppeteer__* が puppeteer サーバーの全ツールに一致します。allowルールでは、mcp__ の後ろのサーバー名にワイルドカードを使えません。

スクリプトを書かせる場合と、MCPで操作させる場合の違いは、再実行できる成果物が残るかです。件数を毎回検算したいなら、スクリプトのほうが向きます。MCPでの調査の流れはChrome DevTools MCPでLCPの原因を絞る手順が具体例です。

テスト目的なら、Playwrightへの切り替えも選択肢です。待機の考え方の違いはSeleniumから移行するときの記事で比べています。

まとめ

許可は node の実行ファイル単位で切り、npx は包まずに都度確認します。待機は waitForSelector に timeout を付け、検算は画面上の件数との一致で組みます。

この3点を指示とCLAUDE.mdに固定すると、スクリプトを書かせる作業は「動いた」ではなく「全件取れた」で終わるようになります。ただし、許可ルールはスクリプトの中身を見ないため、通信先と読み取り範囲はsandboxで絞ります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/permissions
  2. pptr.dev/
  3. pptr.dev/api/puppeteer.page.waitforselector
この記事を共有:XはてブLinkedIn