============================ 6.12 读取嵌套和可变长二进制数据 ============================ ---------- 问题 ---------- 你需要读取包含嵌套或者可变长记录集合的复杂二进制格式的数据。这些数据可能包含图片、视频、电子地图文件等。 ---------- 解决方案 ---------- ``struct`` 模块可被用来编码/解码几乎所有类型的二进制的数据结构。为了解释清楚这种数据,假设你用下面的Python数据结构 来表示一个组成一系列多边形的点的集合: .. code-block:: python polys = [ [ (1.0, 2.5), (3.5, 4.0), (2.5, 1.5) ], [ (7.0, 1.2), (5.1, 3.0), (0.5, 7.5), (0.8, 9.0) ], [ (3.4, 6.3), (1.2, 0.5), (4.6, 9.2) ], ] 现在假设这个数据被编码到一个以下列头部开始的二进制文件中去了: .. code-block:: python +------+--------+------------------------------------+ |Byte | Type | Description | +======+========+====================================+ |0 | int | 文件代码(0x1234,小端) | +------+--------+------------------------------------+ |4 | double | x 的最小值(小端) | +------+--------+------------------------------------+ |12 | double | y 的最小值(小端) | +------+--------+------------------------------------+ |20 | double | x 的最大值(小端) | +------+--------+------------------------------------+ |28 | double | y 的最大值(小端) | +------+--------+------------------------------------+ |36 | int | 三角形数量(小端) | +------+--------+------------------------------------+ 紧跟着头部是一系列的多边形记录,编码格式如下: .. code-block:: python +------+--------+-------------------------------------------+ |Byte | Type | Description | +======+========+===========================================+ |0 | int | 记录长度(N字节) | +------+--------+-------------------------------------------+ |4-N | Points | (X,Y) 坐标,以浮点数表示 | +------+--------+-------------------------------------------+ 为了写这样的文件,你可以使用如下的Python代码: .. code-block:: python import struct import itertools def write_polys(filename, polys): # Determine bounding box flattened = list(itertools.chain(*polys)) min_x = min(x for x, y in flattened) max_x = max(x for x, y in flattened) min_y = min(y for x, y in flattened) max_y = max(y for x, y in flattened) with open(filename, 'wb') as f: f.write(struct.pack('>> f = open('polys.bin', 'rb') >>> phead = PolyHeader(f.read(40)) >>> phead.file_code == 0x1234 True >>> phead.min_x 0.5 >>> phead.min_y 0.5 >>> phead.max_x 7.0 >>> phead.max_y 9.2 >>> phead.num_polys 3 >>> 这个很有趣,不过这种方式还是有一些烦人的地方。首先,尽管你获得了一个类接口的便利, 但是这个代码还是有点臃肿,还需要使用者指定很多底层的细节(比如重复使用 ``StructField`` ,指定偏移量等)。 另外,返回的结果类同样确实一些便利的方法来计算结构的总数。 任何时候只要你遇到了像这样冗余的类定义,你应该考虑下使用类装饰器或元类。 元类有一个特性就是它能够被用来填充许多低层的实现细节,从而释放使用者的负担。 下面我来举个例子,使用元类稍微改造下我们的 ``Structure`` 类: .. code-block:: python class StructureMeta(type): ''' Metaclass that automatically creates StructField descriptors ''' def __init__(self, clsname, bases, clsdict): fields = getattr(self, '_fields_', []) byte_order = '' offset = 0 for format, fieldname in fields: if format.startswith(('<','>','!','@')): byte_order = format[0] format = format[1:] format = byte_order + format setattr(self, fieldname, StructField(format, offset)) offset += struct.calcsize(format) setattr(self, 'struct_size', offset) class Structure(metaclass=StructureMeta): def __init__(self, bytedata): self._buffer = bytedata @classmethod def from_file(cls, f): return cls(f.read(cls.struct_size)) 使用新的 ``Structure`` 类,你可以像下面这样定义一个结构: .. code-block:: python class PolyHeader(Structure): _fields_ = [ ('>> f = open('polys.bin', 'rb') >>> phead = PolyHeader.from_file(f) >>> phead.file_code == 0x1234 True >>> phead.min_x 0.5 >>> phead.min_y 0.5 >>> phead.max_x 7.0 >>> phead.max_y 9.2 >>> phead.num_polys 3 >>> 一旦你开始使用了元类,你就可以让它变得更加智能。例如,假设你还想支持嵌套的字节结构, 下面是对前面元类的一个小的改进,提供了一个新的辅助描述器来达到想要的效果: .. code-block:: python class NestedStruct: ''' Descriptor representing a nested structure ''' def __init__(self, name, struct_type, offset): self.name = name self.struct_type = struct_type self.offset = offset def __get__(self, instance, cls): if instance is None: return self else: data = instance._buffer[self.offset: self.offset+self.struct_type.struct_size] result = self.struct_type(data) # Save resulting structure back on instance to avoid # further recomputation of this step setattr(instance, self.name, result) return result class StructureMeta(type): ''' Metaclass that automatically creates StructField descriptors ''' def __init__(self, clsname, bases, clsdict): fields = getattr(self, '_fields_', []) byte_order = '' offset = 0 for format, fieldname in fields: if isinstance(format, StructureMeta): setattr(self, fieldname, NestedStruct(fieldname, format, offset)) offset += format.struct_size else: if format.startswith(('<','>','!','@')): byte_order = format[0] format = format[1:] format = byte_order + format setattr(self, fieldname, StructField(format, offset)) offset += struct.calcsize(format) setattr(self, 'struct_size', offset) 在这段代码中,``NestedStruct`` 描述器被用来叠加另外一个定义在某个内存区域上的结构。 它通过将原始内存缓冲进行切片操作后实例化给定的结构类型。由于底层的内存缓冲区是通过一个内存视图初始化的, 所以这种切片操作不会引发任何的额外的内存复制。相反,它仅仅就是之前的内存的一个叠加而已。 另外,为了防止重复实例化,通过使用和8.10小节同样的技术,描述器保存了该实例中的内部结构对象。 使用这个新的修正版,你就可以像下面这样编写: .. code-block:: python class Point(Structure): _fields_ = [ ('>> f = open('polys.bin', 'rb') >>> phead = PolyHeader.from_file(f) >>> phead.file_code == 0x1234 True >>> phead.min # Nested structure <__main__.Point object at 0x1006a48d0> >>> phead.min.x 0.5 >>> phead.min.y 0.5 >>> phead.max.x 7.0 >>> phead.max.y 9.2 >>> phead.num_polys 3 >>> 到目前为止,一个处理定长记录的框架已经写好了。但是如果组件记录是变长的呢? 比如,多边形文件包含变长的部分。 一种方案是写一个类来表示字节数据,同时写一个工具函数来通过多少方式解析内容。跟6.11小节的代码很类似: .. code-block:: python class SizedRecord: def __init__(self, bytedata): self._buffer = memoryview(bytedata) @classmethod def from_file(cls, f, size_fmt, includes_size=True): sz_nbytes = struct.calcsize(size_fmt) sz_bytes = f.read(sz_nbytes) sz, = struct.unpack(size_fmt, sz_bytes) buf = f.read(sz - includes_size * sz_nbytes) return cls(buf) def iter_as(self, code): if isinstance(code, str): s = struct.Struct(code) for off in range(0, len(self._buffer), s.size): yield s.unpack_from(self._buffer, off) elif isinstance(code, StructureMeta): size = code.struct_size for off in range(0, len(self._buffer), size): data = self._buffer[off:off+size] yield code(data) 类方法 ``SizedRecord.from_file()`` 是一个工具,用来从一个文件中读取带大小前缀的数据块, 这也是很多文件格式常用的方式。作为输入,它接受一个包含大小编码的结构格式编码,并且也是自己形式。 可选的 ``includes_size`` 参数指定了字节数是否包含头部大小。 下面是一个例子教你怎样使用从多边形文件中读取单独的多边形数据: .. code-block:: python >>> f = open('polys.bin', 'rb') >>> phead = PolyHeader.from_file(f) >>> phead.num_polys 3 >>> polydata = [ SizedRecord.from_file(f, '>> polydata [<__main__.SizedRecord object at 0x1006a4d50>, <__main__.SizedRecord object at 0x1006a4f50>, <__main__.SizedRecord object at 0x10070da90>] >>> 可以看出,``SizedRecord`` 实例的内容还没有被解析出来。 可以使用 ``iter_as()`` 方法来达到目的,这个方法接受一个结构格式化编码或者是 ``Structure`` 类作为输入。 这样子可以很灵活的去解析数据,例如: .. code-block:: python >>> for n, poly in enumerate(polydata): ... print('Polygon', n) ... for p in poly.iter_as('>> >>> for n, poly in enumerate(polydata): ... print('Polygon', n) ... for p in poly.iter_as(Point): ... print(p.x, p.y) ... Polygon 0 1.0 2.5 3.5 4.0 2.5 1.5 Polygon 1 7.0 1.2 5.1 3.0 0.5 7.5 0.8 9.0 Polygon 2 3.4 6.3 1.2 0.5 4.6 9.2 >>> 将所有这些结合起来,下面是一个 ``read_polys()`` 函数的另外一个修正版: .. code-block:: python class Point(Structure): _fields_ = [ ('表示高位优先), 那后面所有字段的顺序都以这个顺序为准。这么做可以帮助避免额外输入,但是在定义的中间我们仍然可能切换顺序的。 比如,你可能有一些比较复杂的结构,就像下面这样: .. code-block:: python class ShapeFile(Structure): _fields_ = [ ('>i', 'file_code'), # Big endian ('20s', 'unused'), ('i', 'file_length'), ('