☰
Python网络协议报文解析与可视化:从Scapy抓包到Matplotlib出图
2026/10/3 10:29:51 网站建设 项目流程

简介:一套基于Python的网络协议报文解析与可视化工具及配套资料,附带完整文档与全部源码,面向计算机相关专业学生、教师及企业开发者,尤其适合人工智能、通信工程、自动化、电子信息、物联网等专业用于毕业设计、课程设计或项目初期立项演示,也适合新手循序渐进学习。工具实现了网络协议报文的解析、分类与可视化展示,代码组织清晰,可在现有基础上二次修改,便于拓展其他协议格式。压缩包共8个文件,包括两个Python源码、交互式示例笔记、详细说明文档、配置文件及授权文件等,整体仅21KB,轻量精炼。该项目为个人高分源码,已获导师指导认可,答辩评审分达95分,代码全部测试运行成功,功能稳定可用。目前已有90人浏览学习;读者可获得可直接运行的解析脚本、演示笔记、项目说明与配置资料,快速掌握Scapy库在报文分析中的实际应用,并可直接用于课程作业、毕业设计或项目实践。

1. 用Python做网络协议报文解析与可视化:这到底是什么,能省多少事

做网络协议报文解析,很多人第一反应是打开Wireshark,右键导出一下,觉得不就完事了吗。可真到要批量分析、要出图表、要和业务数据打通的时候,Wireshark那套手工界面根本扛不住。标题里这个“基于Python的网络协议报文解析及可视化工具”,本质上是把抓包、解析、特征提取、画图做进一条流水线:读入pcap/pcapng或实时网卡数据,按协议栈拆字段,再把时间序列、流量分布、协议占比可视化。适合不想每次手动导出一堆CSV、想在公司内网复用自己的解析逻辑、甚至要做自动化告警的工程师。这篇文章把我自己搭这套工具的路径拆开讲,从选型到踩坑,最后给验证技巧,保证你照着能跑,跑完能改。

2. 报文解析的底层思路与工具选型:从socket到scapy,先定解析框架

2.1 为什么首选Scapy而不是手工解析

网络报文说到底就是一串字节。所谓解析,就是按协议规范把这串字节切成一个个字段:MAC地址占6字节,IP头里有版本、TTL、源地址,TCP头里有端口、序号、标志位。如果只用Python标准库,一条原始socket recv出来的数据都要自己切,做三次握手的场景就得写几百行struct.unpack,还不一定有现成的CRC校验。所以先不要自己造轮子,在Python生态里,解析网络报文有个绕不开的库叫Scapy。

常见做法是用Scapy读pcap文件,它能自动识别以太网、IP、TCP、UDP这些常见协议,把每个字段映射成对象属性。选型对比下来,有几个候选:

方案原理适合场景缺点
裸socket + struct.unpack手动按协议头切字节固定的私有协议、嵌入式串口代码量大,协议一变就要改
dpkt纯Python轻量解析只做读pcap、提取字段API偏底层,不支持发包
Scapy对象化协议栈,支持解析/构造/抓包网络协议分析与工具原型大文件解析慢,内存占用高
pyshark调用tshark内核,解析结果与Wireshark一致需要保持和Wireshark完全一致依赖外部tshark,性能受限于tshark

我一般会选Scapy做主解析,dpkt做备选。为什么不是pyshark?因为它本质是给tshark套了一层壳,你机器上必须装Wireshark才能用,在服务器上部署很不方便。Scapy是纯Python包,pip install scapy就能装,常见协议都认识,还自带抓包功能。下面是最小解析代码:

from scapy.all import rdpcap, IP, TCP, UDP pkts = rdpcap("capture.pcap", count=500) # 只读前500包 for pkt in pkts: if IP in pkt: src = pkt[IP].src dst = pkt[IP].dst if TCP in pkt: sport = pkt[TCP].sport dport = pkt[TCP].dport flags = pkt[TCP].flags print(f"{src}:{sport} -> {dst}:{dport} flags={flags}") # 非IP帧(比如ARP)此时不会走IP分支

