news 2026/10/15 11:56:59

WebSocket实时数据抓取:mitmproxy拦截与帧解析实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WebSocket实时数据抓取:mitmproxy拦截与帧解析实战

1. 为什么我会盯上 WebSocket 这条"非主流"路径

做爬虫这行时间长了,你会发现一个规律:越是看起来"正规"的接口,反爬做得越狠;反而是一些被前端工程师随手写出来的实时通信通道,防护薄得像纸。我最早接触 WebSocket 抓取,是因为一个做数据聚合的朋友找我帮忙——他需要实时监控某类公开数据看板上的数值变化,传统 HTTP 轮询方案要么被限流,要么拿到的数据延迟太高,折腾了两周没什么进展。

后来我让他打开浏览器开发者工具,切到 WS 那一栏,结果发现页面所有实时数值都是通过一条 WebSocket 长连接推过来的,格式是干净的 JSON,字段命名规规矩矩,连加密都没有。那一刻我就意识到,很多项目的实时数据其实走的是 WebSocket,而大部分爬虫教程还在教你怎么逆向sign参数、怎么破解滑块,完全没往这个方向看。

这篇内容就是把我这几年在 WebSocket 抓取上踩过的坑、总结的方法完整梳理一遍。核心思路是:用 mitmproxy 作为中间人,拦截并记录 WebSocket 的双向帧,再从中提取结构化数据。它适合有一定 Python 基础、做过常规 HTTP 爬虫、但被实时数据接口卡住的开发者。读完你能掌握一套可复现的流程:从环境搭建、证书配置、帧解析,到数据落库和断线重连,每一步我都会说清楚"为什么这么做"。

需要先说明一点:这套方法只适用于你自己有权访问的公开数据接口,或者你已获得明确授权的系统。任何绕过访问控制、侵犯他人数据权益的行为都不在讨论范围内,这是底线。

2. WebSocket 抓取和普通 HTTP 抓取的本质差异

2.1 一次握手,长期对话

普通 HTTP 爬虫的逻辑是"请求-响应-断开",你发一个 GET,服务器回一个 200,连接就结束了。每次请求都是独立的,你可以在任意时刻重放、修改参数、加代理。WebSocket 完全不是这个模型——它先通过一次 HTTP 升级握手(Upgrade: websocket),握手成功后这条 TCP 连接就"变性"了,之后双方可以随时互发数据帧,没有请求响应的严格配对关系。

这个差异直接决定了抓取策略的不同。HTTP 抓取你可以"事后补抓",WebSocket 不行,数据帧是推送过来的,你错过了就是错过了。所以 WebSocket 抓取的核心不是"构造请求",而是"持续监听 + 及时落盘"。

2.2 帧结构决定了你该怎么解析

WebSocket 的数据以"帧"为单位传输,一个逻辑消息可能被拆成多个帧(分片),也可能一个帧就是一个完整消息。帧有几种类型:文本帧(opcode 0x1)、二进制帧(opcode 0x2)、关闭帧(0x8)、Ping/Pong 帧(0x9/0xA)。实际项目里,绝大多数业务数据走的是文本帧,内容是 JSON 字符串;少数走二进制帧,可能是 protobuf 或 MessagePack 序列化后的数据。

理解这一点很关键:mitmproxy 抓到的 WebSocket 消息,文本帧可以直接当字符串处理,二进制帧你得先搞清楚它的序列化格式,否则拿到的就是一堆乱码。我后面会专门讲怎么判断和处理二进制帧。

2.3 为什么选 mitmproxy 而不是浏览器插件

有人会问,浏览器开发者工具里 WS 那一栏不就能看到消息吗,为什么还要用 mitmproxy?原因有三个。第一,浏览器工具只能看当前页面,没法批量、自动化地记录;第二,页面一刷新,之前的帧记录就没了,长连接场景下数据量大,手动复制不现实;第三,mitmproxy 可以在帧到达的瞬间触发脚本,做实时解析、过滤、入库,这是浏览器工具做不到的。

当然,如果你只是想快速看一眼某个页面的 WS 消息长什么样,浏览器工具足够了。但要落地成一个稳定的采集系统,mitmproxy 是更合适的选择。

3. mitmproxy 拦截 WebSocket 的完整环境搭建

3.1 安装与版本选择

mitmproxy 的安装本身不复杂,但我强烈建议用独立的虚拟环境,别装到系统 Python 里。原因是 mitmproxy 对 Python 版本和依赖库版本比较敏感,跟其他项目混在一起容易出冲突。

