### 1. 他是什么如果你经常和底层数据打交道比如读取二进制文件、解析网络协议或者干脆想用Python模拟一下C语言的结构体那struct模块就是为此而生。简单说它是个翻译官负责在Python的「对象」和底层字节序列之间来回倒腾。举个例子想象你有一张纸质表格上面有姓名、年龄、身高。在Python里你会用一个字典或类来装这些数据。但机器不认表格它只认0和1组成的长串。struct就是那个把表格翻译成二进制长串或者反过来把长串解读成表格的工具。它定义了一套规则——用格式化字符串描述数据长什么样子第一个字段是整数、第二个是浮点数、第三个是字符串然后按这个规则打包成字节流或者从字节流里拆出来。我个人的理解它就像个古老的电报机。你输入“姓名年龄身高”它按固定格式编码成滋滋响的电波发出去对方收到电波再按同一张密码本解码成文字。这过程中没多余的解释也没自动补全一切都得事先约定好。2. 他能做什么最常见的场景是处理二进制文件比如BMP、WAV、PNG。这些文件的头部都有固定长度的数据包含宽高、采样率、文件大小等信息。用struct可以精准地读出这些字段而不必自己算偏移量或者手动转换字节序。网络协议解析也是它的主场。TCP/UDP包、自定义的RPC协议只要是按字节排列的用struct解剖起来就特别顺手。程序员之间交换数据只要约定好格式字符串两端不管用什么语言都能正确解析。另外做嵌入式开发、写驱动、甚至用Python写一个简单的序列化工具都会用到它。我见过有人用它来模拟C语言的union做类型双关type punning比如把一个float的字节当成int来解读以此研究浮点数的存储结构。3. 怎么使用核心就两个函数pack打包和unpack解包。但它们有个共同的关键——格式化字符串也就是那个描述数据长什么样的密码本。最简单的例子打包一个整数和一个浮点数。importstruct# 格式字符串 if 表示i代表int4字节f代表float4字节datastruct.pack(if,42,3.14)print(data)# 输出字节序列看起来像是乱码pack返回一个bytes对象里面就是按序排列的两个数字。要取出来用unpacka,bstruct.unpack(if,data)print(a,b)# 42 3.14格式化字符串里可以指定字节序本机、小端、大端、!网络字节序。默认是它会用当前系统的字节序和对齐方式。如果你正在写一个跨平台解析工具强烈建议显式指定否则换台机器就可能出错。还有一组方法pack_into和unpack_from它们操作的是缓冲区对象比如bytearray适合需要多次读写同一个缓冲区的场景。比如你要往某个网络缓冲区里连续填充多个协议字段bufbytearray(1024)struct.pack_into(!i,buf,0,12345)# 在偏移0处写入整数struct.pack_into(!h,buf,4,6789)# 在偏移4处写入短整数读取时类似val1struct.unpack_from(!i,buf,0)[0]val2struct.unpack_from(!h,buf,4)[0]值得一提的是版本3.4之后struct支持了iter_unpack专门用于处理一个字节流里包含多个相同结构的记录比如读取一个包含100个三维坐标点的文件# 假设文件里是一串连续的三维浮点坐标x,y,z每个坐标是三个floatimportstructwithopen(points.bin,rb)asf:rawf.read()forx,y,zinstruct.iter_unpack(!fff,raw):print(x,y,z)这比手动分批切成块、再逐个unpack要优雅得多。4. 最佳实践第一格式化字符串一定要写对。少写一个字符或者顺序搞反结果就是拿到的值全错而且很难排查。我一般会在代码里用常量定义好每个字段的名称和类型比如HEADER_FORMAT!iif# 第一个int第二个int第三个floatHEADER_FIELDS[width,height,fps]这样解包时直接unpack(HEADER_FORMAT, data)然后zip(HEADER_FIELDS, values)转成字典。第二尽量显式指定字节序。不要用默认的。跨平台开发的小端大端是个坑很多bug都源于文件是用大端写的但代码里忘了指定结果在x86机器上按小端解析。一种稳妥的做法是读文件时看到\x89PNG这种固定魔数它本身就是大端序的标志那就统一用!或如果是二进制协议文档里写明的是小端就用。第三关于对齐。C语言的struct为了效率会填充字节对齐比如int要4字节对齐Python的struct默认也做对齐模式。如果你要读一个C程序生成的二进制文件要特别注意这一点——C里可能因为对齐而在字段之间插入无用字节。解决方法是要么在格式字符串里手动加填充字节x就是跳过一个字节要么直接用标准大小模式、、!它们不会添加填充。第四对于大型或复杂的二进制结构写一个类来封装解析逻辑。classBMPHeader:FORMATIHHIIiiHHII# 根据BMP规范严格定义def__init__(self,data):fieldsstruct.unpack(self.FORMAT,data[:struct.calcsize(self.FORMAT)])self.typefields[0]# BM的ASCII值self.sizefields[1]self.widthfields[6]self.heightfields[7]这样调用方只需header BMPHeader(raw)不必关心内部如何组织字节。最后尽量使用bytes或bytearray而不是str来传参。Python3里的str是Unicode直接传进去会导致TypeError很多新手在这里翻车。5. 和同类技术对比Python生态里除了struct还有几个处理二进制数据的工具但各有偏重。bytearray 手动索引切片这是最原始的方式。比如读取一个网络包手动拆出前4字节当作int后6字节当作MAC地址。优点是灵活不需要学格式化字符串缺点是代码又臭又长容易算错偏移量可读性差。struct相当于帮你把“取出前4字节并解释为小端整数”这些步骤缩写成了i。pickle它是Python特有的对象序列化工具可以存任意Python对象。但问题是它产生的是内部格式非Python语言基本读不了而且有严重的安全隐患反序列化时可以执行任意代码。struct则是语言无关的只处理基础数据类型适合跨语言数据交换。array模块它可以存储同类型的数值比如array(i, [1,2,3])然后直接写入文件、网络。这比用struct循环打包每个数要快但只适用于一维数值数组。如果结构比较复杂混合了整数、浮点、字符串就无能为力了。ctypes它允许在Python里直接定义C结构体然后从内存或二进制数据中映射读取。比如fromctypesimport*classPoint(Structure):_fields_[(x,c_int),(y,c_int)]pPoint.from_buffer_copy(raw_bytes)这比struct更接近C世界的思维适合用于与C动态库交互的场景。但它比struct重得多定义字段时还要声明C类型而且跨平台时也要注意对齐。dataclasses 手动转换有人喜欢用数据类定义字段然后写个方法调用struct。这样结构更清晰但本质上还是struct在干活。protobuf / msgpack / avro这类是高级序列化框架有IDL定义、类型自动推断、版本兼容性支持甚至能产生多种语言的代码。它们的确能替代struct在序列化领域的很多工作但也引入了依赖和复杂性。对于简单的、字段固定的头结构用struct更轻量、更直接。个人感受越是底层、越追求解析速度和确定性越离不开struct而需要跨版本兼容、跨语言协作的大型项目更适合用protobuf这类工具。说到底struct有点像瑞士军刀里的小螺丝刀——功能单一但关键时刻总能搞定一些别的工具搞不定的任务。当你需要精确控制每个字节的排列时它就是最好的选择。