rdpcap是全量读入内存,count参数控制读取数量,适合先探路。pkt[IP].src是Scapy把IP头解析成层对象后的字段访问方式,flags是TCP标志位。跑这个脚本能快速看到数据包里有没有IP协议、有哪些端口在通信。注意:如果pcap里有非IP帧,直接pkt[IP]会抛异常,所以要先if IP in pkt判断层是否存在。这也是新手最容易踩的坑,后面避坑章会说。

2.2 解析模块的目录结构与公共函数

实际工具不能只有一个脚本,我一般这样组织目录:

protocol_tool/ ├── pcap_reader.py # 读pcap/pcapng,统一入口 ├── protocols/ # 协议解析扩展包 │ ├── __init__.py │ ├── eth_ip_tcp.py # 传统IP协议解析 │ ├── modbus_rtu.py # Modbus RTU解析 │ ├── can_bus.py # CAN报文解析 │ └── iec698.py # 电网698协议解析(示例) ├── visualizer.py # matplotlib画图 ├── utils/ │ ├── byte_tools.py # 字节转换工具 │ └── time_utils.py # 时间戳处理 ├── config.yaml # 解析参数配置 └── output/ # 输出csv/png

公共函数里最常用的就是字节转十六进制、大小端转整型、MAC/IP地址格式化。比如写一个hexdump,用来在调试时打印报文原始字节:

# utils/byte_tools.py def hexdump(data: bytes, base=0, width=16) -> str: """按Wireshark风格输出hexdump,便于和抓包对照""" lines = [] for i in range(0, len(data), width): chunk = data[i:i+width] hex_part = " ".join(f"{b:02x}" for b in chunk) ascii_part = "".join(chr(b) if 32 <= b <= 126 else "." for b in chunk) lines.append(f"{base+i:04x} {hex_part:<{width*3}} {ascii_part}") return "\n".join(lines) if __name__ == "__main__": print(hexdump(b"\x01\x02abc"))

width控制每行显示多少字节,Wireshark默认是16,也可以设成8方便看串口报文。左侧偏移量能帮助定位解析到第几个字节。调试私有协议时,先用这个函数把报文打出来,再对着协议文档数偏移量,比用print(data)直接打印不可读的bytes要直观得多。

3. 手写一套可复用的报文解析与可视化工具:核心代码与参数说明

3.1 用Scapy解析PCAP文件并提取协议字段

现在开始做真正的解析工具。假设手上有一批pcap文件,来自公司网关、CAN总线采集盒或串口服务器。目标是把每条报文的协议类别、源目地址、端口、时间戳、关键载荷统一提出来存成结构化数据。常见做法是写一个读写分离的解析函数,只返回逻辑结果,不直接打印。

# pcap_reader.py from scapy.all import PcapReader, IP, TCP, UDP, Raw from datetime import datetime def parse_pcap(file_path, bpf_filter=None, max_pkts=None): """逐包读取pcap,返回解析记录列表""" records = [] with PcapReader(file_path) as reader: for idx, pkt in enumerate(reader): if max_pkts and idx >= max_pkts: break # BPF过滤:在scapy层自己实现,避免先读全包 if bpf_filter and not _simple_bpf_match(pkt, bpf_filter): continue rec = {} rec["index"] = idx rec["time"] = float(pkt.time) # Epoch时间戳 rec["src_mac"] = pkt.src if hasattr(pkt, "src") else None rec["dst_mac"] = pkt.dst if hasattr(pkt, "dst") else None if IP in pkt: rec["src_ip"] = pkt[IP].src rec["dst_ip"] = pkt[IP].dst rec["proto"] = pkt[IP].proto if TCP in pkt: rec["sport"] = pkt[TCP].sport rec["dport"] = pkt[TCP].dport rec["flags"] = str(pkt[TCP].flags) if UDP in pkt: rec["sport"] = pkt[UDP].sport rec["dport"] = pkt[UDP].dport if Raw in pkt: rec["payload_len"] = len(pkt[Raw].load) # 只保留前64字节做指纹,避免大载荷撑爆内存 rec["payload_hex"] = pkt[Raw].load[:64].hex() records.append(rec) return records

