Python标准库-二进制读写自定义数据类型

  |  

摘要: Python 标准库 struct 模块,以二进制模式读写自定义数据类型

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


在 Python 中,用户是可以定义自己的数据类型的,但是用户定义的数据类型与文件IO关联的功能,Python 并没有直接提供。struct 模块提供了一种以二进制模式读写这些数据的能力。

struct 模块可以执行 Python 值和以 Python bytes 对象表示的 C 结构之间的转换。 这可以被用来处理存储在文件中或是从网络连接等其他来源获取的二进制数据。 它使用格式字符串作为 C 结构布局的精简描述以及与 Python 值的双向转换

参考书(中英文版):


1. bytes 和 bytes()

我们首先需要了解一下与二进制数据相关的 bytes 类型和 bytes() 函数的基础知识。

bytes 和 str

  • bytes 是 byte 的序列,str 是 unicode 的序列。
  • bytes 可以通过 decode 方法转换为 str 类型
  • str 可以通过 encode 转换为 bytes 类型
  • 当内存中的各种类型的数据,需要传输的时候,需要先编码成 bytes 再传输。接收后再正确地解码就可以了。
  • 字节数组 bytes_obj 相当于二进制 str b"str_obj"

内置函数 bytes()

1
class bytes([source[, encoding[, errors]]])

bytes() 返回 bytes 对象。

Return a new “bytes” object, which is an immutable sequence of integers in the range 0 <= x < 256. bytes is an immutable version of bytearray – it has the same non-mutating methods and the same indexing and slicing behavior.
Accordingly, constructor arguments are interpreted as for bytearray().


2. struct 模块

struct 模块中有模块级函数和 Struct 类两种方式处理结构化值。

(1) 模块级函数

模块定义了下列异常和函数:

  • exception struct.error: 会在多种场合下被引发的异常;其参数为一个描述错误信息的字符串。
  • struct.pack(format, v1, v2, ...): 返回一个 bytes 对象,其中包含根据格式字符串 format 打包的值 v1, v2, … 参数个数必须与格式字符串所要求的值完全匹配。
  • struct.pack_into(format, buffer, offset, v1, v2, ...): 根据格式字符串 format 打包 v1, v2, … 等值并将打包的字节串写入可写缓冲区 buffer 从 offset 开始的位置。注意 offset 是必需的参数。
  • struct.unpack(format, buffer): 根据格式字符串 format 从缓冲区 buffer 解包(假定是由 pack(format, ...) 打包)。结果为一个元组,即使其只包含一个条目。缓冲区的字节大小必须匹配格式所要求的大小,如 calcsize() 所示。
  • struct.unpack_from(format, /, buffer, offset=0): 对 buffer 从位置 offset 开始根据格式字符串 format 进行解包。结果为一个元组,即使其中只包含一个条目。缓冲区的字节大小从位置 offset 开始必须至少为 calcsize() 显示的格式所要求的大小。
  • struct.iter_unpack(format, buffer): 根据格式字符串 format 以迭代方式从缓冲区 buffer 解包。此函数返回一个迭代器,它将从缓冲区读取相同大小的块直至其内容全部耗尽。缓冲区的字节大小必须整数倍于格式所要求的大小,如 calcsize() 所示。每次迭代将产生一个如格式字符串所指定的元组。
  • struct.calcsize(format): 返回与格式字符串 format 相对应的结构的大小(亦即 pack(format, …) 所产生的字节串对象的大小)。

(2) Struct 类

一次性地创建 Struct 对象并调用其方法相比使用同样的格式调用 struct 函数更为高效,因为这样格式字符串只需被编译一次。

已编译的 Struct 对象支持以下方法和属性:

  • pack(v1, v2, ...): 等价于 pack() 函数,使用了已编译的格式。 (len(result) 将等于 size。)
  • pack_into(buffer, offset, v1, v2, ...): 等价于 pack_into() 函数,使用了已编译的格式。
  • unpack(buffer): 等价于 unpack() 函数,使用了已编译的格式。 缓冲区的字节大小必须等于 size。
  • unpack_from(buffer, offset=0): 等价于 unpack_from() 函数,使用了已编译的格式。 缓冲区的字节大小从位置 offset 开始必须至少为 size。
  • iter_unpack(buffer): 等价于 iter_unpack() 函数,使用了已编译的格式。 缓冲区的大小必须为 size 的整数倍。
  • format: 用于构造此 Struct 对象的格式字符串。
  • size: 计算出对应于 format 的结构大小(即 pack() 方法所产生的字节串对象的大小)。

