《Python Cookbook(第3版)中文版》——6.11 读写二进制结构的数组

简介:

本节书摘来自异步社区《Python Cookbook(第3版)中文版》一书中的第6章,第6.11节,作者[美]David Beazley , Brian K.Jones,陈舸 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。

6.11 读写二进制结构的数组

6.11.1 问题

我们想将数据编码为统一结构的二进制数组,然后将这些数据读写到Python元组中去。

6.11.2 解决方案

要同二进制数据打交道的话,我们可以使用struct模块。下面的示例将一列Python元组写入到一个二进制文件中,通过struct模块将每个元组编码为一个结构。

from struct import Struct
def write_records(records, format, f):
    '''
    Write a sequence of tuples to a binary file of structures.
    '''
    record_struct = Struct(format)
    for r in records:
        f.write(record_struct.pack(*r))

# Example
if __name__ == '__main__':
    records = [ (1, 2.3, 4.5),
                (6, 7.8, 9.0),
                (12, 13.4, 56.7) ]

with open('data.b', 'wb') as f:
     write_records(records, '<idd', f)

如果要将这个文件重新读取为一列Python元组的话,有好几种方法可以实现。首先,如果打算按块以增量式的方式读取文件的话,可以按照下面的示例来实现:

from struct import Struct
def read_records(format, f):
    record_struct = Struct(format)
    chunks = iter(lambda: f.read(record_struct.size), b'')
    return (record_struct.unpack(chunk) for chunk in chunks)

# Example
if __name__ == '__main__':
    with open('data.b','rb') as f:
        for rec in read_records('<idd', f):
            # Process rec
...

如果只想用一个read()调用将文件全部读取到一个字节串中,然后再一块一块的做转换,那么可以编写如下的代码:

from struct import Struct
def unpack_records(format, data):
    record_struct = Struct(format)
    return (record_struct.unpack_from(data, offset)
            for offset in range(0, len(data), record_struct.size))
# Example
if __name__ == '__main__':
    with open('data.b', 'rb') as f:
        data = f.read()
    for rec in unpack_records('<idd', data):
        # Process rec
        ...

在这两种情况下得到的结果都是一个可迭代对象,它能够产生出之前保存在文件中的那些元组。

6.11.3 讨论

对于那些必须对二进制数据编码和解码的程序,我们常会用到struct模块。要定义一个新的结构,只要简单地创建一个Struct实例即可:

# Little endian 32-bit integer, two double precision floats
record_struct = Struct('<idd')