PcapReader是迭代器,逐包解析,不会一次性把整个文件读进内存,处理几个GB的pcap是关键。pkt.time是Scapy帮我们转好的Epoch浮点秒,可以直接和采集系统的日志对齐。_simple_bpf_match是自己写的一个简易过滤函数,比如只关心TCP端口。注意我这个parse_pcap返回的是列表,遇到超大文件建议改成yield生成器,后面避坑再说。

参数说明:max_pkts用于快速验证,设成1000能秒出结果;bpf_filter传字符串如"tcp and port 8080"时,我这里简化成只做端口匹配,如果你想用完整BPF语法,建议内部直接调用tcpdump或PcapReader的filter参数。实际工作中我倾向于在采集时就按端口过滤,减少无效流量。

3.2 把解析结果落成CSV并用Matplotlib画时序图

解析出来的记录如果只print在终端,谈不上可视化。下一步是把记录写成CSV,再用Matplotlib画流量时序图和协议占比图。这也是标题里“可视化工具”的核心落地。

# visualizer.py import csv from collections import Counter from datetime import datetime, timezone import matplotlib.pyplot as plt def records_to_csv(records, out_csv): """将解析记录写入CSV,字段顺序由fieldnames控制""" if not records: return fieldnames = records[0].keys() with open(out_csv, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) writer.writeheader() writer.writerows(records) print(f"saved {len(records)} records to {out_csv}") def plot_timeline(records, out_png, bucket_sec=1): """按bucket_sec聚合每秒包数/字节数,画折线""" bucket_times = [int(r["time"] // bucket_sec) for r in records] counter = Counter(bucket_times) if not counter: return start = min(counter) end = max(counter) x = list(range(start, end + 1)) y = [counter.get(t, 0) for t in x] # 转换为本地时间字符串 x_time = [datetime.fromtimestamp(t * bucket_sec, tz=timezone.utc) for t in x] plt.figure(figsize=(12, 4)) plt.plot(x_time, y, linewidth=1) plt.title("Packet Count per Second") plt.xlabel("Time (UTC)") plt.ylabel("Packets") plt.grid(True) plt.tight_layout() plt.savefig(out_png, dpi=100) print(f"saved timeline to {out_png}")

bucket_sec是聚合窗口,网络画像建议设为1秒,长时采集可以设60秒,避免曲线毛刺过多。timezone.utc必须显式指定,否则datetime.fromtimestamp会用本地时区,导致和采集端时间对不上。CSV用utf-8-sig编码,Excel打开不会乱码,这是给同事看报告时必备的小教养。

画完时序图,还得画协议占比图。协议占比最适合饼图,但Wireshark按包数统计,我们的工具可以同时统计“包数”和“字节数”,两者差异能提示小包高频攻击或大包传输场景。

def plot_proto_pie(records, out_png): proto_counter = Counter(r.get("proto") for r in records if r.get("proto")) labels = [] sizes = [] for proto, cnt in proto_counter.most_common(8): # 取top8 labels.append(f"{proto}({cnt})") sizes.append(cnt) plt.figure(figsize=(6, 6)) plt.pie(sizes, labels=labels, autopct="%1.1f%%", startangle=140) plt.title("Protocol Distribution") plt.savefig(out_png, dpi=100)

proto字段在3.1里取了IP层的协议号。6是TCP,17是UDP,1是ICMP。如果你发现大量协议号是自己不认识的,说明采集到了私有协议,这时候就要扩展解析逻辑。注意most_common(8)把长尾协议全部丢了,如果你想看小概率协议,可以把8改大或改用柱状图。

3.3 扩展支持CAN/Modbus/698这类非IP协议

标题里的“网络协议”不一定都是IP,工业现场常见的是CAN、Modbus RTU、CDT以及电网的698协议。这些报文在pcap文件里通常封装在Linux SocketCAN或TCP/UDP载荷里,Scapy默认不认识,需要自己写解析扩展。这也是“可复用”的关键点。

设计上我会在protocols/目录里给每个协议写一个解析类,统一实现parse(raw_bytes)方法,返回结构化dict。以Modbus RTU为例,它的报文结构是:地址(1字节) + 功能码(1字节) + 数据(N字节) + CRC(2字节,低字节在前)。