python -m venv mitm_env source mitm_env/bin/activate # Windows 用 mitm_env\Scripts\activate pip install mitmproxy

版本上,我实测下来 9.x 和 10.x 对 WebSocket 的支持都比较稳定。10.x 的 API 有一些变化,比如flow.websocket对象的属性命名调整过,如果你参考的是老教程,注意对照官方文档核对一下。装完之后mitmproxy --version确认一下,同时会看到mitmdump和mitmweb两个命令,前者是无界面命令行版,适合跑脚本;后者带 Web 界面,适合调试。

3.2 证书配置:这一步 90% 的人会卡住

mitmproxy 要解密 HTTPS 流量,必须让客户端信任它的 CA 证书。这一步是新手最容易翻车的地方,我见过太多人卡在这里以为工具坏了。

流程是这样的:先启动一次mitmproxy,它会在~/.mitmproxy/目录下生成一套证书,其中mitmproxy-ca-cert.pem是根证书。然后你需要把这套证书装到发起请求的客户端上。如果客户端是浏览器,就在系统或浏览器里导入这个根证书并设为信任;如果客户端是手机 App,就得在手机的系统设置里安装。

注意:很多 App 会做证书校验(SSL Pinning),即使你装了 mitmproxy 的证书,它也会拒绝连接。这种情况下 mitmproxy 抓到的就是加密的乱码或者直接握手失败。遇到这种情况,说明目标做了额外的证书绑定,需要另想办法,本文不展开。

配置代理的时候,让客户端的 HTTP 和 HTTPS 代理都指向 mitmproxy 监听的地址和端口(默认 8080)。浏览器可以用 SwitchyOmega 之类的插件切换,手机就在 WiFi 设置里手动填代理。

3.3 验证拦截是否生效

配置好之后,随便打开一个网页,看 mitmproxy 界面里有没有流量进来。如果能看到请求列表,说明代理和证书都通了。这时候再打开一个带 WebSocket 的页面,在 mitmproxy 里按W键(mitmweb 里点 WS 标签),就能看到 WebSocket 连接和帧了。

如果 HTTPS 流量能看到但 WebSocket 看不到,检查两点:一是目标页面是否真的用了 WebSocket(有些是轮询伪装成实时);二是 mitmproxy 版本是否支持 WS 拦截(7.0 以后都支持)。

4. 用脚本把 WebSocket 帧变成结构化数据

4.1 mitmproxy 的 WebSocket 事件钩子

mitmproxy 提供了几个和 WebSocket 相关的钩子函数,最常用的是这三个:

  • websocket_connect(flow):连接建立时触发,可以在这里记录连接信息。
  • websocket_message(flow):每收到一个帧就触发一次,这是核心。
  • websocket_end(flow):连接关闭时触发。

在websocket_message里,flow.websocket.messages是一个列表,最后一个元素就是刚到的帧。每个 message 对象有content(帧内容,bytes 类型)、from_client(布尔值,区分方向)、type(帧类型)等属性。

4.2 一个能直接跑的解析脚本

下面这个脚本是我实际项目里简化出来的,功能是把所有文本帧里的 JSON 解析出来,按方向和时间戳打印,同时写入文件。

import json import time from mitmproxy import ctx class WSHook: def __init__(self): self.log_file = open("ws_frames.log", "a", encoding="utf-8") def websocket_message(self, flow): msg = flow.websocket.messages[-1] direction = "S->C" if not msg.from_client else "C->S" ts = time.strftime("%Y-%m-%d %H:%M:%S") raw = msg.content # 尝试按文本帧解析 try: text = raw.decode("utf-8") except UnicodeDecodeError: ctx.log.info(f"[{ts}] {direction} 二进制帧,长度 {len(raw)}") return # 尝试解析 JSON try: data = json.loads(text) pretty = json.dumps(data, ensure_ascii=False) ctx.log.info(f"[{ts}] {direction} JSON: {pretty[:200]}") self.log_file.write(f"{ts}\t{direction}\t{pretty}\n") except json.JSONDecodeError: ctx.log.info(f"[{ts}] {direction} 非JSON文本: {text[:200]}") self.log_file.write(f"{ts}\t{direction}\t{text}\n") self.log_file.flush() addons = [WSHook()]

启动方式是mitmdump -s ws_hook.py。跑起来之后,所有经过的 WebSocket 帧都会被记录到ws_frames.log里,同时控制台会实时打印。

4.3 二进制帧的处理思路

