スクレイパーが空のページを返す?それはJavaScriptです。修正方法はこちら
スクレイパーの<body>タグが空なのに、DevToolsではいっぱいです。それはバグではなく、クライアントサイドレンダリングです。判別方法、データ取得の3つの方法、そして不要なブラウザ起動を避ける方法を紹介します。
ページを取得し、HTMLを印刷すると、欲しかった部分が欠けています — 空の<body>、空のノードリスト、または「JavaScriptが必要です」と書かれたスタブです。一方、ブラウザのインスペクタはデータをその場で表示します。これはスクレイパーのバグではありません。これはJavaScriptでクライアントサイドでコンテンツをレンダリングするページであり、HTTPリクエストはJavaScriptが実行される前の空のシェルしか取得しません。これを確認する方法、データを取得する3つの方法、そして実際には必要のないヘッドレスブラウザを起動しない方法を紹介します。
なぜbodyが空なのか
生のHTTPクライアント — Python requests、Node fetch、cURL — はサーバーが送信するHTMLをダウンロードし、そこで止まります。JavaScriptは実行しません。React、Vue、Angular、Next.jsで構築されたサイトは、ほぼ空のHTMLスケルトンと、データを取得してブラウザでDOMを構築するバンドルを提供します。BeautifulSoupやCheerioのようなパーサーはスケルトンを見ているので、DevToolsで完璧に動作するセレクタがコード内で空の配列を返します。ReactページでCheerioが[]を返すのは典型的な症状です。JavaScriptを実行せず、今後も実行しません。
import * as cheerio from "cheerio";
const html = await (await fetch("https://shop.example.com/products")).text();
const $ = cheerio.load(html);
console.log($("div.product").length);
// 0 -> the products are drawn by JavaScript, not in the HTML
10秒で確認
ブラウザに手を伸ばす前に、診断を証明しましょう。2つの迅速なチェックで、何を扱っているか正確にわかります: 生のレスポンスとブラウザがレンダリングするものを比較し、コンソールで要素を数えます。コンソールが生のHTMLに含まれていないノードを見つけた場合、コンテンツはクライアントサイドです。生のHTMLにすでにある場合、問題はセレクタやヘッダーであり、レンダリングではありません。
# does the raw response actually contain the data?
curl -s https://shop.example.com/products | grep -c 'class="product"'
# 0 -> not in the HTML | 24 -> it IS there, fix your selector
# then, in the browser DevTools console on the same page:
# document.querySelectorAll('div.product').length -> 24
もう一つの可能性を排除しましょう: 空のbodyが常にJavaScriptとは限りません。時にはサーバーが正しいヘッダーやクッキーを送信するまでコンテンツを保留することがあります — セッションクッキー、リファラー、Accept-Language、または「スクリプト」と叫ばないUser-Agentです。cURLが短いページを返すが、ブラウザ(完全なヘッダーセットを送信する)が完全なページを返す場合、レンダリングを仮定する前にブラウザのリクエストヘッダーを再現してみてください。これは5分のテストで、不要なブラウザ起動を避けることができます。