# protocols/modbus_rtu.py def parse_modbus_rtu(raw: bytes) -> dict: """解析Modbus RTU报文帧,校验CRC是否合法""" if len(raw) < 4: return {"valid": False, "reason": "frame too short"} addr, func = raw[0], raw[1] crc_received = int.from_bytes(raw[-2:], "little") # 接收到的CRC crc_calc = _crc16_modbus(raw[:-2]) return { "valid": crc_recalc == crc_received, "slave_addr": addr, "function": func, "data_len": len(raw) - 4, "data_hex": raw[2:-2].hex(), "crc_ok": crc_calc == crc_received, # 字段名和上面valid二选一 } def _crc16_modbus(data: bytes) -> int: crc = 0xFFFF for b in data: crc ^= b for _ in range(8): if crc & 1: crc = (crc >> 1) ^ 0xA001 else: crc >>= 1 return crc

注意我代码里写了两处valid和crc_ok,实际使用时统一用valid就好,避免二次判断。crc16_modbus是固定算法,0xA001是多项式,这是Modbus RTU的行业标准,不要自己发明。如果你解析CAN报文,常见做法是先取11位或29位ID,再拆数据域里的信号位;CAN和Modbus的区别是它没有地址和功能码,而是ID加8字节数据。

对于电网698协议,报文更长,而且有复杂TLV结构,建议按帧头、长度、控制域、数据域拆分,先不急着解业务字段,先把帧完整性验证了。CDT报文则是固定帧长,按报文类型决定字段偏移。这些非IP协议的共同点是:不要在解析函数里做业务判断,只输出“这个字段是什么”,把业务判断放到上层。这样你的工具换个项目还能用,这也是最容易被忽视的软件工程边界。

4. 避坑指南:报文解析里那些让新手一夜回到解放前的坑

4.1 链路层头部长度不一致导致卦错字段

现象:用Scapy解析时,IP层源目地址全对,但是TCP端口看起来像乱码。原因:pcap文件里有带VLAN tag的帧,Scapy会把Ethernet头解析成两层,而你手动pkt[IP]时Scapy自动跳过了VLAN,但如果你自己按tcpdump的偏移量读,就容易多算4字节。解决:不要自己算偏移,始终用pkt[IP]这类语义化访问;实在要手动切,先检查pkt.haslayer(VLAN),然后把偏移加4。另外Linux抓包常见的“any”设备头长度2字节,普通网卡头长度14字节,跨机器比对pcap时一定要看链路层类型。

4.2 Scapy对分片重组和TCP流切分不靠谱

现象:解析大流量pcap时,某些TCP负载只有一半,或者HTTP页面缺内容。原因:IP分片和TCP分段是两回事,Scapy默认不重组分片,也不做TCP流重组;它只把每个包当成独立网络层报文。解决:如果做文件还原或HTTP解析,先开启IP(defrag=True)或者直接用scapy.pipetool里的TCPReassembler。我在实际项目中用pkt[IP].defrag()配合pkt[TCP].payload做最简单重组,但这只对完整TCP会话有效,乱序流还得用tcpreorder。如果业务要求百分百准确,直接调用tshark的-z follow,tcp,ascii更省事。

4.3 时间戳精度和时区问题让可视化曲线变形

