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

解决方案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将其放在<script id="__NEXT_DATA__">标签中;其他堆栈使用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、轮换住宅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为您处理渲染和轮换。