bigywater.tech
爬虫工程 · 60

WebSocket 数据源:怎么抓取推送的数据

有些页面的数据不是 HTTP 响应,而是 WebSocket 推过来的。爬虫该怎么看这些帧、怎么直接连、断线后怎么不丢数据、为什么会被服务器断开?这篇用一个本机的 WebSocket 服务器逐个试。

19 帧 / 秒
Chrome 打开页面后 1 秒内,CDP 的 Network.webSocketFrameReceived 事件收到的帧数(序号连续)
86 对 98 条
服务器每 1.5 秒断开一次、客户端跑 5 秒:重连不带 since 只收到 86 条(丢 12 条),重连带 since 收到 98 条(缺口 0)
1.0 对 3.0 秒
服务器 1.0 秒收不到客户端消息就关闭:不发心跳连接只维持 1.0 秒,每 0.4 秒发一个 ping 则维持到 3.0 秒

用浏览器看帧

实测 · 本机执行输出
  事件种类与个数:{'Created': 1, 'WillSendHandshakeRequest': 1, 'HandshakeResponseReceived': 1, 'FrameSent': 1, 'FrameReceived': 19}
  WebSocket 地址:ws://127.0.0.1:9970/ws;握手响应状态 101;页面发出的帧:['{"op":"subscribe"}'];收到的帧共 19 个,前 3 个 [{'seq': 12, 'price': 101.2}, {'seq': 13, 'price': 101.3}, {'seq': 14, 'price': 101.4}];页面 JS 里数到 19 条
  浏览器的握手头(服务器记录):Origin=http://127.0.0.1:9960;User-Agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KH';Sec-WebSocket-Extensions=permessage-deflate; client_max_window_bits
  1 秒内收到 19 帧,约每 53 ms 一帧;序号是否连续:True

CDP 开着 Network 时,页面里的 WebSocket 会产生 webSocketCreated、webSocketWillSendHandshakeRequest、webSocketHandshakeResponseReceived、webSocketFrameSent、webSocketFrameReceived 等事件,帧的内容在事件的 response.payloadData 里。不用解析页面就能拿到推送的数据;实验里 1 秒收到 19 帧,序号连续,和页面 JS 自己数到的一致。注意 CDP 的事件要持续读取,不读就收不到(我第一次写的脚本就是睡了 1 秒没读,只看到了 Created 一个事件)。

直接连

实测 · 本机执行输出
  服务器记录的握手:Origin=http://127.0.0.1:9970,User-Agent='',Sec-WebSocket-Extensions=None;2 秒内收到 40 条(20 条/秒),序号 31…70,连续:True

知道地址和订阅消息后可以不开浏览器,直接用客户端库连。浏览器的握手带 Origin(页面自己的地址)、长 User-Agent,并请求 permessage-deflate;websocket-client 默认的握手 Origin 是库自动补成服务器自己的地址、User-Agent 为空、不请求压缩。如果服务器对这些有要求,握手阶段就会有差别(见第五节)。

断线与重连

实测 · 本机执行输出
  断了就不再连:收到 30 条,连接 1 次,序号缺口(丢失的条数)0,覆盖的序号范围 30
  断了等 0.3 秒重连并重新订阅(不带 since):收到 86 条,连接 3 次,序号缺口(丢失的条数)12,覆盖的序号范围 98
  重连并带上 since=最后收到的序号:收到 98 条,连接 3 次,序号缺口(丢失的条数)0,覆盖的序号范围 98

服务器重启或断开是常态。不重连就只拿到断开前的数据;重连后重新订阅,断开期间产生的数据会丢(实验里 5 秒丢了 12 条);带上最后收到的序号让服务器回放则没有缺口——前提是服务器支持。每条消息有序号时,客户端才能检查缺口;没有序号的数据流就没法知道丢了多少。

心跳

实测 · 本机执行输出
  不发心跳:连接维持了 1.0 秒,收到 19 帧;客户端:收到关闭帧,关闭码 4000;服务器记录:由服务器关闭,码 4000
  每 0.4 秒发一个应用层 ping:连接维持了 3.0 秒,收到 66 帧;客户端:3 秒内未被关闭;服务器记录:由客户端关闭,码 1000

很多服务器会关闭长时间没收到消息的连接。只收不发的客户端对服务器来说是安静的:实验里服务器 1.0 秒收不到任何消息就关闭,不发心跳的客户端连接只维持了 1.0 秒;每 0.4 秒发一个应用层 ping 就一直保持。真实站点的心跳规则(协议层的 ping/pong 还是应用层的消息、间隔多久)要看它自己的实现,这里的规则是测试服务器定的。

握手里的 Origin

实测 · 本机执行输出
  websocket-client 默认(库自动补上 Origin: http://127.0.0.1:9970,即服务器自己的地址):WebSocketBadStatusException: Handshake status 403 Forbidden -+-+- {'con;服务器看到的 Origin=['http://127.0.0.1:9970']
  websocket-client suppress_origin=True,真的不带 Origin 头:WebSocketBadStatusException: Handshake status 403 Forbidden -+-+- {'con;服务器看到的 Origin=[None]
  websocket-client 带错误的 Origin:WebSocketBadStatusException: Handshake status 403 Forbidden -+-+- {'con;服务器看到的 Origin=['http://evil.test']
  websocket-client 带正确的 Origin:握手成功;服务器看到的 Origin=['http://shop.test']
  浏览器页面(Origin http://127.0.0.1:9960)+ 服务器只允许页面自己的 Origin:页面 JS 里收到 16 条;Network 事件 {'Created': 1, 'WillSendHandshakeRequest': 1, 'HandshakeResponseReceived': 1, 'FrameSent': 1, 'FrameReceived': 16}
  浏览器页面(Origin http://127.0.0.1:9960)+ 服务器只允许 http://shop.test:页面 JS 里收到 0 条;Network 事件 {'Created': 1, 'WillSendHandshakeRequest': 1, 'FrameError': 1, 'Closed': 1}

服务器可以在握手时检查 Origin。实验里服务器只允许一个 Origin 时,库默认补的 Origin、不带 Origin、错误的 Origin 都是 403,带对的才成功;浏览器页面的 Origin 不匹配时,页面收不到数据,CDP 里只看到 FrameError 和 Closed。因为非浏览器客户端可以随便填 Origin(这是从“带对的就成功”推出来的,没有拿别的服务器验证),Origin 校验只能防别的网页跨站连接,不是认证。

分步演示

WebSocket 数据源
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。服务器是 aiohttp 写的本机 WebSocket 服务(每 50 ms 产生一条带递增序号的消息,订阅后推送,保留最近 2000 条用于回放;断开、心跳和 Origin 规则都是我在服务器上设定的);客户端是 websocket-client 1.9.0 和 Chrome 154(pcdp.py + CDP Network 事件);各情形跑一次,没有重复统计
    • 没有验证:真实站点的 WebSocket(协议各不相同:二进制帧、压缩、自定义心跳、签名的订阅消息);协议层 ping/pong(这里只测了应用层 ping);permessage-deflate 压缩帧的解码(浏览器协商了但库没协商,没有比较流量);wss://(TLS)下的行为;大量连接并发;断线重连的退避策略(第 30 篇讲的退避没有在这里测)
    京ICP备2026067502号-1京公网安备11010502063487号