☰
用Python从零构建抓包工具:实时抓包、离线分析与代理调试
2026/10/13 10:38:12 网站建设 项目流程

做小程序后端联调时,我经常遇到这种局面:客户端明明发了某个请求,服务端日志里却什么都没有;真机测试一切正常,一上体验版就超时;前端说参数格式没问题,后端说根本不是自己定义的字段。两边都拿着各自的日志解释,吵到最后会发现缺了一个中立的视角——把客户端实际发出的字节原样摆出来。抓包工具就是干这件事的。

这篇指南不聊现成的抓包软件怎么点鼠标,而是用 Python 从零搭一个够用的抓包工具。它不只是一个 Python 脚本,而是一套可以覆盖三种常见需求的框架:实时抓取网卡流量、离线分析 pcap 文件、以及在小程序调试时充当本地代理做请求留痕。适合小程序前端联调、后端排查接口问题、以及刚接触流量分析想搞明白原理的开发者。

先说清楚边界:所有示例都假定你是在调试自己开发环境里的小程序,或者你拿到了明确授权可以做流量分析。没有权限的前提下,任何抓包行为都不应该执行,这一点后面不反复强调。

1. 先把需求理清楚:抓包工具到底要解决什么问题

1.1 小程序联调最容易出现的三类问题

我做小程序项目时,最常见的网络问题其实就三类。

第一类问题是“请求根本没发出去”。很多看起来是后端的问题,最后发现是前端逻辑被拦截了,或者网络库在某些机型上走了缓存。这种场景下,服务端日志永远是一片空白,但客户端自己看又觉得一切正常。你不抓包就分不清到底是前端没发、还是发到了别的地方。

第二类问题是“请求发出去了,但内容不对”。参数名拼错、JSON 类型对不上、签名没算对、请求头少了关键字段,这些都是联调期的高频问题。后端说“我收到的不是这个”,前端说“我明明发的是对的”。把实际发出的请求头、请求体、目标地址和解包后的响应内容全部列出来,双方一眼就能对齐。

第三类问题是“接口时序和重试行为不确定”。小程序里经常有并发请求、轮询、以及失败后的自动重连。出现问题的时候,单看某一次请求很难判断整个链路是否有问题。抓包能看到时间戳和请求顺序,这比打开一堆页面手动核对要直观得多。

围绕这三类诉求,工具需要有足够基础的抓包能力,而不是只做界面展示。这也是我决定用 Python 自建而不是继续依赖图形化抓包工具的原因。

1.2 为什么选择 Python 而不是直接装个现成工具

现成工具当然好用,Fiddler、Charles 都很成熟。但自己做工具有几个独立的价值。

第一,日志格式可控。团队内部排查问题时,经常要把请求记录贴到群里或者工单里。Charles 的导出格式不一定符合团队习惯。自己定义 JSONL 输出之后,每条记录只保留关键字段,拿出去沟通成本很低。

第二,能够方便地和自动化流程结合。比如把抓包结果喂给数据校验脚本,或者在上线前自动跑一轮接口回归。图形化工具要再做一层解析,自己写的工具直接从内存里读出结构化数据。

第三,深入理解协议。当你亲手解析过一次 TCP 流和 HTTP 报文之后,再去看很多网络现象会有完全不同的感觉。比如为什么本地抓包能看到明文,而线上抓到的是加密密文;为什么会有 TCP 粘包;为什么代理模式要单独处理 CONNECT 方法。这些在图形界面里都被隐藏了。

Python 生态里做抓包有几个可用选项。scapy 适合交互式分析,灵活但对新手有一点理解成本;dpkt 轻量、适合离线解析;pyshark 是对 tshark 的封装,能够直接利用 Wireshark 的解析能力。我习惯的组合是:实时抓包用 scapy 的 sniff,离线解析用 pyshark 处理重组的 HTTP 层,代理模式用 http.server 配合 mitmproxy 做协议侧录。

2. 方案设计:一个抓包工具的模块划分

2.1 工具的三种工作模式

在设计这个小工具时,我把它分成三个互不干扰的入口。

第一个入口是网卡实时抓包。适用于设备在同一个局域网内,比如手机连着测试环境的 Wi-Fi,我们需要看到当前正在产生哪些网络请求。因为不修改客户端的任何配置,这种模式下看到的就是真实物理链路上的包,代价是 HTTPS 流量只能看到密文。

第二个入口是离线 pcap 分析。适用于 tcpdump 已经抓到文件的情况。把文件交给脚本,解析出里面有价值的 HTTP 请求和响应,过滤掉图片、字体这些静态资源。这种方式对现场环境没有要求,适合事后复盘。

