Python为了保持语言的简洁,仅仅为用户提供了几种简单的数据结构:intfloat, str, list, dict, tuple

不同于编译型语言,在Python中,我们一般不需要关心不同类型的变量在解释器内部的实现方式,例如:对于一个长整型数据,在Python中可以直接写成x=1123324345455234,而不用去考虑变量x占了几个字节,这种抽象的方式为程序的编写提供了足够的支持,但是在某些情况下(比如读取二进制文件,进行网络socket编程)的时候,我们需要一些其他的模块来实现我们关于变量长度控制的需求

struct模块

在Python中和二进制数据打交道的时候,需要使用到struct这个模块,struct模块为Python和C的混合编程,处理二进制文件以及进行网络协议交互提供了便利,理解这个模块主要需要理解三个函数:

1
# pack负责将不同的变量打包在一起,成为一个字节字符串,即类似于C语言中的字节流
2
struct.pack(fmt, v1, v2, ...)
3
# unpack将字节字符串解包成为变量
4
struct.unpack(fmt, string)
5
# 计算按照格式fmt打包的结果有多少个字节,这个打包格式fmt确定了将变量按照什么方式打包成字节流,
6
# 其包含了一系列的格式字符串 具体参考:https://docs.python.org/2/library/struct.html
7
struct.calcsize(fmt)
使用struct打包成定长结构

一般来说,在使用struct的时候,要打包的数据都是定长的,定长的数据代表你需要明确给出打包的或者解包的数据长度,否则打包解包函数将会出错

1
In [1]: import struct                           
2
In [2]: a = struct.pack("2I3sI", 12, 45, "abc", 23)                             
3
---------------------------------------------------------------------------
4
error                                     Traceback (most recent call last)
5
<ipython-input-2-baa4d09bbd35> in <module>
6
----> 1 a = struct.pack("2I3sI", 12, 45, "abc", 23)
7
error: argument for 's' must be a bytes object
8
# 注意:Python3 打包的时候需要将字符串转换成 bytes object
9
In [4]: a = struct.pack("2I3sI", 12, 45, b"abc", 23)                             
10
In [5]: b = struct.unpack("2I3sI", a)                                           
11
In [6]: b                                                                       out[6]: (12, 45, b'abc', 23)
12
        
13
In [8]: a = struct.pack("2I3sI", 12, 45, bytes("abc", encoding='utf-8'), 23)     
14
In [9]: b = struct.unpack("2I3sI", a)                                           
15
In [10]: b                                                                       
16
Out[10]: (12, 45, b'abc', 23)

上面的代码将俩个整数1245,一个字符串abc和一个整数23一起打包成一个字节流字符串,然后再解包。其中打包格式中明确指出了打包的长度:2I表明起始时俩个unsigned int3s表明长度为3的字符串,最后一个I表示最后紧跟一个unsigned int。所以上面的结果是:(12, 45, b'abc', 23)

我们可以调用calcsize来计算2I3sI模式占用的字节数

1
In [14]: print(struct.calcsize("2I3sI"))                                         
2
16

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

struct的打包过程中,根据特定类型的要求,必须进行字节对齐,由于默认unsigned int类型占用四个字节,因此要在字符串的位置进行4个字节对齐,因此即使是3个字符的字符串也要占用4个字节

不要字节对齐的模式:

1
In [18]: print(struct.calcsize("2Is"))                                           
2
9

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

需要指出的是,对于unpack而言,只要fmt对应的字节数和字节字符串string的字节数一致,就可以成功的进行解析,否则unpack函数将抛出异常,例如我们也可以使用如下的fmt解析出a:

1
In [19]: c = struct.unpack("2I2sI", a)                                           
2
In [20]: c                                                                       
3
Out[20]: (12, 45, b'ab', 23)
4
In [21]: print(struct.calcsize("2I2sI"))                                         
5
16

可以看到这里struct解析出了字符串的前两个字符,没有产生任何问题,同理打包的时候也是只打包指定长度的字符串

使用struct处理不定长结构

在使用packunpack的过程中,我们需要明确的指出打包模式中每个位置的长度,比如格式2I3sI就明确指出整型的个数和字符串的个数。

我们还可能需要处理变长的打包数据

  • 变长字符串的打包

    例如我们在程序中可能会得到一个字符串s,这个s没有一个固定的长度,所以我们每次打包的时候都需要将s的长度也打包到一起,这样我们才能进行正确的解包,这种情况在处理网络数据包中非常常见,在使用网络编程的时候,我们可能利用报文的第一个字段记录报文的长度,每次读取报文的时候,我们先读取报文的第一个字段,获取其长度之后再处理报文内容

    处理方式:

    1
    In [22]: x = "nihaoma"                                                       
    2
    In [23]: x = bytes(x, 'utf-8')                                               
    3
    In [24]: struct.pack("I%ds" %(len(x),),len(x), x)                           
    4
    Out[24]: b'\x07\x00\x00\x00nihaoma

    或者:

    1
    In [25]: struct.pack("I", len(x)) + bytes(x, 'utf-8')                       
    2
    Out[25]: b'\x07\x00\x00\x00nihaoma'

    第一种方式先将报文转变成字节码,然后获取字节码的长度,将长度嵌入到打包之后的报文中去,

    可以看到格式字符串中的I就用来记录报文的长度

    第二种方式是直接将字符串的长度打包成字节字符串,再跟原始字符串做一个连接操作

  • 变长字符串的解包

    根据上面的打包方式,可以轻松解包

    1
    In [2]: s = "hello world"
    2
    In [4]: s = bytes(s, 'utf-8')
    3
    In [5]: data = struct.pack("I%ds" % (len(s),), len(s), s)
    4
    In [6]: data                                                                 
    5
    Out[6]: b'\x0b\x00\x00\x00hello world'
    6
    In [7]: int_size = struct.calcsize("I")
    7
    In [8]: (i,), data = struct.unpack("I", data[:int_size]), data[int_size:]
    8
    In [9]: data                                                                 
    9
    Out[9]: b'hello world'

    由于报文的长度len(s)我们使用定长的整型I进行了打包,所以解包的时候我们可以先将报文长度获取出来,之后再根据报文长度读取报文内容