有些页面的数据不是 HTTP 响应,而是 WebSocket 推过来的。爬虫该怎么看这些帧、怎么直接连、断线后怎么不丢数据、为什么会被服务器断开?这篇用一个本机的 WebSocket 服务器逐个试。
事件种类与个数:{'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 一帧;序号是否连续:TrueCDP 开着 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 还是应用层的消息、间隔多久)要看它自己的实现,这里的规则是测试服务器定的。
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 校验只能防别的网页跨站连接,不是认证。