(3) 格式字符串

  • 表示数据类型的字符(格式字符)
  • 计数
  • 字节顺序

字节顺序, 大小和对齐方式

默认情况下,C 类型以机器的本机格式和字节顺序表示,并在必要时通过跳过填充字节进行正确对齐。打包给定 C 结构的结果也会包含填充字节以使得所涉及的 C 类型保持正确的对齐;类似地,对齐在解包时也会被纳入考虑。 选择此种行为的目的是使得被打包结构的字节能与相应 C 结构在内存中的布局完全一致。

格式字符串的第一个字符可用于指示打包数据的字节顺序,大小和对齐方式

Code 字节顺序 大小 对齐方式
@ 按原字节 按原字节 按原字节
= 按原字节 标准
< 小端 标准
> 大端 标准
! 网络(大端) 标准
  • 本机大小和对齐方式是使用 C 编译器的 sizeof 表达式来确定的。 这总是会与本机字节顺序相绑定。
  • 请注意 ‘@’ 和 ‘=’ 之间的区别:两个都使用本机字节顺序,但后者的大小和对齐方式是标准化的。
  • 当使用非本机大小和对齐方式即 ‘<’, ‘>’, ‘=’, and ‘!’ 时不会添加任何填充

格式字符

“标准大小”列是指当使用标准大小时以字节表示的已打包值大小;也就是当格式字符串以 ‘<’, ‘>’, ‘!’ 或 ‘=’ 之一开头的情况。 当使用本机大小时,已打包值的大小取决于具体的平台。

具体的格式字符及其对应的 C 类型,对应的 Python 类型,标准大小可以参考 格式字符


3. 例子

(1) 结构化值与字符串的互相转换

Struct 对象的 pack() 方法:结构化值打包为字符串
Struct 对象的 unpack() 方法:字符串解包为结构化值

  • 打包
1
2
3
4
5
6
7
8
9
10
11
import struct
import binascii

values = (1, "ab".encode("utf-8"), 2.7)
s = struct.Struct("I 2s f") # 一个整数, 一个2字节字符串,一个浮点数
packed_data = s.pack(*values)

print('Original values:', values)
print('Format string :', s.format)
print('Uses:', s.size, 'bytes')
print('Packed Value :', binascii.hexlify(packed_data))

binascii.hexlify() 将打包后的数据转换为十六进制 bytes。

打印结果

1
2
3
4
Original values: (1, b'ab', 2.7)
Format string : b'I 2s f'
Uses: 12 bytes
Packed Value : b'0100000061620000cdcc2c40'
  • 解包
1
2
3
4
packed_data = binascii.unhexlify(b'0100000061620000cdcc2c40')
s = struct.Struct('I 2s f')
unpacked_data = s.unpack(packed_data)
print('Unpacked Values:', unpacked_data)

打印结果

1
Unpacked Values: (1, b'ab', 2.700000047683716)

(2) 预先分配 Buffers,在进行打包解包

pack_into()/unpack_from(): 直接与 pre-allocated buffers 交互。避免每个打包的结构化值都分配一次 buffer。

pre-allocated buffer 的创建方式: ctypes.create_string_buffer(s.size),其中 s 为 Struct 对象,其 size 方法告诉我们需要多大的 buffer

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 binascii
import array
import ctypes

s = struct.Struct('I 2s f')
values = (1, "ab".encode("utf-8"), 2.7)
print("original: {}".format(values))
print('---ctypes string buffer---')

# Struct 对象的 size 属性告诉我们需要多大的 buffer。
b = ctypes.create_string_buffer(s.size)
print('Before :', binascii.hexlify(b.raw))
s.pack_into(b, 0, *values)
print('After:', binascii.hexlify(b.raw))
print('Unpacked:', s.unpack_from(b, 0))

print("---array---")
a = array.array('b', b'\0' * s.size)
print('Before :', binascii.hexlify(a))
s.pack_into(a, 0, *values)
print('After:', binascii.hexlify(a))
print('Unpacked:', s.unpack_from(a, 0))

打印结果

1
2
3
4
5
6
7
8
9
original: (1, b'ab', 2.7)
---ctypes string buffer---
Before : b'000000000000000000000000'
After: b'0100000061620000cdcc2c40'
Unpacked: (1, b'ab', 2.700000047683716)
---array---
Before : b'000000000000000000000000'
After: b'0100000061620000cdcc2c40'
Unpacked: (1, b'ab', 2.700000047683716)

Share