现象:画出来的每秒包数曲线和采集端日志对不上,峰谷错位。原因:Scapy源码里pkt.time是浮点数,但格式是unix epoch秒;不同抓包工具对微秒处理不一样,pcapng可能带有纳秒精度,而pcap老格式只有微秒。解决:统一用float(pkt.time),然后按int(time // bucket_sec)聚合;时区一律用UTC,不要用本地时区,否则凌晨有抓包结果的话曲线会歪。另外不要把pkt.time直接当整数去重,因为同一秒内多个包会重复;我见过有人把时间当主键,结果记录数少于实际包数,后面排查半天。

4.4 编码与字节序:大小端搞错全盘皆错

现象:解析CAN报文时,车速总是理不清,数值要么大得离谱要么是负数。原因:CAN信号传输中常见的Intel字节序(小端)和Motorola字节序(大端)混着用。Modbus RTU也是,寄存器值可以是高字节在前或低字节在前。解决:写工具时把字节序参数化,显式传给解析函数,比如int.from_bytes(field, byteorder="little")。并且单独写一个endian_utils.py,处理8/16/32/64位整型转换。我的血泪经验是:在字段名里直接加后缀_le、_be,一眼能看出哪个是哪个,调试起来不用再去翻协议文档。

4.5 大文件解析内存暴涨,可视化直接卡死

现象:读一个1.5GB的pcap,rdpcap直接把内存吃满,机器卡到swap,解析跑了10分钟还没完。原因:rdpcap是全量载入,把每个包都变成Scapy对象,1GB文件轻松吃4GB内存。解决:解析阶段用PcapReader迭代器(我在3.1里用了),已经避免全量载入;但如果我把记录全存进列表再返回,内存也会涨。正确做法是把记录分批写成CSV,或者用yield生成器。可视化阶段瓶颈在Matplotlib画几百万个点,此时要先用Counter聚合,结果只保留几百个桶,再画图就不会卡。另外建议优先用--max_pkts参数做小样本验证,确认逻辑正确后再跑全量。

5. 让工具更实用:实时捕获、协议识别与验证的进阶技巧

5.1 用pyshark做实时抓包并回灌进现有可视化

静态pcap分析只适合事后复盘,做监控预警就需要实时抓包。我常用pyshark的LiveCapture接口观察当前网卡流量,不需要root权限,但依赖tshark。加了这层之后,整个工具就从一个“解析器”变成“监控平台”了。

import pyshark def stream_capture(interface="eth0", duration=60): cap = pyshark.LiveCapture(interface=interface) for pkt in cap.sniff_continuously(packet_count=100): # 判断协议后直接落库,不做列表缓存 print(pkt.highest_layer, pkt.length)

interface传网卡名,Windows上可能叫“以太网”或\Device\NPF_{...}。packet_count限制数量,避免无限抓。实时抓包拿到的对象和pyshark的解析结果必须转成字符串类型才能和其他模块对接,否则内存里留着对象指针,每秒钟泄漏一堆。

5.2 用一条命令验证解析结果与Wireshark是否一致

解析工具最怕自己认为对、实际和Wireshark对不上。我的验证方法是:随机抽取100条记录,输出关键字段,然后和Wireshark的“显示列”对照。不用手动一个个看,我写了个快速比对脚本,读取CSV后直接打印不匹配的行。

# verify.py # 用法:python verify.py records.csv wireshark_export.csv import csv, sys def main(rec_csv, ws_csv): rec = {f"{r['index']}": r for r in csv.DictReader(open(rec_csv))} ws = {f"{r['index']}": r for r in csv.DictReader(open(ws_csv))} for idx in rec: if idx not in ws: print(f"missing in wireshark: {idx}") continue if rec[idx]["src_ip"] != ws[idx]["src_ip"]: print(f"mismatch {idx}: rec={rec[idx]['src_ip']} ws={ws[idx]['src_ip']}")

关键点:两端都导出“序号”,并且要保证包序号对应同一个包。Wireshark的“首包编号”默认从1开始,而Scapy的索引从0开始,所以需要对齐偏移。如果发现大量不匹配,先检查你的协议解析逻辑,而不是怀疑Wireshark。

5.3 日常维护中养成的三个验证习惯

第一,每次改协议解析函数之前,固定一组“黄金pcap”,这组文件里包含正常帧、异常帧、分片帧和边界帧,改完必须重新跑回归,保证历史结果不受影响。第二,遇到新增协议,先用Wireshark看三层报文结构,把偏移量和字段长度写进注释,再动手写解析。第三,所有时间值都以UTC存储,展示时再转本地时区,避免时区问题从源头污染数据。我早期图省事在解析时直接转本地时间,结果统计曲线在夏令时切换当天愣是多出来3600个点,查了整整一个下午才找到原因。从那以后,我所有工具内部一律UTC,只有画图最后一个环节才转字符串。这套习惯让我现在接手任何报文解析项目都很少返工,希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询