第三个入口是本地代理模式。把手机或者小程序的网络代理指向电脑端口,所有流量集中经过我们自己的程序。这个模式最接近日常调试体验,能配合 HTTPS 加解密得到明文的请求和响应内容,但前提是必须只在有权限的测试环境里使用。

三个入口最终都朝同一个方向收敛:把原始流量变成一条一条结构化的记录。记录的核心字段固定为时间戳、源地址、目标地址、方法、URL、状态码、关键请求头、响应体预览。这样三种模式可以共用同一套下游处理逻辑。

2.2 流量数据应该怎么建模

我见过很多临时抓包脚本,写到后面就变成一坨 print,输出格式三天两头改,没法复用。所以最初就要把记录的结构定义清楚。

用 dataclass 定义一条请求记录,字段包括元数据和预警标记。元数据就是上面说的那些通用字段。预警标记用于标记可疑点,比如某个请求耗时超过阈值、响应状态码异常、请求体字段超出预期等等。

关键是确认所有模式输出的记录统一。实时抓包输出的 JSON 和代理模式输出的 JSON 放在同一个目录下,后续做统计、画图、比对才有可能。我通常把每一行 JSON 作为一条记录,最终落盘成 JSONL 文件。后续查找某个订单号或者某个用户 ID 相关的全部请求时,用 grep 就能直接过滤。

2.3 过滤规则要提前设计,否则日志会爆炸

不设置过滤规则就去抓包,十分钟就能产出几百 MB 的文本日志。抓包工具必须内置过滤层。

过滤分三个维度。第一是主机名,只记录与目标接口域名匹配的请求;第二是路径关键词,比如登录、下单、支付相关的接口;第三是资源类型,把图片、字体、音视频等静态资源跳过。对小程序调试来说,尤其要忽略那些启动时批量加载的静态资源包,否则有效请求会被淹没。

过滤不是越严越好,建议默认记录所有文本接口,对二进制内容做截断。响应体只保留前 2KB 左右的预览就足够判断问题,既能保留足够信息,又不至于让日志大到失控。

2.4 合规使用的前提条件

这一点必须单独说。抓包工具属于网络分析类的调试工具,它本身是开发和安全测试中的常规手段。但是否能对某个目标使用,取决于你是否拥有该系统或是否获得充分授权。

自测自己的小程序后端、把代理指向自己可控的测试环境、对已授权的应用做安全评估,这些都是合理场景。相反,拦截别人设备的通讯内容,或者试图绕过商业应用的传输保护措施,不仅超出技术探讨范围,更是明显的违规行为。文章的代码我只用于描述自测和授权场景,希望你也这样使用。

3. 动手实现:从零搭一套可运行的抓包框架

3.1 目录结构和核心流程

工具不复杂,目录按边界分清楚就行。

packet-debug/ ├── capture_live.py # 网卡实时抓包入口 ├── capture_file.py # 离线 pcap 解析入口 ├── proxy_mode.py # 本地代理入口 ├── models.py # 数据模型和过滤规则 ├── exporter.py # 统一的 JSONL 输出 └── recorder.py # 供 mitmproxy 调用的侧录插件

主流程从左到右依次是:流量源进入、按过滤规则筛选、解析出结构化字段、归一化字段后落盘。后面三个环节解析和导出是纯函数,输入输出都很简单。只要流量源那一层能够正常采集,其余部分基本不需要改动。

3.2 数据模型与过滤规则实现

先把 models.py 写出来,作为后续所有模块的依赖。

from dataclasses import dataclass, field, asdict from typing import Optional import json import re @dataclass class HttpTrace: timestamp: float src_ip: str dst_ip: str method: str = "" host: str = "" path: str = "" status_code: Optional[int] = None request_headers: dict = field(default_factory=dict) response_body_preview: str = "" duration_ms: Optional[float] = None def to_json(self) -> str: return json.dumps(asdict(self), ensure_ascii=False) class RuleFilter: def __init__(self, hosts=(), path_keywords=(), ignore_extensions=()): self.hosts = set(hosts) self.path_keywords = list(path_keywords) self.ignore_extensions = tuple(ignore_extensions) def should_keep(self, host: str, path: str) -> bool: if self.ignore_extensions and path.lower().endswith(self.ignore_extensions): return False if self.hosts and host not in self.hosts: return False if self.path_keywords and not any(k in path for k in self.path_keywords): return False return True

