打开一个现代网页,观察其网络活动,你常常会看到你想要的数据以 JSON 的形式先行到达,早于页面将其绘制出来。爬虫本来需要费力地从 HTML 中提取的价格、列表或结果,其实就存放在页面自己获取的、结构清晰的响应中。
从该响应中采集数据,而不是从渲染后的页面采集,可以做到体积更小、更稳定,也更容易解析。但这并不像复制一个 URL 那么简单,有两点会让多数团队感到意外:一个页面加载的 JSON 中,真正是数据的部分有多么少;以及数据端点在脱离调用它的页面之后,有多么频繁地拒绝工作。本指南展示如何找到正确的端点、我们在真实页面上测得了什么,以及如何在不越界的前提下从中采集数据。
关键要点
- 页面加载的大多数 JSON 并不是数据。在某航空公司的航班搜索页面上,票价数据装在一个 44.9 KB 的响应中,约占该页面 JSON 总量的 8%,约占其 3.6 MB 总传输量的 1.2%。
- 有些页面根本没有数据 API。在某国家气象部门的天气预报页面上,所有 JSON 响应都属于 cookie 同意工具,而预报数据本身在 HTML 里。
- 通过在响应体中搜索页面上可见的某个值来找到端点,而不是靠猜测 URL。
- 页面 API 通常与页面的会话绑定。该航空公司的票价端点在页面内可以工作,但直接调用时返回 HTTP 409。
- 只使用页面为匿名访客调用的公开端点,按照普通浏览者的节奏访问,并且只要存在官方 API,就优先使用官方 API。
我们测量了什么
我们在 2026 年 9 月 30 日用真实浏览器加载了两个公开页面,并记录了每一个响应。
| 航空公司航班搜索 | 天气预报 | |
|---|---|---|
| 请求数 | 61 | 46 |
| 总传输量 | 3.62 MB | 2.57 MB |
| JSON 响应 | 15 个,534 KB | 3 个,1.04 MB |
| 承载数据的 JSON 响应 | 1 个,44.9 KB | 0 |
| 数据存放位置 | 一个票价端点 | 服务端渲染的 HTML |
在该航空公司的页面上,最大的 JSON 响应根本不是票价。一个第三方功能开关(feature-flag)服务返回了同一个 97 KB 的响应四次,一个翻译语言包又添加了 92 KB。票价数据来自该航空公司自家预订 API 的一个单一响应。
在天气页面上,全部三个 JSON 响应都来自 consent 管理工具,其中一个是 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 个请求中恰好返回了一个匹配项:预订 API 的可用性(availability)响应。请注意其中的等待策略。我们最初的版本是等待页面进入网络空闲(network-idle)状态,但它从未达到过这个状态,因为分析类的信标请求一直在触发;结果在 90 秒后超时。先等待 DOM,再等待匹配出现的方式更可靠。
你可以直接调用它吗?
通常不行。当我们直接、脱离页面地请求同一个航空公司可用性端点时,来自六个不同国家的请求都同样返回了 HTTP 409,而页面本身加载它却毫无问题。页面 API 通常依赖于页面预先设置好的一些东西:会话 cookie、嵌入在 HTML 中的令牌、请求头,或者一连串在此之前发生的调用。
这就剩下两种方式:
- 让页面自己发起调用。 在浏览器中加载页面,并在 JSON 响应到达时读取它,就像上面的脚本那样。你能获得干净的数据,而无需重建整个会话,代价是需要运行一个浏览器。
- 只回放简单的公开端点。 有些端点只需要一个 URL 即可,无需其他。同一家航空公司还发布了一个公开的票价端点,能够响应普通请求,我们在一次单独的测试中用它来验证价格是否会因出口国家而不同。在端点能够如此简单地工作的情况下,这无疑是成本最低的选择。
不应该做的是绕过会话本身:收集令牌、伪造请求头,或者回放经过身份验证的调用,以获取网站并未向匿名访客开放的数据。这就是采集不再是”观察”的分界点。
JSON 在存在时为何值得使用
当数据确实以 JSON 形式到达时,收益是实实在在的:
- 体积。 票价响应只有 44.9 KB,而完整页面是 3.6 MB。在按带宽计费的采集中,这个差距占了账单的绝大部分,正如每条干净记录的成本一文所展示的那样。
- 结构。 字段以命名和类型化的形式到达,无需编写或维护任何选择器。
- 完整性。 响应中携带的内容常常比页面展示的更多,比如标识符、全部票价等级或库存标记。
但代价同样真实存在。未文档化的端点会毫无预警地改变,而形态发生变化的响应会悄无声息地破坏你的解析器,这正是模式漂移(schema drift)监测存在的意义所在。路径中的版本号,比如该航空公司预订 API 中的 v4,只是稳定性的一个轻微信号,并不是一种承诺。
这在各种替代方案中处于什么位置
| 数据来源 | 稳定性 | 工作量 | 适用场景 |
|---|---|---|---|
| 官方、文档化的 API | 最高 | 最低 | 始终优先检查 |
| JSON-LD 或嵌入的页面状态 | 高 | 低 | 页面本身发布了它,参见停止解析 HTML |
| 页面自身的 JSON 端点 | 中 | 中 | 数据在页面加载之后才出现,且端点是公开的 |
| 渲染后的 HTML 与选择器 | 最低 | 最高 | 没有其他方式能承载数据 |
| 由模型阅读页面 | 不定 | 每个站点低,每个页面高 | 模板众多的情形,如LLM 提取与选择器对比 |
负责任地采集
页面调用的端点是网站的一部分,网站的规则依然适用。只使用页面为匿名访客调用的端点,以普通浏览者的节奏发起请求,遵守 robots.txt 和网站条款,对于登录或令牌背后的任何内容,除非获得许可,否则一律不去触碰。如果网站提供官方 API,就使用它;它更稳定,也是网站已同意支持的方式。更广泛的原则见于robots.txt、AI 退出选项与预留信号一文。
结论
现代页面背后的数据常常以 JSON 形式到达,而当它如此到达时,从那里采集比解析 HTML 更小、更干净,也更容易维护。但要找到它需要搜索,而不是猜测:在我们测量的页面中,有用的 JSON 只是众多响应中的一个,而在其中一个页面上,它根本不存在。
在响应体中搜索一个你能看到的值,检查该端点是否能单独工作,在它不能单独工作时让页面自己去调用,并始终停留在网站向任何匿名访客所提供的范围之内。
来源与参考
- 由 Shifter 于 2026 年 9 月 30 日使用上述代码加载并测量的页面。
- Playwright,网络事件文档。