暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

用Python读写二进制文件

语和言 2021-05-09
1092

一、缘起


写了一个讲义,发现内容太多了,于是把这部分内容从讲义中移出,发到这里。本文所有代码都经过运行验证。



二、环境


Win 10 中文专业版64位 + Python 3.65 64位 + 8G RAM



三、正文


Python中提供了struct标准库和其它标准库来读写二进制文件,本文主要介绍struct标准,顺带介绍其它标准库。

(一) struct标准库介绍


Python中读写二进制文件,open()函数的mode参数一定要包含"b"这个值,文件对象read()方法读取文件得到的是字节串,write()方法也要写入字节串。为此,我们需要在在字节串和其它类型的数据之间进行转换,struct标准库有两个主要的方法:packunpack,前者把其他数据类型转换为字节串类型,后者把字节串类型转换为其他数据类型。这两个函数的使用格式如下:

pack("格式串", 一到多个其它类型的表达式)

unpack("格式串", 字节串类型的对象)

格式串中至少包含一个格式符,格式符前面还有一个可选的字节顺序标识符。

部分格式符如表1所示。


1 struct标准库的packunpack方法的格式符


格式符

参数类型

在字节串中所占字节数

x

无效类型,转换字节串时对应的一个字节无效

1

?

布尔类型

1

bB

字符、无符号字符;值小的整数

1

hH

短整数、无符号短整数

2

iI

整数、无符号整数

4

lL

长整数、无符号长整数

4

f

float类型的浮点数

4

d

Double类型的浮点数

8

s

字符串,它的每个字符占1字节

1

 

对于整数和浮点数来说,格式化之后如果数据填补不满预定的字节数,会用0值字节b'\x00'来填补高位字节。
整数和浮点数格式化之后如果是多个字节构成的字节串,这多个字节还存在一个字节序问题,也就是字节串中数据的高位字节在前还是低位字节在前的问题,换句话说,是自然顺序(高位在前低位在后)还是逆序。

这里的字节序可以用字节序标识符来规定,常用的字节序标识符有><这两个:

>表示自然顺序,高位在前低位在后;

<表示逆序,字节顺序跟自然顺序完全相反。


如果不写字节序标识符,则默认的字节序不确定,因电脑而异。

下面分别举例介绍struct标准库的packunpack这两个方法的使用。


(二)struct.pack将其它数据类型转为字节串


我们常用的数据类型有布尔、整数、浮点数和字符串,现在看一下将这些数据类型转换为字节串类型的方法。


1  布尔值转为字节串


布尔值转为字节串,用半角问号格式符。

 

>>> from struct import pack

>>> pack("?", False)

b'\x00'

>>> pack("?", True)

b'\x01'


2  整数转为字节串


整数转为字节串,可选的格式符有很多,都可以,可以根据整数的大小来选择四个小写字母字符bhil之一。如果是对无符号整数进行转换,则需要用对应的大写格式符,可以从BHIL当中选择一个。

 

>>> from struct import pack

>>> pack("b", 25)

b'\x19'

>>> pack("h", 258)

b'\x02\x01'

>>> pack("i", 258)

b'\x02\x01\x00\x00'

>>> pack(">i",258)

b'\x00\x00\x01\x02'

 

需要注意格式符对应的字节数要能容纳被转换的整数,否则会出错。

 

>>> from struct import pack

>>> pack("b", 258)

Traceback (most recent call last):

  File"<pyshell#77>", line 1, in <module>

   pack("b", 258)

struct.error: byte format requires -128 <= number<= 127

 

另外,转换的时候,格式串中可以写多个格式符,相应地,在格式串后面需要提供数量相等的多个待格式化的表达式。

 

>>> from struct import pack

>>> pack(">bhi", 1, 2, 3)

b'\x01\x00\x02\x00\x00\x00\x03'

 

如果格式串中有连续多个相同的格式符,可以改写成单个格式符前面加上数字的形式,例如:

 

>>> from struct import pack

>>> pack(">hhh", 1, 2, 3)

b'\x00\x01\x00\x02\x00\x03'

