# 爬虫返回空白页面？是JavaScript的问题。以下是解决方法

在爬虫中，\<body>标签是空的，但在开发者工具中却是满的。这不是一个bug——这是客户端渲染。以下是如何识别、获取数据的三种方法，以及如何避免启动不必要的浏览器。

您获取页面，打印HTML，您想要的部分却缺失了——一个空的`<body>`，一个空的节点列表，或者一个“需要JavaScript”的提示。同时，浏览器的检查器显示数据就在那。这不是爬虫的bug。这是一个用JavaScript在客户端渲染内容的页面，而您的HTTP请求只获取到JavaScript运行前的空壳。以下是如何确认这一点，三种获取数据的方法，以及如何避免启动不必要的无头浏览器。

## 为什么body是空的

一个原始HTTP客户端——Python `requests`、Node `fetch`、cURL——下载服务器发送的HTML并停止。它不执行JavaScript。基于React、Vue、Angular或Next.js构建的网站发送一个几乎空的HTML骨架加上一个在浏览器中获取数据并构建DOM的包。像BeautifulSoup或Cheerio这样的解析器看到的是骨架，因此在开发者工具中完美工作的选择器在代码中返回空数组。Cheerio在React页面上返回`[]`是典型症状；它不运行JavaScript，也永远不会。

```js
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
```

## 十秒内确认

在您使用浏览器之前，证明诊断。两个快速检查告诉您确切的情况：比较原始响应与浏览器渲染的内容，并在控制台中计数元素。如果控制台找到原始HTML中没有的节点，内容是客户端的。如果原始HTML已经有了，问题在于您的选择器或您的头信息，而不是渲染。

```bash
# 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。有时服务器会在您发送正确的头信息或cookie之前不提供内容——一个会话cookie，一个referer，一个`Accept-Language`，或者一个不会显得“脚本”的User-Agent。如果cURL返回一个简短的页面，但您的浏览器（发送完整的头信息集）返回一个完整的页面，尝试重放浏览器的请求头信息，然后再假设是渲染问题。这是一个五分钟的测试，可以让您避免启动不必要的浏览器。

![一个客户端渲染页面的原始 HTML，标出了三个特征：空的 body 或“JavaScript is required”占位内容、__NEXT_DATA__ 等框架状态数据块，以及在加载后通过 XHR 获取数据的脚本包；卡片说明了每个特征对抓取器意味着什么。](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335154/blog/generated/qp-b9-016-diag1.png)
*空body、原始HTML中的框架状态blob或网络选项卡中的XHR调用：三个可靠的标志表明页面在客户端渲染。*

## 解决方案1：访问隐藏的JSON API（最快）

大多数人忽略的一点是：如果页面通过JavaScript自我绘制，数据来自某个地方——通常是前端在加载后调用的内部JSON端点。打开网络选项卡，过滤到Fetch/XHR，重新加载，并寻找返回数据的请求。直接调用该端点比解析HTML更快、更轻且更不易碎，因为您跳过了整个浏览器，直接获取结构化JSON。有时端点需要初始HTML嵌入的令牌——从页面中获取它，然后调用API。

```js
// 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：挖掘hydration JSON

即使没有单独的API调用，框架也会将初始状态直接嵌入HTML中，以便客户端可以在不进行往返的情况下“hydrate”。Next.js将其放在`<script id="__NEXT_DATA__">`标签中；其他堆栈使用`window.__INITIAL_STATE__`或类似的blob。该JSON在您已经下载的原始响应中——不需要渲染。解析脚本标签，您通常会得到页面将显示的确切结构化数据，比抓取渲染的DOM更干净。

