Python 内置模块之 struct

官方文档:https://docs.python.org/zh-cn/3/library/struct.html

0、前言

不同类型的语言支持不同的数据类型,比如 Go 有 int32、int64、uint32、uint64 等不同的数据类型,这些类型占用的字节大小不同,而同样的数据类型在其他语言中比如 Python 中,又是完全不同的处理方式,比如 Python 的 int 既可以是有符号的,也可以是无符号的,这样一来 Python 和 Go 在处理同样大小的数字时存储方式就有了差异。

除了语言之间的差别,不同的计算机硬件存储数据的方式也有很大的差异,有的 32 bit 是一个 word,有的 64 bit 是一个 word,而且他们存储数据的方式或多或少都有些差异。

当这些不同的语言以及不同的机器之间进行数据交换,比如通过 network 进行数据交换,他们需要对彼此发送和接受的字节流数据进行 pack 和 unpack 操作,以便数据可以正确的解析和存储。所以出现了大端和小端的方式。

0.1、大端与小端:计算机如何存储整型

可以把计算机的内存看做是一个很大的字节数组,一个字节包含 8 bit 信息可以表示 0-255 的无符号整型,以及 -128—127 的有符号整型。当存储一个大于 8 bit 的值到内存时,这个值常常会被切分成多个 8 bit 的 segment 存储在一个连续的内存空间,一个 segment 一个字节。

有些处理器会把高位存储在内存这个字节数组的头部,把低位存储在尾部,这种处理方式叫 大端,有些处理器则相反,低位存储在头部,高位存储在尾部,称之为 小端 。

一般情况下,主机字节序是小端模式,网络字节序是大端模式

假设一个寄存器想要存储 0x12345678 到内存中,大端 和 小端 分别存储到内存 1000 的地址表示如下

address 大端 小端
1000 0x12 0x78
1001 0x34 0x56
1002 0x56 0x34
1003 0x78 0x12

计算机如何存储 character

和存储 number 的方式类似,character 通过一定的编码格式进行编码比如 unicode,然后以字节的方式存储。

1、为什么需要 struct

python 的进制转化

1
2
3
4
5
6
7
dec = 18
print("十进制数为:", dec)
print("转换为二进制为:", bin(dec))
print("转换为八进制为:", oct(dec))
print("转换为十六进制为:", hex(dec))

print(int('0x10', 16)) # 16进制转10进制,结果16

Python没有专门把数据转化为大端或者小端的字节的方法。但由于b'str'可以表示字节,所以,字节数组=二进制str。而在C语言中,我们可以很方便地用struct、union来处理字节,以及字节和int,float的转换。

在Python中,比方说要把一个8位无符号整数变成字节,也就是4个长度的bytes,你得配合位运算符这么写:

1
2
3
4
5
6
7
n = 10240099
b1 = (n & 0xff000000) >> 24 # 0
b2 = (n & 0xff0000) >> 16 # 156
b3 = (n & 0xff00) >> 8 # 64
b4 = n & 0xff # 99
bs = bytes([b1, b2, b3, b4])
bs # b'\x00\x9c@c'

非常麻烦。如果换成浮点数就无能为力了。

好在Python提供了一个struct模块来解决bytes和其他二进制数据类型的转换。

structpack函数把任意数据类型变成bytes

1
2
3
>>> import struct
>>> struct.pack('>I', 10240099)
b'\x00\x9c@c'

pack的第一个参数是处理指令,'>I'的意思是:

>表示字节顺序是big-endian,也就是网络序,I表示4字节无符号整数。

后面的参数个数要和处理指令一致。

unpackbytes变成相应的数据类型:

1
struct.unpack('>IH', b'\xf0\xf0\xf0\xf0\x80\x80')  # (4042322160, 32896)

根据>IH的说明,后面的bytes依次变为I:4字节无符号整数和H:2字节无符号整数。

所以,尽管Python不适合编写底层操作字节流的代码,但在对性能要求不高的地方,利用struct就方便多了。

1.1、struct 用处

  1. 按照指定格式将Python数据转换为字符串,该字符串为字节流,如网络传输时,不能传输int,此时先将int转化为字节流,然后再发送;
  2. 按照指定格式将字节流转换为Python指定的数据类型;
  3. 处理二进制数据,如果用struct来处理文件的话,需要用’wb’,’rb’以二进制(字节流)写,读的方式来处理文件;
  4. 处理c语言中的结构体;