>>> pack(">3h", 1, 2, 3)

b'\x00\x01\x00\x02\x00\x03'


3  浮点数转为字节串


整数转为字节串,可以根据浮点数的大小从两个小写字母字符fd中选择一个。

 

>>> from struct import pack

>>> pack("f", 3.14159)

b'\xd0\x0fI@'

>>> pack("f", -3.14159)

b'\xd0\x0fI\xc0'

>>> pack(">f", 3.14159)

b'@I\x0f\xd0'

 

4  字符串转为字节串


将字符串转换为字节串,我们可以用str.encode()函数来处理。

 

>>> name = "胡凤国"

>>> name.encode("gb18030")

b'\xba\xfa\xb7\xef\xb9\xfa'

 

直接用str.encode()函数转为字节串存在一个缺点,不能指定转换后的字节串所占的字节数。为此,可以用struct.pack()函数来转换。
struct.pack()函数转换字符串时,需要在格式符前面写一个整数来表示格式化后的字节串所占的位数。如果不指定,则只能转换字符串中的第一个字符;如果指定的字节数多于字符串变字节串所需要的字节数,则在后面用0值字节b'\x00'来补足字节数;如果指定的字节数少于转换所需字节数,则会将结果截断。

 

>>> from struct import pack

>>> name = "胡凤国"

>>> pack("10s",name.encode("gb18030"))

b'\xba\xfa\xb7\xef\xb9\xfa\x00\x00\x00\x00'

 

现在举一个将数字和字符串一起转换为字节串的例子。

 

>>> from struct import pack

>>> name, age = "张三", 23

>>> r = pack(">10si",name.encode("gb18030"), age)

>>> r

b'\xd5\xc5\xc8\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17'


(三) struct.unpack将字节串转为其它数据类型


unpack是pack的逆操作,转换的结果都是元组,元组的各个元素是我们所希望的到的数据。同一个字节串,可以转换为不同的数据类型,结果自然大不一样。一般情况下,我们在执行unpack操作的时候,最好知道字节串是怎么pack得到的。


1  字节串转为布尔值


>>> from struct import unpack

>>> unpack("?", b'\x00')

(False,)

>>> unpack("?", b'\x01')

(True,)

>>> unpack("??", b'\x01\x05')

(True, True)


2  字节串转为整数


>>> from struct import unpack

>>> b = b'\xba\xfa'

>>> unpack("bb", b)          # 转换成两个单字节整数

(-70, -6)

>>> unpack("BB", b)          # 转换成两个单字节无符号整数

(186, 250)

>>> unpack("h", b)           # 以默认字节序转换成一个双字节整数

(-1350,)

>>> unpack("H", b)           # 以默认字节序转换成一个双字节无符号整数

(64186,)

>>> unpack(">h", b)          # 以自然顺序转换成一个双字节整数

(-17670,)


3  字节串转为浮点数


>>> from struct import pack, unpack

>>> a = 3.14

>>> b = pack("f", a)

>>> b

b'\xc3\xf5H@'

>>> c = unpack("f", b)

>>> c

(3.140000104904175,)


4  字节串转为字符串


字节串转为字符串,并不需要struct.unpack,只需要字节串自身的解码方法decode(),不过需要提供把字符串编码为字节串时采用的编码方案。

 

>>> name = "胡凤国"

>>> bname =name.encode("gb18030")

>>> bname

b'\xba\xfa\xb7\xef\xb9\xfa'

>>> bname.decode("gb18030")

'胡凤国'

 

当字节串中包含多个数据的时候,struct.unpack可以很方便地将字节串拆分成原始数据对应的多个字节串。

 

>>> from struct import unpack

>>> b = b'\xd5\xc5\xc8\xfd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17'

>>> bname, age =unpack(">4s6xi", b)

>>> bname

b'\xd5\xc5\xc8\xfd'

>>> age

23

>>> bname.decode("gb18030")

'张三'

 

这个例子中的字节串b是第10.6.2.4节最后一个例子得到的结果。在这个字节串中,前4个字节是姓名,中间的6个字节是0值字节,没什么用,最后4个字节是一个整数,表示年龄。
这里解释一下unpack()使用的格式串">4s6xi"

