Шаг 114.
Python: сборник рецептов.Кодирование и обработка данных. Чтение вложенных и различных по размеру бинарных структур (окончание)

    На этом шаге мы закончим изучение этого вопроса.

    Первый путь - написать класс, который просто представляет кусок (chunk) бинарных данных вместе с вспомогательной функцией для интерпретирования содержимого различными способами:

>>> 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.fmm_file() используется для чтения из файла куска (chunk) данных с префиксом, определяющим размер, что является обычным для многих форматов файлов. На вход он принимает код форматирования структуры, который содержит кодировку размера, который должен быть представлен в байтах. Необязательный аргумент includessize определяет, включает число байтов заголовок размера или нет. Вот пример того, как вы можете использовать этот код для прочтения отдельного многоугольника из файла с многоугольниками:

>>> f = open('polys.bin', 'rb')
>>> phead = PolyHeader.from_file(f)
>>> phead.num_polys
3
>>> polydata = [SizedRecord.from_file(f, '<i') for n in range(phead.num_polys)]
>>> polydata
[<__main__.SizedRecord object at 0x000001F2D9A4FFA0>, 
 <__main__.SizedRecord object at 0x000001F2D9A286D0>, 
 <__main__.SizedRecord object at 0x000001F2D9A58040>]
>>> 

    Как показано выше, содержимое экземпляров SizeRecord пока еще не интерпретировано. Чтобы сделать это, используйте метод iter_as(), который принимает на вход код структуры формата или класс Structure. Это предоставляет вам немалую гибкость в том, как можно интерпретировать

>>> for n, poly in enumerate(polydata):
	print('Polygon', n)
	for p in poly.iter_as('<dd'):
		print(p)

		
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)
>>> 
>>> 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():

>>> class Point(Structure):
        _fields_ = [
            ('<d', 'x'),
            ('d', 'y')
            ]

        
>>> class PolyHeader(Structure):
        _fields_ = [
            ('<i', 'file_code'),
            (Point, 'min'),
            (Point, 'max'),
            ('i', 'num_polys')
            ]

        
>>> def read_polys(filename):
        polys = []
        with open(filename, 'rb') as f:
            phead = PolyHeader.from_file(f)
            for n in range(phead.num_polys):
                rec = SizedRecord.from_file(f, '<i')
                poly = [(p.x, p.y) for p in rec.iter_as(Point)]
                polys.append(poly)
        return polys

>>> polys = read_polys('polys.bin')
>>> 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)]]
>>> 


Обсуждение

    Этот рецепт предоставляет практическое применение различных продвинутых приемов программирования, в том числе дескрипторов, ленивых вычислений, метаклассов, переменных класса и представлений памяти (memoryviews). И все они служат очень четко определенной цели.

    Главная идея этой реализации в том, что она полностью построена на идее ленивой распаковки. Когда экземпляр Structure создан, __init__() просто создает memoryview предоставленных байтовых данных и больше ничего не делает. В этот момент не производится распаковка или другие связанные со структурой операции. Причина использовать этот подход в том, что вы можете быть заинтересованы только в получении нескольких конкретных частей бинарной записи. Вместо распаковки файла целиком будут распакованы только участки, к которым осуществляется доступ.

    Чтобы реализовать ленивую распаковку и упаковку значений, используется класс-дескриптор StructField. Каждый атрибут, который пользователь запишет в __fields__, конвертируется в дескриптор StructField, который сохраняет связанный код формата структуры и байтовый сдвиг в хранимый буфер. Метакласс StructureMeta - то, что автоматически создает эти дескрипторы при определении различных структурных классов. Главная причина использовать метакласс в том, что он очень сильно облегчает пользователю определение формата структуры, давая возможность высокоуровневого описания без необходимости волноваться о низкоуровневых деталях.

    Тонкий момент использования метакласса StructureMeta в том, что он делает порядок байтов общим. Так что если любой атрибут определил порядок байтов (< для little endian или > для big endian), этот порядок будет применен ко всем последующим полям. Это помогает избежать излишнего ввода с клавиатуры, но также оставляет возможность переключиться на другой порядок в середине определения. Например, если у вас что- то сложное:

class ShapeFile(Structure):
    _fields_ = [('>i', 'file_code'), # Big endian 
      ('20s', 'unused'),
      ('i', 'file_length'),
      ('<i', 'version'),  # Little endian
      ('i', 'shape_type'),
      ('d', 'min_x'),
      ('d', 'min_y'),
      ('d', 'max_x'),
      ('d', 'max_y'),
      ('d', 'min_z'),
      ('d', 'max_z'),
      ('d', 'min_m'),
      ('d', 'max_m') 
  ]

    Как было отмечено, использование memoryview() в решении позволяет избавиться от копий в памяти. Когда структуры начинают вкладываться одна в другую, представления памяти (memoryviews) могут быть использованы для наложения разных частей определения структуры на одну и ту же область памяти. Этот аспект решения довольно тонкий, и он касается различий работы со срезами при использовании представлений памяти и обычных байтовых массивов. Если вы извлекаете срез из байтовой строки или массива, вы обычно получаете копию данных. А с представлением памяти это не так: срезы просто накладываются на существующую память. Поэтому этот подход эффективнее.

    На следующем шаге мы рассмотрим суммирование данных и обсчет статистики.




Предыдущий шаг Содержание Следующий шаг