开发中有一个经验:保存过滤配置时保留一份全量记录开关。遇到复杂问题时要能快速关闭过滤规则,把所有流量都先存下来,事后再分析。我在实际调试中就遇到过某个异常请求的路径写法超出预期,过滤器把它丢掉了,最后只能重新抓一遍。所以过滤规则设计成可动态开关的。

3.3 实时抓网卡流量

scapy 的 sniff 是实时抓包最直接的入口。下面的代码监听本机 80 端口的 TCP 数据,解析出 HTTP 请求首行和 Host 字段,并按照过滤规则判断是否需要保留。

from scapy.all import sniff, IP, TCP, Raw from models import HttpTrace, RuleFilter RULE = RuleFilter( hosts={"api.test.local", "applet-gateway.example.cn"}, path_keywords={"/v1/", "/order", "/login"}, ignore_extensions=(".jpg", ".png", ".woff2", ".mp4"), ) def parse_http_from_payload(payload: bytes): try: text = payload.decode("utf-8", "ignore") except Exception: return None if not text.startswith(("GET ", "POST ", "PUT ", "DELETE ", "PATCH ")): return None head, _, _ = text.partition("\r\n\r\n") lines = head.split("\r\n") parts = lines[0].split() if len(parts) < 2: return None method, path = parts[0], parts[1] host = "" for line in lines[1:]: if line.lower().startswith("host:"): host = line.split(":", 1)[1].strip() break return method, host, path def on_packet(pkt): if not (pkt.haslayer(IP) and pkt.haslayer(TCP)): return ip = pkt[IP] tcp = pkt[TCP] payload = bytes(tcp.payload) if not payload: return parsed = parse_http_from_payload(payload) if not parsed: return method, host, path = parsed if not RULE.should_keep(host, path): return trace = HttpTrace( timestamp=float(pkt.time), src_ip=ip.src, dst_ip=ip.dst, method=method, host=host, path=path, ) print(trace.to_json()) sniff(prn=on_packet, filter="tcp port 80", store=False)

这段代码有一个明显限制:对于跨多个 TCP 数据段的 HTTP 请求,单包解析会失效。遇到这类情况,最简单的处理是先落盘再用 pyshark 分析,而不是自己去实现 TCP 重组。实时抓包的精确定位只在请求足够小且网络环境简单的场景下才可靠。在工具中留一个开关,默认把实时抓包的原始包同时落盘,后续用离线解析做复核。

3.4 离线解析 pcap 文件

解析 pcap 文件我推荐 pyshark。它背后的 tshark 已经处理好了 TCP 重组和 HTTP 头部分割,我们能直接拿到 layer 级别的字段。

import pyshark from models import HttpTrace, RuleFilter def parse_pcap(path: str, rule: RuleFilter): cap = pyshark.FileCapture(path, display_filter="http") traces = [] for packet in cap: try: http = packet.http host = getattr(http, "host", None) or "" path = getattr(http, "request_uri", None) or "" method = getattr(http, "request_method", None) or "" status = getattr(http, "response_code", None) if not rule.should_keep(host, path): continue trace = HttpTrace( timestamp=float(packet.sniff_timestamp), src_ip=packet.ip.src, dst_ip=packet.ip.dst, method=method, host=host, path=path, status_code=int(status) if status else None, ) traces.append(trace) except AttributeError: continue cap.close() return traces if __name__ == "__main__": result = parse_pcap("debug.pcap", RULE) for item in result: print(item.to_json())

实践提示:pyshark 依赖本机 tshark,环境搭建时记住把 tshark 的可执行目录加到系统 PATH 中,否则会报找不到可执行文件的错误。另外,FileCapture 默认是一次性读入所有包,处理大文件时建议用 keep_packets=False,让它边读边丢弃,降低内存占用。

3.5 本地代理模式的实现

代理模式是日常调试小程序最常用的方式。手机设置代理指向电脑之后,所有 HTTP 请求都会先经过我们的程序。对于 HTTP 明文流量,自己实现一个简单代理就够。对于 HTTPS,推荐把 mitmproxy 作为代理底座,我们再写一个 Python 插件处理记录,这样能省掉证书管理和 TLS 解密的大量细节。

先看一个最简的 HTTP 转发代理,直接基于 http.server 实现。