```js
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和hydration挖掘之间，大多数“空页面”问题在不启动浏览器的情况下就解决了。我们的[JavaScript密集型网站抓取指南](https://quantumproxies.io/blog/scraping-javascript-heavy-websites-with-proxies)深入探讨了如何可靠地找到这些端点。

对两者的一个警告：隐藏端点和状态blob是非官方的接口，因此它们会在没有警告的情况下更改。将解析包装在一个检查中，当形状变化时会发出响亮的失败——缺少的键或零长度数组应该引发，而不是默默地返回空。这样，网站重构其API时会在您的日志中显示为明显的错误，而不是您一周都没有注意到的空数据集。也固定您使用的User-Agent和头信息；其中一些端点悄悄地依赖于它们。

## 解决方案3：渲染——但将其视为最后的手段

当数据确实在DOM中构建且没有公开的JSON时，您必须运行JavaScript。Playwright或Puppeteer驱动真实浏览器，等待内容，然后将完成的HTML交给您。它在任何地方都有效，但这是昂贵的选择：渲染的带宽和时间成本大约是JSON调用的10-50倍，而数据中心IP上的无头浏览器很快就会被封锁。通过住宅代理路由，并等待正确的信号，而不是固定的休眠。

```js
import { chromium } from "playwright";

const browser = await chromium.launch();
const context = await browser.newContext({
  proxy: { server: "http://residentialpremium.quantumproxies.io:8080", 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在一次调用中渲染和旋转](https://quantumproxies.io/scraper-api)

![JavaScript 渲染页面的修复阶梯：先调用在 Network 面板中找到的隐藏 JSON API，然后挖掘原始 HTML 中的 hydration JSON，再通过住宅代理用 Playwright 渲染，最后使用 Scraper API；卡片指出，渲染消耗的带宽和时间大约是一次 JSON 调用的 10-50 倍。](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335154/blog/generated/qp-b9-016-diag2.png)
*沿着阶梯往下走：首先尝试隐藏的API和hydration JSON，只有在两者都失败时才渲染。*

## 当自己渲染不再值得时

运行自己的浏览器集群意味着管理无头检测、代理轮换、内存泄漏和每个目标的等待逻辑。一旦您在维护这些，一个[Scraper API](https://quantumproxies.io/scraper-api)可以渲染JavaScript、轮换住宅IP并在单个请求中返回干净的HTML、JSON或markdown，通常更少的代码且成功率更高——它还可以在渲染的页面上运行AI提取，因此您可以完全跳过选择器。我们关于[渲染成本与普通请求](https://quantumproxies.io/blog/headless-browser-vs-http-requests-cost)的分析显示了何时进行这种切换。

## 常见问题解答

### 为什么我的网页爬虫返回空body？

因为页面在HTML加载后用JavaScript渲染其内容，而您的HTTP客户端不执行JavaScript。您收到的是服务器发送的空壳。通过比较原始响应（通过cURL）与开发者工具显示的内容来确认——如果浏览器有原始HTML缺乏的数据，那就是客户端渲染。

### 如何在不使用浏览器的情况下抓取JavaScript渲染的页面？

有两种方法完全避免使用浏览器。在网络选项卡中找到页面调用的内部JSON端点并直接请求它，或者提取嵌入在HTML中的hydration状态（例如`__NEXT_DATA__`脚本标签）。两者都比渲染更快、更可靠地提供结构化数据。

### BeautifulSoup或Cheerio会运行JavaScript吗？

不会。两者都解析您提供的HTML字符串，且都不执行JavaScript。在React、Vue或Next.js页面上，它们只看到预渲染的骨架，这就是为什么您的选择器返回空数组。您需要渲染的DOM（Playwright/Puppeteer）或底层JSON来获取数据。

### 为什么在抓取时会收到“JavaScript已禁用”的消息？

该网站为不运行JavaScript的客户端提供了一个回退，而您的爬虫看起来像其中之一。这通常伴随着机器人检测。在干净的住宅IP上用无头浏览器执行页面可以清除它；如果它再次出现，目标是在指纹识别请求，而不仅仅是检查JavaScript。

空页面是一种诊断，而不是死胡同。确认是客户端渲染，首先寻找隐藏的API或hydration JSON，只有在数据无处可存时才渲染。您将编写更少的代码，使用更少的带宽，并且更少被封锁。如果您不想维护这些，[Scraper API](https://quantumproxies.io/scraper-api)为您处理渲染和轮换。

[试试QuantumProxies Scraper API](https://quantumproxies.io/scraper-api)