Ø >表示其中的格式符i按自然顺序去解读对应的4个字节;

Ø 4s表示前4个字节是一个数据;

Ø 6x表示接下来的6个字节放弃,不予解读;

Ø i表示接下来的4个字节解读成整数。

这样一共解读出来两个数据,表示姓名的字节串和表示年龄的整数,姓名字节串再进一步解码得到字符串形式的姓名。


(四)struct标准库读写二进制文件示例


现在有一个列表a,存储了四名同学的姓名和年龄:

a = [(李逍遥,19), (赵灵儿,16), (林月如,18), (阿奴,14)]

现要把这些数据写入一个二进制文件。由于各个人的姓名长短不一,不方便未来从二进制文件读出,所以规定姓名用gb18030编码转换成长度为6个字节的字节串,年龄用单字节存储。写入程序如下:


【例1】 struct标准库将数据写入二进制文件示例


 

下面我们将读取这个二进制文件,把各个同学的姓名和年龄显示在屏幕上。当然,读取之前我们必须要知道如下信息:

这个二进制文件中每7个字节表示一个人的信息,这7个字节的前6个字节表示姓名,需要用gb18030来解码,最后一个字节表示年龄。


【例2】 struct标准库从二进制文件读取数据示例



有了前面的基础知识和具体实例做铺垫,这里读写二进制文件的两个例子应该不难理解。需要强调的是,文本文件的读写可以以行为单位进行,各行的长度可以不同,但二进制文件的读写最好以固定长度的字节串为单位进行,这样方便读取。如果不写成固定长度的字节串,可能就得设置一个索引文件,具体指示某某数据存储在二进制文件的第几个字节到第几个字节,这样就得额外维护一个索引文件。


(五) 用其它标准库读写二进制文件


关于二进制文件的读写,前面介绍了struct标准库。Python还提供了picklemarshalshelve等标准库,也能完成二进制文件的读写。


1  pickle标准库读写二进制文件


pickle标准库读写二进制文件,常用的方法主要有dump()load()这两个方法。这里用pickle标准库实现【例219】和【例220】的功能。


【例3】 pickle标准库将数据写入二进制文件示例


 

我们把需要写入二进制文件的全部数据放入列表,然后通过pickle标准库的dump()方法进行写入操作:先把列表长度写入,再把列表的每个元素顺次写入。在写入过程中,我们不需要关心数据是怎么转换为字节串的,pickle会在写入的时候自动进行转化。注意用pickle创建的二进制文件只能用pickle来读取。


【例4】 pickle标准库从二进制文件读取数据示例


 

读取用pickle创建的二进制文件是通过pickle标准库的load()方法进行读取的:创建文件对象之后,先读取数据个数,再循环读取每条数据。读取的数据可以像那样直接处理,也可以放入一个列表,留待以后统一处理。

2  marshal标准库读写二进制文件

Python还有一个标准库marshal能实现跟pickle一样的功能,它也有dump()load()这两个方法,甚至这两个方法的使用格式都跟pickle一模一样。把【例221】和【例222】代码当中的字符串"pickle"替换为"marshal",其它不用任何改动,程序就能顺利读写二进制文件。当然,marshalpickle创建的二进制文件的格式与大小都不一样。


3  shelve标准库读写二进制文件


Python的标准库shelve可以像读写字典一样来读写二进制文件,这里不详细展开,只举一个简单的例子。


【例5】 shelve标准库读写二进制文件


 

我们在程序中读写的二进制文件名叫pal3.bin,但运行程序创建文件之后会生成三个文件:pal3.bin.dat、pal3.bin.bak、pal3.bin.dir,shelve会自动管理这三个文件。我们只需要利用shelve.open打开文件得到文件对象,然后把文件对象当成字典去存取数据就行。
另外,关于shelve标准库有一点需要注意,如果shelve标准库打开的文件已经存在,但不是shelve创建的,程序运行时会报错。



四、后记


如果需要跟作者交流,请扫码入群。



文章转载自语和言,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论