bigywater.tech
爬虫工程 · 61

CDP 网络事件:不用解析页面直接拿响应体

页面里的数据多半是 JS 发请求拿来的。用浏览器抓取时,与其解析渲染后的 HTML,不如直接让 CDP 把网络响应体交出来。这篇用一个本机页面测 Network 事件的顺序、什么时候能取响应体,以及取不到的几种情况。

258780 对 16627
/api/data 经 getResponseBody 拿到的字符数(已解压) 与 loadingFinished 里记的网络字节数(encodedDataLength)
0 对 2000 字节
在 responseReceived 时取到的字节(报错) 与 在 loadingFinished 之后取到的字节
142 / 150
连续收到 150 个 1 MiB 的响应后,默认缓冲下事后还取得到的个数(设成 1 GiB 全部取得到)

一个请求的事件

实测 · 本机执行输出
  事件个数:requestWillBeSent 5、responseReceived 5、loadingFinished 5、dataReceived 6
  /          type=Document  状态 200 mimeType=text/html                Content-Length(响应头)=90 encodedDataLength=206  getResponseBody:ok 长度=90 base64Encoded=False
  /a.png     type=Image     状态 200 mimeType=image/png                Content-Length(响应头)=70 encodedDataLength=186  getResponseBody:ok 长度=96 base64Encoded=True
  /app.js    type=Script    状态 200 mimeType=application/javascript   Content-Length(响应头)=75 encodedDataLength=204  getResponseBody:ok 长度=75 base64Encoded=False
  /api/data  type=Fetch     状态 200 mimeType=application/json         Content-Length(响应头)=16477 encodedDataLength=16627  getResponseBody:ok 长度=258780 base64Encoded=False
  /favicon.ico type=Other     状态 404 mimeType=text/plain               Content-Length(响应头)=0 encodedDataLength=97  getResponseBody:ok 长度=0 base64Encoded=False

开着 Network 域,每个请求会按 requestWillBeSent、responseReceived、(若干 dataReceived)、loadingFinished 的顺序给出事件,用 requestId 关联。type 字段区分 Document、Script、Image、Fetch 等。实验里页面只有 img 和 script,浏览器还自己请求了 /favicon.ico,一共 5 个请求。getResponseBody 对文本给出文本,对图片给出 base64。

解压之后的响应体

实测 · 本机执行输出
  /api/data 网络上传输的字节数(loadingFinished 的 encodedDataLength)是 16627,getResponseBody 给出的是解压之后的文本 258780 字符、解析成 3000 个对象;服务器实际发送的 gzip 字节数 16477;页面 JS 里 window.n = 3000

响应带了 Content-Encoding: gzip 时,getResponseBody 给出的是浏览器已经解压的内容,不用自己解压。loadingFinished 里的 encodedDataLength 是网络上传输的字节数。实验里 gzip 正文 16477 字节,encodedDataLength 是 16627,解压后的文本 258780 字符(多出的 150 字节我推测是响应头,没有逐字节核对)。

什么时候能取

实测 · 本机执行输出
  responseReceived 到达时(正文 2000 字节还没发完)取:('err', "Network.getResponseBody: {'code': -32000, 'message': 'No data found for resource with give", None)
  loadingFinished 到达后取:('ok', 2000, False)(距 responseReceived 1.26 秒;页面 JS 里收到的正文长度 2000)

响应头到了(responseReceived)不等于正文到了。正文还在传输时调 getResponseBody 会报错 “No data found for resource with give…”(输出被截断,后面是 identifier);等 loadingFinished 之后才取得到。实验里 /slow 的正文分两块,中间停 0.6 秒。

重定向与页面跳走

实测 · 本机执行输出
  访问 /redir:requestWillBeSent 里带 redirectResponse 的有 1 个,状态 302,Location=/api/data
  跳转之后:此前 /api/data 那个 requestId 再取 getResponseBody:('err', "Network.getResponseBody: {'code': -32000, 'message': 'No resource with given identifier fo", None)

重定向的那一跳在下一个 requestWillBeSent 的 redirectResponse 字段里(状态、Location)。页面跳走之后,旧页面的 requestId 取 getResponseBody 会报 “No resource with given identifier fo…”(输出被截断)。所以要在页面还在的时候取走需要的响应体。

少下载

实测 · 本机执行输出
  不阻止:服务器收到 /a.png 1 次,其它请求 3 次;浏览器侧 loadingFailed 0 个([]);页面 JS 里 window.n = 3000
  阻止 *.png:服务器收到 /a.png 0 次,其它请求 3 次;浏览器侧 loadingFailed 1 个([('', 'inspector')]);页面 JS 里 window.n = 3000

Network.setBlockedURLs 能让浏览器不发出匹配的请求(实验里图片请求没有到达服务器,页面其余部分照常工作),对不需要图片、样式的抓取能省流量。

别被挤掉

实测 · 本机执行输出
  Network.enable 默认:页面收完 150 个,事后取得到响应体的有 142 个,取得到的是第 8–149 个(从 0 开始)
  maxTotalBufferSize=10 MiB:页面收完 150 个,事后取得到响应体的有 7 个,取得到的是第 143–149 个(从 0 开始)
  maxTotalBufferSize=1 GiB:页面收完 150 个,事后取得到响应体的有 150 个,取得到的是第 0–149 个(从 0 开始)

DevTools 会缓存响应体供事后查看,缓冲有上限:实验里默认设置下连续 150 个 1 MiB 的响应,事后只取得到 142 个,最早的 8 个被挤掉;把 maxTotalBufferSize 设成 10 MiB 就只剩最后 7 个。大量或大的响应要在 loadingFinished 时就取走,不要攒到最后。

分步演示

CDP 网络事件
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。页面和接口都在本机服务器上(img、script、一个 258780 字节压缩成 16477 字节的 JSON、一个分块慢响应 /slow、一个 302、可指定大小的不可压缩响应 /blob);Chrome 154 通过 pcdp.py 控制,用 Network.enable 读事件(要持续读 websocket 才收得到事件),各情形跑一次
    • 没有验证:默认缓冲上限的具体数值(只观察到 150 个 1 MiB 的响应里最早 8 个被挤掉,没有去确定它是按字节还是按个数、阈值是多少);Fetch 域(拦截并修改请求/响应);跨域 iframe、Service Worker、Web Worker 里的请求(需要 Target.setAutoAttach 才看得到,没有试);HTTP/2 和 HTTPS 下的差别;encodedDataLength 比 gzip 正文多出的 150 字节到底是什么
    京ICP备2026067502号-1京公网安备11010502063487号