如果日志里出现大量"二进制帧",说明目标用的是二进制协议。这时候你得先判断序列化格式。常见的判断方法:看帧的前几个字节有没有明显的特征。protobuf 通常没有固定魔数,但字段结构有规律;MessagePack 的第一个字节往往能对应到类型(比如 0x82 表示 2 元素的 map)。

我一般的做法是先把几帧二进制内容 dump 成文件,用十六进制工具看一眼,再结合前端代码里的序列化库(搜protobuf、msgpack、flatbuffers这些关键词)来判断。确定格式后,用对应的 Python 库反序列化即可。这一步没有通用解,得具体问题具体分析。

5. 实战中那些文档不会告诉你的坑

5.1 帧被分片,一条消息拆成好几段

WebSocket 协议允许把一条逻辑消息拆成多个帧发送,第一个帧的 FIN 位是 0,最后一个帧 FIN 位是 1。mitmproxy 的websocket_message钩子是按帧触发的,不是按消息触发的。这意味着你可能会收到半截 JSON,json.loads直接报错。

我踩这个坑的时候,日志里全是JSONDecodeError,一度以为是编码问题。后来才发现是分片。解决办法是在脚本里维护一个缓冲区,遇到 FIN 为 0 的帧先存起来,等 FIN 为 1 的帧到了再拼接解析。mitmproxy 的 message 对象有is_finished之类的属性可以判断,具体字段名随版本略有差异,用之前查一下当前版本的文档。

5.2 心跳帧淹没业务数据

很多 WebSocket 连接会定期发 Ping/Pong 或者自定义的心跳 JSON(比如{"type":"ping"})。这些帧频率高、没价值,如果不过滤,日志会被刷屏,真正有用的数据反而被淹没。

我的做法是在脚本里加一层过滤:先判断帧类型,Ping/Pong 直接 return;文本帧里如果 JSON 的某个字段(比如type或cmd)是心跳标识,也跳过。过滤规则最好做成可配置的,因为不同项目的心跳格式不一样。

5.3 连接断开后数据就断了

WebSocket 长连接不是永久的,服务端可能因为超时、负载、重启等原因主动断开。一旦断开,你的采集就停了。mitmproxy 本身不会自动帮你重连,因为它是被动拦截,连接的发起方是客户端。

实际项目里,我的处理方式是:mitmproxy 只负责"抓",重连逻辑放在客户端侧。如果客户端是你自己控制的(比如你自己写的脚本去连),那就在客户端实现指数退避重连;如果客户端是别人的页面或 App,那你能做的就是监控连接状态,断了就提醒人工介入,或者用自动化工具重新触发页面加载。

5.4 数据量大了之后内存和磁盘都吃不消

长连接跑几个小时,帧数量可能到几十万条。如果脚本里把所有帧都堆在内存里,迟早 OOM。所以一定要边收边写,写完就释放。我上面的脚本用的是追加写文件 + 及时 flush,就是这个考虑。如果数据量特别大,建议直接写数据库或者消息队列,别用文件。

另外,日志文件要定期切割,不然一个文件几个 G,后面分析起来很痛苦。可以用 Python 的logging.handlers.RotatingFileHandler,或者干脆按小时/按天分文件。

6. 从抓到的帧到可用的数据集

6.1 先做数据画像,别急着入库

抓到一堆帧之后,别急着往数据库里灌。先花点时间做数据画像:统计一下有多少种不同的消息类型,每种类型的字段有哪些,哪些字段是稳定的,哪些是变化的。这一步能帮你发现很多问题,比如某些字段是时间戳需要转换、某些字段是嵌套结构需要打平、某些消息其实是重复推送。

我一般会写个小脚本,把日志里的 JSON 全部读出来,统计每个顶层 key 出现的频率,以及每个 key 对应的值类型分布。跑一遍下来,数据结构就清清楚楚了。

6.2 去重和时序处理

WebSocket 推送的数据经常有重复,尤其是状态类消息,服务端可能每隔几秒就把当前全量状态推一遍。如果你直接入库,会有大量冗余。去重的关键是找到一个业务主键(比如某个 ID 字段),结合时间戳判断是否是新数据。

时序上要注意,帧到达的顺序不一定等于业务发生的顺序,尤其是分片和网络抖动的情况下。如果业务对时序敏感,最好用消息里自带的时间戳字段排序,而不是用你收到帧的本地时间。

6.3 落库方案的选择

