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

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

您获取页面,打印HTML,您想要的部分却缺失了——一个空的&lt;body&gt;,一个空的节点列表,或者一个“需要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,也永远不会。

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已经有了,问题在于您的选择器或您的头信息,而不是渲染。

# 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返回一个简短的页面,但您的浏览器(发送完整的头信息集)返回一个完整的页面,尝试重放浏览器的请求头信息,然后再假设是渲染问题。这是一个五分钟的测试,可以让您避免启动不必要的浏览器。

检查表图表通过空body标签、嵌入状态JSON和XHR调用等标志区分静态HTML页面和JavaScript渲染页面
空body、原始HTML中的框架状态blob或网络选项卡中的XHR调用:三个可靠的标志表明页面在客户端渲染。

解决方案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:挖掘hydration JSON

即使没有单独的API调用,框架也会将初始状态直接嵌入HTML中,以便客户端可以在不进行往返的情况下“hydrate”。Next.js将其放在&lt;script id="__NEXT_DATA__"&gt;标签中;其他堆栈使用window.__INITIAL_STATE__或类似的blob。该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和hydration挖掘之间,大多数“空页面”问题在不启动浏览器的情况下就解决了。我们的JavaScript密集型网站抓取指南深入探讨了如何可靠地找到这些端点。

对两者的一个警告:隐藏端点和状态blob是非官方的接口,因此它们会在没有警告的情况下更改。将解析包装在一个检查中,当形状变化时会发出响亮的失败——缺少的键或零长度数组应该引发,而不是默默地返回空。这样,网站重构其API时会在您的日志中显示为明显的错误,而不是您一周都没有注意到的空数据集。也固定您使用的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在一次调用中渲染和旋转

JavaScript渲染页面修复阶梯的流程图:隐藏JSON API、hydration挖掘、使用代理渲染,然后是Scraper API
沿着阶梯往下走:首先尝试隐藏的API和hydration JSON,只有在两者都失败时才渲染。

当自己渲染不再值得时

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

常见问题解答

为什么我的网页爬虫返回空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为您处理渲染和轮换。

试试QuantumProxies Scraper API