2、struct 模块

函数 return explain
pack(fmt,v1,v2…) string 按照给定的格式(fmt),把数据转换成字符串(字节流),并将该字符串返回.
pack_into(fmt,buffer,offset,v1,v2…) None 按照给定的格式(fmt),将数据转换成字符串(字节流),并将字节流写入以offset开始的buffer中.(buffer为可写的缓冲区,可用array模块)
unpack(fmt,v1,v2…..) tuple 按照给定的格式(fmt)解析字节流,并返回解析结果
pack_from(fmt,buffer,offset) tuple 按照给定的格式(fmt)解析以offset开始的缓冲区,并返回解析结果
calcsize(fmt) size of fmt 计算给定的格式(fmt)占用多少字节的内存,注意对齐方式

2.1、格式化字符串

当打包或者解包的时,需要按照特定的方式来打包或者解包.该方式就是格式化字符串,也就是参数fmt,它指定了数据类型,除此之外,还有用于控制字节顺序、大小和对齐方式的特殊字符。

格式化字符串包括 对齐方式和 格式符两部分

对齐方式

为了同c中的结构体交换数据,还要考虑c或c++编译器使用了字节对齐,通常是以4个字节为单位的32位系统,故而struct根据本地机器字节顺序转换.可以用格式中的第一个字符来改变对齐方式.定义如下

Character Byte order Size Alignment
@(默认) 本机 本机 本机,凑够4字节
= 本机 标准 none,按原字节数
< 小端 标准 none,按原字节数
> 大端 标准 none,按原字节数
! network(大端) 标准 none,按原字节数

格式符

格式符 C语言类型 Python类型 Standard size
x pad byte(填充字节) no value
c char string of length 1 1
b signed char integer 1
B unsigned char integer 1
? _Bool bool 1
h short integer 2
H unsigned short integer 2
i int integer 4
I(大写的i) unsigned int integer 4
l(小写的L) long integer 4
L unsigned long long 4
q long long long 8
Q unsigned long long long 8
f float float 4
d double float 8
s char[] string
p char[] string
P void * long

注:

  1. _Bool在C99中定义,如果没有这个类型,则将这个类型视为char,一个字节;
  2. qQ只适用于64位机器;
  3. 每个格式前可以有一个数字,表示这个类型的个数,如s格式表示一定长度的字符串,4s表示长度为4的字符串;4i表示四个int;
  4. P用来转换一个指针,其长度和计算机相关;
  5. f和d的长度和计算机相关;

3、示例

示例1

Pack 操作必须接受一个 template string 以及需要进行 pack 一组数据,这就意味着 pack 处理操作 定长 的数据

1
2
3
4
5
6
7
a = struct.pack("2I3sI", 12, 34, b"abc", 56)
b = struct.unpack("2I3sI", a)
print('a:',a)
print('b:',b)

# a: b'\x0c\x00\x00\x00"\x00\x00\x00abc\x008\x00\x00\x00'
# b: (12, 34, b'abc', 56)
  1. 输出中的“b”代表二进制。

  2. 上面的代码将两个整数 12 和 34,一个字符串 “abc” 和一个整数 56 一起打包成为一个字节字符流,然后再解包。其中打包格式中明确指出了打包的长度: “2I” 表明起始是两个 unsigned int , “3s” 表明长度为 4 的字符串,最后一个 “I” 表示最后紧跟一个 unsigned int ,所以上面的打印 b 输出结果是:(12, 34, ‘abc’, 56)

  3. struct将数字和使用的是哪种编码 – ASCII(“美国信息交换标准代码”)

示例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#一段MAC地址解析的实例

import struct

eth_header = b'E\x00\x00UC\x83@\x00>\x06\xd0\x03\n\x01'
eth_d = struct.unpack("!6s6sH",eth_header)
print(eth_d)

def eth_addr(a):
print('in eth_addr start==============')
print(a)
print(a[0], a[1], a[2], a[3], a[4], a[5])
bb = "%.2X:%.2X:%.2X:%.2X:%.2X:%.2X" % (a[0], a[1], a[2], a[3], a[4], a[5])
print(bb)
print('in eth_addr end==================\n')
return bb