修正1: 隠れたJSON APIを叩く(最速)
多くの人が見逃しているのはこれです: ページがJavaScriptで自らを描画する場合、データはどこかから到着します — 通常はロード後にフロントエンドが呼び出す内部JSONエンドポイントです。ネットワークタブを開き、Fetch/XHRでフィルタリングし、リロードしてデータを返すリクエストを探します。そのエンドポイントを直接呼び出す方が、HTMLを解析するよりも速く、軽く、はるかに脆弱性が少ないです。ブラウザ全体をスキップして、構造化されたJSONをすぐに取得できます。時にはエンドポイントが初期HTMLに埋め込まれたトークンを必要とすることがあります — ページから取得して、APIを呼び出します。
// 1) the SPA loads data from its own endpoint after paint
const res = await fetch("https://shop.example.com/api/catalog?page=1", {
headers: { accept: "application/json", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" },
});
const { products } = await res.json();
console.log(products.length); // real data, no browser
// 2) if it needs a token, the initial HTML usually carries it
const page = await (await fetch("https://shop.example.com/")).text();
const token = page.match(/"s_token":\s*"([^"]+)"/)?.[1];
修正2: ハイドレーションJSONを探す
別のAPI呼び出しがない場合でも、フレームワークは初期状態をHTMLに直接埋め込んで、クライアントが往復せずに「ハイドレート」できるようにします。Next.jsはそれを<script id="__NEXT_DATA__">タグに入れます。他のスタックはwindow.__INITIAL_STATE__や類似のブロブを使用します。そのJSONはすでにダウンロードした生のレスポンスに含まれています — レンダリングは不要です。スクリプトタグを解析すると、ページが表示するはずだった正確な構造化データを得ることができ、レンダリングされたDOMをスクレイピングするよりもクリーンです。
import * as cheerio from "cheerio";
const html = await (await fetch(url)).text();
const $ = cheerio.load(html);
const blob = $("#__NEXT_DATA__").html(); // Next.js embeds page state here
if (blob) {
const data = JSON.parse(blob);
const products = data.props.pageProps.products;
console.log(products.length); // structured, from static HTML
}
隠れたAPIとハイドレーションマイニングの間で、ほとんどの「空のページ」問題はブラウザを起動せずに解決されます。JavaScriptを多用するサイトのスクレイピングガイドでは、これらのエンドポイントを確実に見つける方法をさらに詳しく説明しています。
両方に関する注意点: 隠れたエンドポイントと状態ブロブは非公式のサーフェスであり、予告なしに変更されることがあります。解析を、形状が変わったときに大きなエラーを発生させるチェックでラップしてください — 欠落したキーやゼロ長の配列は、何も返さないのではなく、エラーを発生させるべきです。APIが再構築されると、ログに明確なエラーとして表示され、1週間気づかない空のデータセットにはなりません。また、動作したときに使用したUser-Agentとヘッダーを固定してください; これらのエンドポイントの一部は静かにそれに依存しています。
修正3: レンダリング — しかし最後の手段と考える
データがDOM内で本当に構築され、公開されたJSONがない場合、JavaScriptを実行する必要があります。PlaywrightやPuppeteerは実際のブラウザを駆動し、コンテンツを待ち、完成したHTMLを渡します。どこでも動作しますが、これは高価なオプションです: レンダリングはJSON呼び出しの約10〜50倍の帯域幅と時間を要し、データセンターIP上のヘッドレスブラウザはすぐにブロックされます。住宅プロキシを通してルートし、固定スリープではなく正しいシグナルを待ちます。
import { chromium } from "playwright";
const browser = await chromium.launch();
const context = await browser.newContext({
proxy: { server: "http://gate.quantumproxies.io:8000", username: "USER", password: "PASS" },
});
const page = await context.newPage();
await page.goto(url, { waitUntil: "networkidle", timeout: 30000 });
await page.waitForSelector("div.product"); // wait for data, not a sleep
const html = await page.content();
await browser.close();
Scraper APIでレンダリングと回転を1回の呼び出しで

自分でレンダリングする価値がなくなるとき
自分のブラウザフリートを運用することは、ヘッドレス検出、プロキシ回転、メモリリーク、ターゲットごとの待機ロジックを管理することを意味します。それを維持するようになったら、JavaScriptをレンダリングし、住宅IPを回転させ、クリーンなHTML、JSON、またはmarkdownを単一のリクエストで返すScraper APIは、通常、より少ないコードで高い成功率を提供します — そして、レンダリングされたページでAI抽出を実行できるので、セレクタを完全にスキップできます。レンダリングコストとプレーンリクエストの比較では、切り替えるタイミングを示しています。
よくある質問
なぜ私のウェブスクレイパーは空のbodyを返すのですか?
ページがHTMLのロード後にJavaScriptでコンテンツをレンダリングするため、HTTPクライアントはJavaScriptを実行しません。サーバーが送信する空のスケルトンを受け取ります。生のレスポンス(cURL経由)とDevToolsが表示するものを比較して確認してください — ブラウザに生のHTMLにないデータがある場合、それはクライアントサイドレンダリングです。
ブラウザなしでJavaScriptでレンダリングされたページをスクレイピングするにはどうすればよいですか?
ブラウザを完全に避ける2つの方法があります。ネットワークタブでページが呼び出す内部JSONエンドポイントを見つけて直接リクエストするか、HTMLに埋め込まれたハイドレーション状態を抽出します(例えば__NEXT_DATA__スクリプトタグ)。どちらもレンダリングよりも速く、信頼性の高い構造化データを提供します。
BeautifulSoupやCheerioはJavaScriptを実行しますか?
いいえ。どちらも与えられたHTML文字列を解析し、JavaScriptを実行しません。React、Vue、Next.jsページでは、プリレンダスケルトンしか見えないため、セレクタが空の配列を返します。データを取得するには、レンダリングされたDOM(Playwright/Puppeteer)または基礎となるJSONが必要です。
スクレイピング時に「JavaScriptが無効です」というメッセージが表示されるのはなぜですか?
サイトはJavaScriptを実行しないクライアント用のフォールバックを提供し、スクレイパーがそのように見えるからです。これはしばしばボット検出と組み合わされます。クリーンな住宅IP上のヘッドレスブラウザでページを実行するとクリアされます; 再発する場合、ターゲットはJavaScriptのチェックだけでなく、リクエストのフィンガープリンティングを行っています。
空のページは診断であり、行き止まりではありません。それがクライアントサイドであることを確認し、まず隠れたAPIまたはハイドレーションJSONを探し、データが他に住む場所がない場合にのみレンダリングします。コードを書く量が減り、帯域幅の使用が減り、ブロックされる頻度も減ります。すべてを維持したくない場合、Scraper APIがレンダリングと回転を処理します。