小规模数据(每天几万条以内),直接写 SQLite 或 CSV 就够了,简单省事。中等规模(每天百万级),上 PostgreSQL 或 MySQL,配合批量插入。大规模(每天千万级以上),考虑时序数据库或者消息队列 + 流处理。

我个人的经验是,别一上来就搞复杂架构。先用最简单的方案跑通,等数据量真的上来了再优化。很多项目其实根本到不了需要分布式的那一步,过早优化纯属浪费时间。

7. 几个我反复用到的实操技巧

第一个技巧是给帧打标签。在脚本里根据消息内容自动分类,比如"行情类""状态类""错误类",写日志的时候带上标签。这样后面分析的时候可以直接按标签过滤,不用每次都在海量日志里翻。

第二个技巧是保留原始帧。解析后的结构化数据方便使用,但原始帧一定要留着。因为你的解析逻辑可能会变,或者后来发现某个字段之前漏了,有原始帧就能重新解析,没有就只能重新抓。

第三个技巧是用 mitmweb 做可视化调试。写脚本之前,先用 mitmweb 把 WS 流量看一遍,确认消息格式和频率,心里有数了再写解析逻辑,能少走很多弯路。

第四个技巧是注意字符编码。有些服务端推的中文是 GBK 编码的,直接decode("utf-8")会报错。遇到解码失败,先试试gbk或gb18030,再不行就用chardet之类的库自动检测。

这套 WebSocket + mitmproxy 的方案,我从最早的手忙脚乱到现在能比较从容地应对各种实时数据接口,中间踩的坑基本都写在上面的内容里了。它不是什么银弹,遇到强加密或者自定义二进制协议一样得啃硬骨头,但至少给你多开了一扇窗——当 HTTP 那条路走不通的时候,别忘了抬头看看 WS 那一栏。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 11:56:36

OpenShell:跨平台系统级诊断工具原理与实战

1. OpenShell 不是 Shell,而是一把“系统级万能钥匙” 很多人第一次看到 OpenShell 这个名字,下意识会以为它是某种 Linux 或 macOS 的新终端替代品——比如像 Zsh、Fish 那样带自动补全和主题的 shell。但事实恰恰相反: OpenShell 与命令行…

作者头像 李华
网站建设 2026/10/15 11:56:36

CLI工具设计与发布实战:从命名误区到可信分发

1. “Agent-Reach”不是新框架,而是一个被严重误读的CLI工具命名现象 最近在多个技术社区和GitHub趋势榜上反复看到“Agent-Reach”这个词——它频繁出现在搜索热词、安装教程、镜像加速讨论甚至Python入门帖里。但翻遍PyPI、GitHub官方索引、主流CLI工具聚合平台&a…

作者头像 李华
网站建设 2026/10/15 11:56:32

CFDTool:MATLAB环境下开源CFD仿真从入门到批处理实战

简介:CFDTool是一款专为MATLAB环境设计的开源计算流体动力学工具箱,面向需要开展流体流动与耦合传热仿真的工程师、科研人员和学生。其图形界面简化了CFD建模流程,并支持OpenFOAM与SU2求解器集成,可在1D/2D/3D及轴对称坐标系中完成…

作者头像 李华
网站建设 2026/10/15 11:56:20

Varscan1文献解读:读段计数框架在体细胞突变检测中的原理与实战

1. 从一次肿瘤样本复核说起:为什么还要读Varscan的文献去年帮一个做肿瘤方向的朋友复核一批配对样本的体细胞突变结果,他用的是某款主流的贝叶斯类检测器,跑出来的候选位点数量很漂亮,但真阳性率一直上不去。我建议他换一个思路&a…

作者头像 李华
网站建设 2026/10/15 11:56:10

手写BMP180驱动:I2C时序、寄存器与校准补偿实战解析

简介:面向STM32开发者的BMP180气压与温度传感器驱动资源,解决在嵌入式项目中快速接入BMP180并稳定读取环境数据的问题。包内提供软件IIC与硬件IIC两套驱动方案:软件IIC通过GPIO模拟时序,灵活适配任意引脚;硬件IIC利用S…

作者头像 李华
网站建设 2026/10/15 11:56:10

微信小程序粤语文化平台开发实践

1. 项目背景与核心价值 粤语作为中国南方地区的重要方言,承载着丰富的文化内涵和历史记忆。随着移动互联网的普及,方言文化的传承面临新的机遇与挑战。这个基于微信小程序的粤语文化传播平台,正是针对这一需求而设计的数字化解决方案。 我在…

作者头像 李华