print(eth_header[0:6])
print(eth_header[6:12])
print('Destination MAC : ' + eth_addr(eth_header[0:6]) + \
' Source MAC : ' + eth_addr(eth_header[6:12]) + ' Protocol : ' + str(eth_d[2]))

"""
(b'E\x00\x00UC\x83', b'@\x00>\x06\xd0\x03', 2561)
b'E\x00\x00UC\x83'
b'@\x00>\x06\xd0\x03'
in eth_addr start==============
b'E\x00\x00UC\x83'
69 0 0 85 67 131
45:00:00:55:43:83
in eth_addr end==================

in eth_addr start==============
b'@\x00>\x06\xd0\x03'
64 0 62 6 208 3
40:00:3E:06:D0:03
in eth_addr end==================

Destination MAC : 45:00:00:55:43:83 Source MAC : 40:00:3E:06:D0:03 Protocol : 2561
"""
  1. 编码后的第一个字符”E” ,单独打印时python直接显示69(ascii 中E对应的十进制是69);UC字符同理.

  2. \x为转移字符,其后面长度一个字节表示十六进制所代表的大小

示例3:标准库中整型与字节互转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
a = (52).to_bytes(1,byteorder='big')
print(a)

b = b'4'
b1 = int.from_bytes(b,byteorder='big')
print(b1)

c = b'E'
c1 = int.from_bytes(c,byteorder='big')
print(c1)

d = b'E4'
d1 = int.from_bytes(d,byteorder='big')
print(d1)

# b'4'
# 52
# 69
# 17716

示例4:计算字节大小

可以利用 calcsize 来计算模式 “2I3sI” 占用的字节数

1
print struct.calcsize("2I3sI") # 16

可以看到上面的三个整型加一个 3 字符的字符串一共占用了 16 个字节。为什么会是 16 个字节呢?不应该是 15 个字节吗?1 个 int 4 字节,3 个字符 3 字节。