from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.request import urlopen, Request class MiniProxy(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def _forward(self): body = self.rfile.read(int(self.headers.get("Content-Length", 0) or 0)) req = Request( url=f"http://{self.headers['Host']}{self.path}", data=body or None, headers=dict(self.headers), method=self.command, ) with urlopen(req, timeout=10) as resp: self.send_response(resp.status) for key, value in resp.headers.items(): self.send_header(key, value) self.send_header("Content-Length", str(len(resp.read()))) self.end_headers() data = resp.read() self.wfile.write(data) do_GET = _forward do_POST = _forward do_PUT = _forward do_DELETE = _forward def log_message(self, fmt, *args): pass server = HTTPServer(("0.0.0.0", 8899), MiniProxy) print("proxy running on 8899") server.serve_forever()

这段代码只负责转发,没有记录功能。要记录,在 _forward 里把 method、URL、状态码和 body 按 HttpTrace 保存一份即可。注意这个简易实现不支持 CONNECT 方法,所以 HTTPS 请求会失败。真正调试 HTTPS 时,用 mitmproxy 更稳。

3.6 用 mitmproxy 插件做 HTTPS 侧录

mitmproxy 是一个成熟的 Python 代理工具,支持加载插件脚本。我们新建 recorder.py,在请求和响应两个阶段分别记录。

from mitmproxy import http, ctx import json, time IGNORE_EXTENSIONS = (".jpg", ".png", ".woff2", ".mp4", ".js", ".css") class Recorder: def __init__(self): self.start_ts = {} def request(self, flow: http.HTTPFlow) -> None: url = flow.request.pretty_url path = flow.request.path if path.lower().endswith(IGNORE_EXTENSIONS): flow.metadata["skip"] = True return self.start_ts[flow.id] = time.time() ctx.log.info(f">> {flow.request.method} {url}") def response(self, flow: http.HTTPFlow) -> None: if flow.metadata.get("skip"): return try: body_preview = flow.response.get_text(strict=False)[:2000] except Exception: body_preview = "" record = { "timestamp": time.time(), "method": flow.request.method, "url": flow.request.pretty_url, "status": flow.response.status_code, "duration_ms": round((time.time() - self.start_ts.get(flow.id, time.time())) * 1000, 2), "request_headers": dict(flow.request.headers), "response_body_preview": body_preview, } with open("trace.jsonl", "a", encoding="utf-8") as fp: fp.write(json.dumps(record, ensure_ascii=False) + "\n") addons = [Recorder()]

启动命令很简单:

mitmproxy -s recorder.py -p 8899

手机连上同一局域网,Wi-Fi 代理设置为电脑 IP 加 8899 端口,然后在调试小程序里发起请求。首次使用时,手机会提示安装 mitmproxy 的根证书。这个操作只应在自己的测试设备上执行,并且测试结束后最好移除该证书。

需要注意的一点是,很多小程序运行在含有证书固定逻辑的容器里。如果遇到证书校验失败,不要试图绕过它。证书固定本身就是保护机制,绕过它已经不属于正常调试范畴。你需要的是在服务端配合下,把测试环境切到允许信任的证书,或者在掌握完整环境中调整配置,而不是去破解别人应用的证书校验逻辑。

3.7 输出和导出:让日志可以继续喂给其他系统

抓到数据后,我通常会把 JSONL 文件和一个小型查询脚本放在一起。这样后续可以直接用 Python 做统计,也可以导入到其他分析平台。

import pandas as pd df = pd.read_json("trace.jsonl", lines=True) slow_requests = df[df["duration_ms"] > 500].sort_values("duration_ms", ascending=False) print(slow_requests[["method", "url", "status", "duration_ms"]].head(20))

这个步骤很简单,价值却不小。排查线上问题时,把耗时排名前几的请求拉出来,往往比看错误日志更快定位瓶颈。特别是小程序冷启动阶段的那批并发请求,经常能在这个排名里暴露出明显的串行问题。

4. 实操中的常见问题与排查记录

4.1 实时抓包提示“Operation not permitted”

大部分操作系统不允许普通用户在网卡上进行原始套接字操作。scapy 的 sniff 经常会直接抛权限错误。

解决路径是运行工具时使用管理员权限,或者在开发环境给 Python 解释器单独放开能力位。macOS 上我习惯直接在终端里加 sudo,Linux 上可以用 setcap 限制到 python3 可执行文件,这样弱化权限风险的同时也能避免总是用管理员身份跑脚本。

不同平台网络设备名不一样。启动抓包前先看一下设备列表,确认本机对应的物理网卡或 Wi-Fi 接口名称。抓错接口是最常见的问题,表现是程序没有报错,但一直没有包进来。

4.2 抓到了包,但 HTTP 解析不出来

这个问题八成发生在 TCP 数据被拆分的情况。单个请求被分成了多个 TCP 段,当前包里只有一部分负载,直接用字符串判断开头必然失败。

我的做法是回到 pyshark 方案,它做的是流重组而不是单包解析。说实话,除非你在写协议分析工具,否则没有必要自己实现 TCP 重组。OSI 模型那一层的问题交给成熟的解析库处理,我们专注业务侧。

4.3 为什么 HTTPS 抓到的全是密文

这是最常见也最容易误判的一个点。HTTPS 的明文加密发生在传输层之上,如果只是在网络链路层抓包,看不到任何符合 HTTP 特征的文本。你想要看到明文,必须在某一个终点上做 TLS 解密,或者用调试代理配合测试证书解开加密通道。

顺带提醒一句:小程序里面的数据请求大部分都走了 HTTPS。如果你的自建工具只做了明文解析,那场景会受到很大限制。真正落地的时候,要么把关注的接口临时改成 HTTP 提供到测试环境,要么就用 mitmproxy 这类支持 TLS 终止的工具。

4.4 大 pcap 文件内存撑不住

用 pyshark 的 FileCapture 解析大文件时,如果默认把每个包都保留在列表里,几百 MB 的包能轻松吃掉几个 GB 内存。解决办法是设置 keep_packets=False,逐包迭代处理。

scapy 同样面临这个问题。rdpcap 是一次性读进内存,文件一大会上百兆就可能卡死。优先用 PcapReader 配合循环读取,每次只处理一个包。

from scapy.all import PcapReader from scapy.layers.inet import IP, TCP with PcapReader("large.pcap") as pcap_reader: for pkt in pcap_reader: if pkt.haslayer(TCP): pass

4.5 流量里全是无关请求,有效信息被淹没

开始抓包之前不配置过滤规则,一旦启动,各种后台心跳、统计上报、静态资源请求全都涌进来。等你翻到目标请求时,眼睛已经看花了。

处理办法是在规则里加一层“动态开关”。平时用白名单过滤,只保留跟当前问题相关的域名和路径。如果遇到过滤器把有效请求也过滤掉的情况,立即切到全量记录模式,事后再离线分析。这个开关我一般做成命令行参数,比如--verbose-all。

5. 落地效果与工作流整合建议

工具写完之后,不要停留在“能跑”的层面。我建议把它嵌入到日常工作流里,形成一个固定的排查步骤。

第一步,联调开始前先启动代理模式,让整个测试期间的所有请求默认留痕。第二步,出问题时直接查当天的 JSONL 记录,按照时间、用户标识、接口名过滤。第三步,把可疑请求导出成格式化文本,贴给后端同学时附带完整请求头。第四步,如果是性能问题,就按照耗时统计拉一个排行,先看最慢的几个接口。

还有一个小技巧值得分享:在拦截规则里面加一个“关键字提取”字段,比如当前排查的订单号或用户 ID。只要请求或响应里出现这个关键字,就在记录上打一个标记。后续用一条 grep 命令就能把这个用户的所有网络行为串起来,对定位偶现问题很有帮助。

不要小看这种简单的记录习惯。它实际上把抓包从“出了事才临时抓”变成了“平时就在积累现场数据”。我合作过的一个后端团队,就是因为保留了完整的接口调用记录,后来排查一个线上偶发超时问题时,直接从历史记录里定位到了某个特定版本客户端的异常重试行为,省了大量线下复现时间。

另外,这个工具框架是可以继续扩展的。把过滤规则从简单的字符串匹配升级成正则表达式,或者接入消息队列把记录导到实时分析平台,都是顺理成章的事。核心数据模型稳定之后,上面的功能都能逐层叠加。

我在实际使用中最大的感受是:别把抓包工具当成一个冷冰冰的命令行程序,它更像是调试过程中的一个“旁路监视器”。你不需要理解每一个 TCP 标志位的含义,也不必掌握 Wireshark 的每一种显示过滤语法,只要能快速拿到自己关心的那几条请求,就已经解决了一大半问题。工具的价值永远是为问题服务的,不是为技术本身的复杂度服务的。

最后再分享一点经验:小程序的联调问题,十有八九都能通过“把请求原样摆出来”来定位。无论是参数类型、请求时序还是环境差异,抓包数据一放,争论自然就结束。你需要的不是一个功能庞大的系统,而是一条稳定、快速、可记录的通路。按这个思路去写,哪怕只有几百行代码,它也能成为你日常开发里最趁手的调试工具之一。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询