前言:
背景:很多时候我们需要用python处理二进制数据。例如,存储文件、进行socket操作等。这个时候就需要用到struct模块。
struct用途:
(1)按照指定格式将Python数据转换为字符串(字节流)。如网络传输时不能直接传输int/long数据,此时要先将int/long转化为字节流,然后再发送;
(2)按照指定格式将字节流转换为Python指定的数据类型;
(3)处理二进制数据,如果用struct来处理文件的话,需要用’wb’,’rb’以二进制(字节流)写,读的方式来处理文件;
(4)处理C或者C++语言中的结构体;
接下来我们介绍struct模块中最重要的pack,unpack两个函数。
注:例如二进制流与数值的互相转换在编解码、开发小工具方面用途非常广泛。
二、struct模块
1、pack()函数
#按照给定的格式(format),把数据封装成字符串(实际上是类似于C结构体的字节流)
pack(format,v1,v2,...)
2、unpack()函数
#按照给定的格式(format),解析字节流string,返回解析出来的tuple
pack(format,string)
3、calcsize()函数
#计算给定的格式(format)占用多少字节的内存。
#注:icc表示三个成员数据类型是interger, char, char。
size = struct.calcsize(’@icc’) 结果是6。
size = struct.calcsize(’@cic’) 结果是9。
4、上述format支持的格式参见末尾表格。关于格式有如下几点说明:
(1)每个格式前面可以带一个数字,表示个数;
(2)s格式表示一定长度的字符串,4s表示长度为4的字符串;
(3)P用来转换一个指针,其长度和机器字长相关;
5、为了同C结构体交换数据还要考虑C或者C++编译器用到的字节对齐。字节序和对齐方式有如下5种:
Character | Byte order | Size | Alignment |
---|---|---|---|
@(默认) | 本机 | 本机 | 本机,凑够4字节 |
= | 本机 | 标准 | none,按原字节数 |
< | 小端 | 标准 | none,按原字节数 |
> | 大端 | 标准 | none,按原字节数 |
! | network(大端) | 标准 | none,按原字节数 |
(1)Byte order列中的native(本机)表示和当前系统的字节序一致;
(2)size列的native(本机)表示这个时候数据类型的大小和sizeof一致;如果是standard(标准)表示数据类型的长度是固定的;
(3)alignment是native(本机)的时候表示和当前系统的字节对齐方式一致。但是要注意的是python中的字节填充(padding)只会在连续的结构体成员之间添加,不会在结构体的开头和结尾添加。例如:
size = struct.calcsize(’@icc’) 结果是6。
size = struct.calcsize(’@cic’) 结果是9。
原因是系统字节对齐是4,第一个c和i之间添加了3个字节padding,但是最后一个c,后边没有添加padding,所以结果是9而不是12。
1)如何解决这个问题?当然有办法,在格式字符串的最后添加一个“0X”,“X”是一个数据类型,如“i”,代表的就是long,这时候就是指定数据类型,这时候就和C语言中的字节对齐完全一致。
size = size = struct.calcsize(’@cic0i’) 结果是12。
2)举个例子
在C代码中定义结构体:
Struct Person{
int fortune;
char height;
char weight;
};
假设从C代码中获取到的结构体数据是buf, 在python中使用struct模块按照上述的格式解析结构体中的数据。
python中使用struct.unpack(’@icc’, buf);解析结构体数据,这个时候就会报错。原因就是字节对齐导致两方的长度不一样。
size = struct.calcsize(’@icc’) 的结果是6,而sizeof(Person)的长度是8。这样就会导致解析错误。这是后需要指定格式对齐。
struct.unpack("@icc0i",buf)。这样就能正确解析C语言中的结构体。
(4)默认情况下使用字节序且对齐字符即表格中的”@“。
(5)对于涉及到socket收发的情况我们一般用”i“,即网络字节序。
三、使用举例
1、首先要明白不同类型数据的自然边界对齐值,如下:
数据类型 | 对齐值 |
char | 1 |
short | 2 |
int | 4 |
long | 4(8,64位) |
float | 4 |
double | 8 |
void * | 4(8,64位) |
注意:如果一个结构体全部由char组成,也就是说其对齐值为1;此时完全没有担心字节对齐引发问题的必要了。
2、可以随心所欲的在C/C++的结构体之前来回解析与转换(也就是说socket发送C结构体其实一样很好操作)
比如有如下一个结构体
struct Header
{
unsigned short id;
char[4] tag;
unsigned int version;
unsigned int count;
}
通过socket.recv接收到这个结构体buf的时候需要要把它解析出来,此时就可以使用unpack函数。
import struct
#'!'表示我们用网络字节序解析,因为我们是从网络上接收到这个buf的。
#'H'表示unsigned short的id;
#'4s'表示四个字节长的字符串;
#'2I'表示有两个unsigned int类型的数据;
id,tag,version,count = struct.unpack('!H4s2I',buf)
同样我们也可以很方便的把本地数据在pack成struct格式。
buf = struct.pack('!H4s2I',id,tag,version,count)
pack函数就是把四个参数按照指定的格式转换成了结构体Header,buf现在是一个字符串(实际上类似于C结构体的字节流),这个字节流是可以通过socket.send(buf)发送出去的。接受者按照上面unpack的方法就可以还原出原始的四个参数。
3、举例二:
a='hello'
b='world!'
c=2
d=45.123
bytes=struct.pack('5s6sif',a,b,c,d)
此时的bytes就是二进制形式的数据了,可以直接写入文件比如 binfile.write(bytes);
当我们需要时可以再读出来,bytes=binfile.read()
再通过struct.unpack()解码成python变量
a,b,c,d=struct.unpack('5s6sif',bytes)
'5s6sif'这个叫做fmt,就是格式化字符串,由数字加字符构成,5s表示占5个字符的字符串,2i,表示2个整数等等,下面是可用的字符及类型,ctype表示可以与python中的类型一一对应。
struct的format支持的格式:
Format | C Type | Python type | Standard size | Notes |
---|---|---|---|---|
x | pad byte | no value | ||
c | char | string of length 1 | 1 | |
b | signed char | integer | 1 | (3) |
B | unsigned char | integer | 1 | (3) |
? | _Bool | bool | 1 | (1) |
h | short | integer | 2 | (3) |
H | unsigned short | integer | 2 | (3) |
i | int | integer | 4 | (3) |
I | unsigned int | integer | 4 | (3) |
l | long | integer | 4 | (3) |
L | unsigned long | integer | 4 | (3) |
q | long long | integer | 8 | (2), (3) |
Q | unsigned long long | integer | 8 | (2), (3) |
f | float | float | 4 | (4) |
d | double | float | 8 | (4) |
s | char[] | string | 1 | |
p | char[] | string | ||
P | void * | integer | (5), (3) |