但是在 struct 的打包过程中,根据特定类型的要求,必须进行字节对齐(关于字节对齐详见 https://en.wikipedia.org/wiki/Data_structure_alignment) 。

由于默认 unsigned int 型占用四个字节,因此要在字符串的位置进行4字节对齐,因此即使是 3 个字符的字符串也要占用 4 个字节。

再看一下不需要字节对齐的模式

1
print struct.calcsize("2Is") # 9

由于单字符出现在两个整型之后,不需要进行字节对齐,所以输出结果是 9。

不定长数据 pack

如果打包的数据长度未知该如何打包,这样的打包在网络传输中非常常见。处理这种不定长的内容的主要思路是把长度和内容一起打包,解包时首先解析内容的长度,然后再读取正文。

打包变长字符串

对于变长字符在处理的时候可以把字符的长度当成数据的内容一起打包。

1
2
s = bytes(s)
data = struct.pack("I%ds" % (len(s),), len(s), s)

上面代码把字符 s 的长度打包成内容,可以在进行内容读取的时候直接读取。

解包变长字符串

1
2
int_size = struct.calcsize("I")
(i,), data = struct.unpack("I", data[:int_size]), data[int_size:]

解包变长字符时首先解包内容的长度,在根据内容的长度解包数据

网络协议的打包与解包

pack_into(format, buffer, offset, v1, v2, ...)

unpack_from(format, buffer, offset=0)

它们在组包拆包时,可以指定所需的偏移量,这让组包拆包变得更加灵活。

假设,网络协议由消息id(unsigned short类型)、消息size(unsigned int类型)及可变长度的消息payload(若干个unsigned int类型)组成。如何进行打包与解包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import struct
import ctypes

def load_packet(msg_id, msg_size, msg_payload):
packet = ctypes.create_string_buffer(msg_size)
struct.pack_into('>HI', packet, 0, msg_id, msg_size)
struct.pack_into('>%dH' % (int(msg_size-6)/2), packet, 6, *msg_payload)
return packet

def unload_packet(packet):
msg_id, msg_size = struct.unpack_from('>HI', packet, 0)
msg_payload = struct.unpack_from('>%dH' % (int(msg_size-6)/2), packet, 6)
return msg_id, msg_size, msg_payload

if __name__ == '__main__':
packet = load_packet(0x1002, 12, (0x1003, 0x1004, 0x1005))
print("packet: %s" % packet.raw)
msg_id, msg_size, msg_payload = unload_packet(packet)
print(hex(msg_id), msg_size, [hex(item) for item in msg_payload])


# packet: b'\x10\x02\x00\x00\x00\x0c\x10\x03\x10\x04\x10\x05'
# 0x1002 12 ['0x1003', '0x1004', '0x1005']

struct 解析位图文件

Windows的位图文件(.bmp)是一种非常简单的文件格式,我们来用struct分析一下。

首先找一个bmp文件,没有的话用“画图”画一个。

读入前30个字节来分析:

1
s = b'\x42\x4d\x38\x8c\x0a\x00\x00\x00\x00\x00\x36\x00\x00\x00\x28\x00\x00\x00\x80\x02\x00\x00\x68\x01\x00\x00\x01\x00\x18\x00'

BMP格式采用小端方式存储数据,文件头的结构按顺序如下:

两个字节:'BM'表示Windows位图,'BA'表示OS/2位图;一个4字节整数:表示位图大小;一个4字节整数:保留位,始终为0;一个4字节整数:实际图像的偏移量;一个4字节整数:Header的字节数;一个4字节整数:图像宽度;一个4字节整数:图像高度;一个2字节整数:始终为1;一个2字节整数:颜色数。

所以,组合起来用unpack读取:

1
2
>>> struct.unpack('<ccIIIIIIHH', s)
(b'B', b'M', 691256, 0, 54, 40, 640, 360, 1, 24)

结果显示,b'B'b'M'说明是Windows位图,位图大小为640x360,颜色数为24。

请编写一个bmpinfo.py,可以检查任意文件是否是位图文件,如果是,打印出图片大小和颜色数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# -*- coding: utf-8 -*-

import base64,struct

bmp_data = base64.b64decode('Qk1oAgAAAAAAADYAAAAoAAAAHAAAAAoAAAABABAAAAAAADICAAASCwAAEgsAAAAAAAAAAAAA/3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9/AHwAfAB8AHwAfAB8AHwAfP9//3//fwB8AHwAfAB8/3//f/9/AHwAfAB8AHz/f/9//3//f/9//38AfAB8AHwAfAB8AHwAfAB8AHz/f/9//38AfAB8/3//f/9//3//fwB8AHz/f/9//3//f/9//3//f/9/AHwAfP9//3//f/9/AHwAfP9//3//fwB8AHz/f/9//3//f/9/AHwAfP9//3//f/9//3//f/9//38AfAB8AHwAfAB8AHwAfP9//3//f/9/AHwAfP9//3//f/9//38AfAB8/3//f/9//3//f/9//3//fwB8AHwAfAB8AHwAfAB8/3//f/9//38AfAB8/3//f/9//3//fwB8AHz/f/9//3//f/9//3//f/9/AHwAfP9//3//f/9/AHwAfP9//3//fwB8AHz/f/9/AHz/f/9/AHwAfP9//38AfP9//3//f/9/AHwAfAB8AHwAfAB8AHwAfAB8/3//f/9/AHwAfP9//38AfAB8AHwAfAB8AHwAfAB8/3//f/9//38AfAB8AHwAfAB8AHwAfAB8/3//f/9/AHwAfAB8AHz/fwB8AHwAfAB8AHwAfAB8AHz/f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//3//f/9//38AAA==')


def bmp_info(data):
str = struct.unpack('<ccIIIIIIHH',data[:30]) #bytes类也有切片方法
if str[0]==b'B' and str[1]==b'M':
print("这是位图文件")
return {
'width': str[-4],
'height': str[-3],
'color': str[-1]
}
else:
print("这不是位图文件")


if __name__ == '__main__':
bmp_info(bmp_data)
print('ok')

Python 内置模块之 struct
https://flepeng.github.io/021-Python-31-Python-标准库-Python-标准库之-struct-将字节串解读为打包的二进制数据/
作者
Lepeng
发布于
2021年3月17日
许可协议