モダンなWebページを開き、ネットワークの通信を観察すると、欲しいデータがページに描画される前にJSONとして届いているのをよく目にするはずです。スクレイパーがHTMLから苦労して抽出するはずの価格、リスト、結果が、ページ自身が取得したクリーンで構造化されたレスポンスの中に収まっています。
そのレスポンスから収集する方が、描画済みのページから収集するよりも小さく、安定していて、はるかにパースしやすくなります。しかし、URLをコピーするほど単純ではなく、ほとんどのチームが驚く点が2つあります。ページのJSONのうち実際にデータである部分がいかに少ないか、そしてデータエンドポイントがそれを呼び出したページの外では動作を拒否することがいかに多いか、です。本ガイドでは、正しいエンドポイントの見つけ方、実際のページで計測した内容、そして行き過ぎることなくそこから収集する方法を示します。
主なポイント
- ページが読み込むJSONの大半はデータではありません。ある航空会社のフライト検索ページでは、運賃は44.9 KBの単一レスポンスに含まれており、これはページのJSON全体の約8%、ページの総転送量3.6 MBの約1.2%に過ぎませんでした。
- データAPIが全く存在しないページもあります。ある国の気象局の予報ページでは、すべてのJSONレスポンスがクッキー同意ツールのものであり、予報自体はHTMLに含まれていました。
- エンドポイントを見つけるには、URLから推測するのではなく、ページ上で見える値をレスポンスボディの中から検索してください。
- ページのAPIはそのページのセッションに紐づいていることがよくあります。この航空会社の運賃エンドポイントはページ内では機能しましたが、直接呼び出すとHTTP 409を返しました。
- 匿名の訪問者向けにページが呼び出す公開エンドポイントのみを、人がブラウジングする程度のペースで使用し、公式APIが存在する場合は常にそれを優先してください。
計測した内容
2026年9月30日に、実際のブラウザで2つの公開ページを読み込み、すべてのレスポンスを記録しました。
| 航空会社のフライト検索 | 天気予報 | |
|---|---|---|
| リクエスト数 | 61 | 46 |
| 総転送量 | 3.62 MB | 2.57 MB |
| JSONレスポンス | 15件、534 KB | 3件、1.04 MB |
| データを含むJSONレスポンス | 1件、44.9 KB | 0件 |
| データの所在 | 運賃エンドポイント | サーバーレンダリングされたHTML |
航空会社のページでは、最大のJSONレスポンスはそもそも運賃ではありませんでした。サードパーティのフィーチャーフラグサービスが同じ97 KBのレスポンスを4回返し、翻訳バンドルがさらに92 KBを加えていました。運賃は航空会社自身の予約APIからの単一のレスポンスで届きました。
天気予報のページでは、3件のJSONレスポンスすべてが同意管理ツールからのもので、そのうち1件は860 KBのベンダーリストであり、予報自体はサーバー側でHTMLにレンダリングされていました。「JSONから収集する」というアプローチでは、何も有用なものは得られなかったはずです。
データを含むエンドポイントを見つける
信頼できる方法は、推測ではなく検索することです。価格、商品名、識別子など、ページ上で見える値を選び、実際のブラウザでページを読み込み、その値を含むJSONレスポンスを探します。
手作業で行う場合、これはブラウザの開発者ツールで行います。Networkタブを開き、Fetch/XHRでフィルタし、リロードして、レスポンスボディの中からその値を検索します。自動化する場合は、以下のような短いスクリプトで済みます。
import { chromium } from 'playwright';
// Load a page once and report which JSON responses contain a value you can see on it.
export async function findEndpoint(url, needle, waitMs = 20000) {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
const hits = [];
page.on('response', async (response) => {
const type = response.request().resourceType();
const contentType = response.headers()['content-type'] || '';
if (!['xhr', 'fetch'].includes(type) || !contentType.includes('json')) return;
try {
const body = await response.text();
if (body.includes(needle)) {
hits.push({
method: response.request().method(),
status: response.status(),
bytes: Buffer.byteLength(body),
url: response.url(),
});
}
} catch {
// Some responses (redirects, aborted requests) have no readable body.
}
});
// Analytics beacons keep many pages from ever going network-idle,
// so wait for the DOM, then until a match appears or the time runs out.
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 90000 });
for (let waited = 0; waited < waitMs && hits.length === 0; waited += 500) {
await page.waitForTimeout(500);
}
await page.waitForTimeout(1000);
await browser.close();
return hits;
}
航空会社のフライトページと、そこに表示されている運賃を与えると、このスクリプトは61件のリクエストのうちちょうど1件、予約APIの空席状況レスポンスを一致として返しました。待機戦略に注目してください。最初のバージョンではページがネットワークアイドルになるのを待っていましたが、アナリティクスのビーコンが送信され続けるため、それは決して起きず、90秒でタイムアウトしました。DOMを待ち、その後一致が現れるのを待つ方がより確実です。
直接呼び出せるか
多くの場合、できません。同じ航空会社の空席状況エンドポイントをページを介さずに直接リクエストすると、6か国からのリクエストのいずれに対してもHTTP 409が返されましたが、ページ自体は問題なくそれを読み込みました。ページのAPIは、セッションクッキー、HTMLに埋め込まれたトークン、リクエストヘッダー、あるいは先行する一連の呼び出しなど、ページが事前に準備するものに依存していることが一般的です。
そこには2つのアプローチが残ります。
- ページに呼び出させる。 ブラウザでページを読み込み、上記のスクリプトのようにJSONレスポンスが届いた時点で読み取ります。ブラウザを実行するコストと引き換えに、セッションを再構築することなくクリーンなデータを得られます。
- シンプルで公開されたエンドポイントのみをリプレイする。 URLだけで済むエンドポイントもあります。同じ航空会社は、単純なリクエストに応答する公開運賃エンドポイントも公開しており、出国国によって価格が変わるかどうかを検証する別のテストでこれを使用しました。エンドポインがそれほど単純に動作する場合、それは圧倒的に最も安価な選択肢です。
してはいけないのは、セッションを回避することです。トークンを収集したり、ヘッダーを偽造したり、サイトが匿名の訪問者に提供していないデータに到達するために認証済みの呼び出しをリプレイしたりすることです。それは収集が観察であることをやめてしまう境界線です。
JSONが存在する場合になぜ価値があるのか
データが実際にJSONとして届く場合、その利点は本物です。
- サイズ。 運賃のレスポンスは44.9 KBで、ページ全体の3.6 MBに対してこの大きさでした。帯域幅で課金される収集においては、この差が請求額の大部分を占めます。これはクリーンなレコード1件あたりのコストが示す通りです。
- 構造。 フィールドは名前付きで型が定まった状態で届き、書いたり保守したりすべきセレクタはありません。
- 完全性。 レスポンスは、識別子、すべての運賃クラス、在庫フラグなど、ページに表示される以上のものを含んでいることがよくあります。
トレードオフも同じく本物です。ドキュメント化されていないエンドポイントは予告なく変わり、形が変わったレスポンスはパーサーを静かに壊します。これはまさにスキーマドリフトの監視が存在する理由です。この航空会社の予約APIのパスにあるv4のようなバージョン番号は、安定性のわずかな兆候であり、約束ではありません。
代替手段の中でのこの位置づけ
| ソース | 安定性 | 労力 | 使用すべき場面 |
|---|---|---|---|
| 公式のドキュメント化されたAPI | 最高 | 最低 | 常に最初に確認する |
| JSON-LDまたは埋め込まれたページ状態 | 高 | 低 | ページがそれを公開している場合、HTMLをパースするのをやめるを参照 |
| ページ自身のJSONエンドポイント | 中 | 中 | データがページの後に読み込まれ、エンドポイントが公開されている場合 |
| レンダリング済みHTMLとセレクタ | 最低 | 最高 | 他にデータを含むものがない場合 |
| ページを読むモデル | さまざま | サイトごとに低いがページごとに高い | テンプレートが多数ある場合、LLM抽出とセレクタの比較のように |
責任を持って収集する
ページが呼び出すエンドポイントはサイトの一部であり、サイトのルールは依然として適用されます。匿名の訪問者向けにページが呼び出すエンドポイントのみを使用し、人がブラウジングするようなペースでリクエストを行い、robots.txtとサイトの利用規約を尊重し、ログインやトークンの背後にあるものには許可がない限り手を出さないでください。サイトが公式APIを提供している場合はそれを使用してください。その方が安定していますし、それがサイトがサポートに合意しているものです。より広範な原則はrobots.txt、AIオプトアウト、予約シグナルにあります。
結論
モダンなページの背後にあるデータは、しばしばJSONとして届き、そうである場合、そこから収集する方がHTMLをパースするよりも小さく、クリーンで、保守しやすくなります。しかし、それを見つけるには推測ではなく検索が必要です。今回計測したページでは、有用なJSONは多数のレスポンスのうちの1件にすぎず、あるページではそもそも存在しませんでした。
見える値をレスポンスボディの中から検索し、エンドポイントが単独で動作するかどうかを確認し、動作しない場合はページに呼び出させ、サイトが匿名の訪問者に提供している範囲内にとどまってください。
出典と参考文献
- Shifterが2026年9月30日に上記のコードを使用して読み込み、計測したページ。
- Playwright、ネットワークイベントのドキュメント。