结构总是通过一组结构化代码来定义,比如i、d、f等(参见Python的文档http://docs.python.org/3/library/struct.html )。这些代码同特定的二进制数据相对应,比如32位整数、64位浮点数、32位浮点数等。而第一个字符<指定了字节序。在这个例子中表示为“小端序”。将字符修改为>就表示为大端序,或者用!来表示网络字节序。

得到的Struct实例有着多种属性和方法,它们可用来操纵那种类型的结构。size属性包含了以字节为单位的结构体大小,这对于I/O操作来说是很有用的。pack()和unpack()方法是用来打包和解包数据的。示例如下:

>>> from struct import Struct
>>> record_struct = Struct('<idd')
>>> record_struct.size
20
>>> record_struct.pack(1, 2.0, 3.0)
b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@'
>>> record_struct.unpack(_)
(1, 2.0, 3.0)
>>>

有时候我们会发现pack()和unpack()会以模块级函数(module-level functions)的形式调用,就像下面的示例这样:

>>> import struct
>>> struct.pack('<idd', 1, 2.0, 3.0)
b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@'
>>> struct.unpack('<idd', _)
(1, 2.0, 3.0)
>>>

这么做行的通,但是比起创建一个单独的Struct实例来说还是显得不那么优雅,尤其是如果相同的结构会在代码中多处出现时。通过创建一个Struct实例,我们只用指定一次格式化代码,所有有用的操作都被漂亮地归组到了一起(通过实例方法来调用)。如果需要同结构打交道的话,这么做肯定会使得代码更容易维护(因为只需要修改一处即可)。

用来读取二进制结构的代码中涉及一些有趣而且优雅的编程惯用法(programming idioms)。在函数read_records()中,我们用iter()来创建一个迭代器,使其返回固定大小的数据块(参见5.8节)。这个迭代器会重复调用一个用户提供的可调用对象(即,lambda: f.read(record_struct.size))直到它返回一个指定值为止(即,b''),此时迭代过程结束。示例如下:

>>> f = open('data.b', 'rb')
>>> chunks = iter(lambda: f.read(20), b'')
>>> chunks
<callable_iterator object at 0x10069e6d0>
>>> for chk in chunks:
...      print(chk)
...
b'\x01\x00\x00\x00ffffff\x02@\x00\x00\x00\x00\x00\x00\x12@'
b'\x06\x00\x00\x00333333\x1f@\x00\x00\x00\x00\x00\x00"@'
b'\x0c\x00\x00\x00\xcd\xcc\xcc\xcc\xcc\xcc*@\x9a\x99\x99\x99\x99YL@'
>>>

创建一个可迭代对象的原因之一在于这么做允许我们通过一个生成器表达式来创建records记录,就像解决方案中展示的那样。如果不采用这种方式,那么代码看起来就会像这样:

def read_records(format, f):
    record_struct = Struct(format)
    while True:
        chk = f.read(record_struct.size)
        if chk == b'':
            break
        yield record_struct.unpack(chk)
    return records

在函数unpack_records()中我们采用了另一种方法。这里使用的unpack_from()方法对于从大型的二进制数组中提取出二进制数据是非常有用的,因为它不会创建任何临时对象或者执行内存拷贝动作。我们只需提供一个字节串(或者任意的数组),再加上一个字节偏移量,它就能直接从那个位置上将字段解包出来。

如果用的是unpack()而不是unpack_from(),那么需要修改代码,创建许多小的切片对象并且还要计算偏移量。示例如下:

def unpack_records(format, data):
    record_struct = Struct(format)
    return (record_struct.unpack(data[offset:offset + record_struct.size])
            for offset in range(0, len(data), record_struct.size))

这个版本的实现除了读取数据变得更加复杂之外,还需要完成许多工作,因为它得计算很多偏移量,拷贝数据,创建小的切片对象。如果打算从已读取的大型字节串中解包出许多结构的话,那么unpack_from()是更加优雅的方案。

我们可能会想在解包记录时利用collections模块中的namedtuple对象。这么做允许我们在返回的元组上设定属性名。示例如下:

from collections import namedtuple
Record = namedtuple('Record', ['kind','x','y'])
with open('data.p', 'rb') as f:
    records = (Record(*r) for r in read_records('<idd', f))
for r in records:
    print(r.kind, r.x, r.y)

如果正在编写一个需要同大量的二进制数据打交道的程序,最好使用像numpy这样的库。比如,与其将二进制数据读取到元组列表中,不如直接将数据读入到结构化的数组中,就像这样:

>>> import numpy as np
>>> f = open('data.b', 'rb')
>>> records = np.fromfile(f, dtype='<i,<d,<d')
>>> records
array([(1, 2.3, 4.5), (6, 7.8, 9.0), (12, 13.4, 56.7)],
      dtype=[('f0', '<i4'), ('f1', '<f8'), ('f2', '<f8')])
>>> records[0]
(1, 2.3, 4.5)
>>> records[1]
(6, 7.8, 9.0)
>>>

最后但同样重要的是,如果面对的任务是从某种已知的文件结构中读取二进制数据(例如图像格式、shapefile、HDF5等),请先检查是否已有相应的Python模块可用。没必的话就别去重复发明轮子了。

相关文章
|
3月前
|
Java 数据处理 索引
(Pandas)Python做数据处理必选框架之一!(二):附带案例分析;刨析DataFrame结构和其属性;学会访问具体元素;判断元素是否存在;元素求和、求标准值、方差、去重、删除、排序...
DataFrame结构 每一列都属于Series类型,不同列之间数据类型可以不一样,但同一列的值类型必须一致。 DataFrame拥有一个总的 idx记录列,该列记录了每一行的索引 在DataFrame中,若列之间的元素个数不匹配,且使用Series填充时,在DataFrame里空值会显示为NaN;当列之间元素个数不匹配,并且不使用Series填充,会报错。在指定了index 属性显示情况下,会按照index的位置进行排序,默认是 [0,1,2,3,...] 从0索引开始正序排序行。
337 0
|
3月前
|
存储 Java 数据处理
(numpy)Python做数据处理必备框架!(一):认识numpy;从概念层面开始学习ndarray数组:形状、数组转置、数值范围、矩阵...
Numpy是什么? numpy是Python中科学计算的基础包。 它是一个Python库,提供多维数组对象、各种派生对象(例如掩码数组和矩阵)以及用于对数组进行快速操作的各种方法,包括数学、逻辑、形状操作、排序、选择、I/0 、离散傅里叶变换、基本线性代数、基本统计运算、随机模拟等等。 Numpy能做什么? numpy的部分功能如下: ndarray,一个具有矢量算术运算和复杂广播能力的快速且节省空间的多维数组 用于对整组数据进行快速运算的标准数学函数(无需编写循环)。 用于读写磁盘数据的工具以及用于操作内存映射文件的工具。 线性代数、随机数生成以及傅里叶变换功能。 用于集成由C、C++
384 1
|
6月前
|
数据采集 存储 NoSQL
Python爬虫案例:Scrapy+XPath解析当当网网页结构
Python爬虫案例:Scrapy+XPath解析当当网网页结构
|
9月前
|
开发框架 Java .NET
Python中main函数:代码结构的基石
在Python中,`main`函数是程序结构化和模块化的重要组成部分。它实现了脚本执行与模块导入的分离,避免全局作用域污染并提升代码复用性。其核心作用包括:标准化程序入口、保障模块复用及支持测试驱动开发(TDD)。根据项目复杂度,`main`函数有基础版、函数封装版、参数解析版和类封装版四种典型写法。 与其他语言相比,Python的`main`机制更灵活,支持同一文件作为脚本运行或模块导入。进阶技巧涵盖多文件项目管理、命令行参数处理、环境变量配置及日志集成等。此外,还需注意常见错误如全局变量污染和循环导入,并通过延迟加载、多进程支持和类型提示优化性能。
881 0
|
12月前
|
存储 数据采集 数据处理
如何在Python中高效地读写大型文件?
大家好,我是V哥。上一篇介绍了Python文件读写操作,今天聊聊如何高效处理大型文件。主要方法包括:逐行读取、分块读取、内存映射(mmap)、pandas分块处理CSV、numpy处理二进制文件、itertools迭代处理及linecache逐行读取。这些方法能有效节省内存,提升效率。关注威哥爱编程,学习更多Python技巧。
332 8
|
12月前
|
存储 JSON 对象存储
如何使用 Python 进行文件读写操作?
大家好,我是V哥。本文介绍Python中文件读写操作的方法,包括文件读取、写入、追加、二进制模式、JSON、CSV和Pandas模块的使用,以及对象序列化与反序列化。通过这些方法,你可以根据不同的文件类型和需求,灵活选择合适的方式进行操作。希望对正在学习Python的小伙伴们有所帮助。欢迎关注威哥爱编程,全栈路上我们并肩前行。
296 4
|
机器学习/深度学习 自然语言处理 语音技术
Python在深度学习领域的应用,重点讲解了神经网络的基础概念、基本结构、训练过程及优化技巧
本文介绍了Python在深度学习领域的应用,重点讲解了神经网络的基础概念、基本结构、训练过程及优化技巧,并通过TensorFlow和PyTorch等库展示了实现神经网络的具体示例,涵盖图像识别、语音识别等多个应用场景。
471 8
|
算法 定位技术 Python
震惊!Python 图结构竟然可以这样玩?DFS&BFS 遍历技巧大公开
在 Python 编程中,图是一种重要的数据结构,而深度优先搜索(DFS)和广度优先搜索(BFS)是遍历图的两种关键算法。本文将通过定义图的数据结构、实现 DFS 和 BFS 算法,并通过具体示例展示其应用,帮助读者深入理解这两种算法。DFS 适用于寻找路径和检查图连通性,而 BFS 适用于寻找最短路径。掌握这些技巧,可以更高效地解决与图相关的复杂问题。
227 2
|
算法 Python
SciPy 教程 之 SciPy 图结构 5
SciPy 图结构教程,介绍图的基本概念和SciPy中处理图结构的模块scipy.sparse.csgraph。重点讲解贝尔曼-福特算法,用于求解任意两点间最短路径,支持有向图和负权边。通过示例演示如何使用bellman_ford()方法计算最短路径。
109 3
|
Python
SciPy 教程 之 SciPy 图结构 7
《SciPy 教程 之 SciPy 图结构 7》介绍了 SciPy 中处理图结构的方法。图是由节点和边组成的集合,用于表示对象及其之间的关系。scipy.sparse.csgraph 模块提供了多种图处理功能,如 `breadth_first_order()` 方法可按广度优先顺序遍历图。示例代码展示了如何使用该方法从给定的邻接矩阵中获取广度优先遍历的顺序。
120 2

推荐镜像

更多