bigywater.tech
爬虫工程 · 54

找到真实数据接口:SPA 页面背后的请求

很多网页用 requests 拿到的 HTML 里没有数据:数据是页面加载后由 JavaScript 请求接口取来的。找到那个接口,直接请求它,往往比启动浏览器渲染便宜得多。这篇用 Chrome 的 DevTools 协议记录一个本机单页应用的请求,再逐步弄清直接重放需要什么。

136 字节
用 requests 取单页应用首页拿到的 HTML 大小——没有任何商品
3 / 7
页面加载时发出的 7 个请求里,真正含商品数据的只有 3 个
2 对 63 ms
直接调接口翻页取 25 条 与 浏览器渲染完成 的耗时(本机回环)

HTML 里没有数据

实测 · 本机执行输出
  状态 200,HTML 大小 136 字节;<li> 个数 0;<script src> = ['/app.js'];ul#list 里的内容 [] ——数据不在 HTML 里,要等 JS 去取

首页只有骨架和一个 <script src>。数据由 /app.js 里的 fetch 请求接口得到,再用 JS 写进 DOM。

用 CDP 记录请求

实测 · 本机执行输出
  从打开页面到 JS 渲染完成:63 ms;DOM 里渲染出 25 条,第一条 'Item 1 - 10.37'
  方法    路径                                   状态        类型       发起者  请求头里值得注意的
  GET   /                                   200  Document     other  
  GET   /app.js                             200    Script    parser  
  GET   /api/config                         200     Fetch    script  {}
  GET   /api/products?page=1&limit=10       200     Fetch    script  {'X-Requested-With': 'XMLHttpRequest'}
  GET   /api/products?page=2&limit=10       200     Fetch    script  {'X-Requested-With': 'XMLHttpRequest'}
  GET   /api/products?page=3&limit=10       200     Fetch    script  {'X-Requested-With': 'XMLHttpRequest'}
  POST  /api/track                            ?      Ping    script  {'Content-Type': 'text/plain;charset=UTF-8'}
  GET   /favicon.ico                          ?     Other     other  

通过 Chrome DevTools 协议的 Network 事件,可以看到页面发的每个请求:方法、URL、资源类型(Document、Script、Fetch、Ping)、发起者、响应状态。本例 7 个请求里 3 个 /api/products 才是数据;/api/config 是配置,/api/track 是统计。再用 Network.getResponseBody 可以读到响应内容。

重放需要什么

实测 · 本机执行输出
  什么都不带:401;只带 Cookie:403;只带 X-Requested-With:401;两者都带:200
  → 缺 Cookie 返回 401,缺 X-Requested-With 返回 403;这两个都是浏览器自动带上的,直接请求时要自己补(Cookie 的值来自第一次访问 / 时服务器设置的 Set-Cookie)

直接请求接口会遇到 401(没有会话 Cookie)和 403(没有 X-Requested-With 头)。Cookie 来自首页响应的 Set-Cookie,用 requests.Session 先访问首页即可;必需的头可以从 CDP 记录的请求头里看出来。这个服务器是我写的,真实站点的校验可能更多。

直接翻页

实测 · 本机执行输出
  3 次请求取到 25 条,2 ms(浏览器从打开页面到渲染完成要 63 ms,这里 Chrome 已经启动好,时间包括加载页面、运行 JS 和同样的 3 次接口请求;两者都在本机回环上,绝对值只供比较)
  条数一致:True;名称一致:True;API 返回的字段:['id', 'name', 'price_cents', 'internal_sku']
  DOM 里只显示了 名称 和 价格(元);API 里还有页面没显示的字段:internal_sku='SKU-00001',价格是分 price_cents=1037

三次请求取到 25 条,和 DOM 里渲染出的一致;而且接口返回的字段比页面显示的多(内部编号、以分为单位的价格)。是否应该使用这些额外字段,取决于你的数据用途和站点的使用条款——本文没有涉及。

分步演示

找到数据接口
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。单页应用是本机服务器上的一个小页面:/ 返回骨架并设置 Cookie,/app.js 里用 fetch 先取 /api/config,再翻页取 /api/products(需要 Cookie 与 X-Requested-With 头),最后用 sendBeacon 发 /api/track;Chrome 154 通过 pcdp.py(websocket-client)的 CDP 控制,开启 Network 域记录事件,独立 user-data-dir,实验后结束该进程;requests 重放时先访问首页拿 Cookie
    • 没有验证:需要签名、时间戳、动态 token 的接口;WebSocket 或 GraphQL 接口(WebSocket 在后面单独讲);接口有速率限制或反爬时的行为;HAR 导出;CDP 里 Cookie 的 ExtraInfo 事件(这里请求头里没显示 Cookie,是 CDP 把它放在另外的事件中);请求体很大的 POST 接口;站点使用条款对直接调用接口的限制
    京ICP备2026067502号-1京公网安备11010502063487号