На этом шаге мы рассмотрим способы решения этой задачи.
Вам нужно прочесть сложные бинарные данные, которые содержат коллекцию вложенных записей и/или записей различного размера. Такие данные могут включать изображения, видео, векторные изображения и т. д.
Модуль struct может быть использован для декодирования и кодирования бинарных данных практически любой структуры. Чтобы проиллюстрировать работу с задачей этого рецепта, предположим, что у вас есть структура данных 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) ], ] >>>
Теперь предположим, что данные были закодированы в бинарный файл, который начинается следующим заголовком:
Byte Type Description 0 int File code (0x1234, little endian) 4 double Minimum x (little endian) 12 double Minimum y (little endian) 20 double Maximum x (little endian) 28 double Maximum y (little endian) 36 int Number of polygons (little endian)
После заголовка идет набор многоугольников, каждый из которых закодирован так:
Byte Type Description 0 int Record length including length (N bytes) 4-N Points Pairs of (X,Y) coords as doubles
Чтобы записать этот файл, вы можете использовать такой код:
>>> import struct >>> import itertools >>> def write_polys(filename, polys): # Определяем ограничивающий параллелепипед 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('<iddddi', 0x1234, min_x, min_y, max_x, max_y, len(polys))) for poly in polys: size = len(poly) * struct.calcsize('<dd') f.write(struct.pack('<i', size+4)) for pt in poly: f.write(struct.pack('<dd', *pt)) >>> # Вызываем с нашими данными полигонов >>> write_polys('polys.bin', polys) >>>
Чтобы прочесть получившиеся данные, вы можете написать похожий код с использованием функции struct.unpack(), которая обращает операции, проделанные во время записи. Например:
>>> import struct >>> def read_polys(filename): with open(filename, 'rb') as f: # Читаем заголовок header = f.read(40) file_code, min_x, min_y, max_x, max_y, num_polys = \ struct.unpack('<iddddi', header) polys = [] for n in range(num_polys): pbytes, = struct.unpack('<i', f.read(4)) poly = [] for m in range(pbytes // 16): pt = struct.unpack('<dd', f.read(16)) poly.append(pt) polys.append(poly) return polys >>>
Хотя этот код работает, он представляет собой довольно-таки беспорядочный набор небольших операций чтения, распаковки структур и прочие детали. Если такой код используется для обработки реального файла с данными, он быстро станет еще более запутанным. Это делает очевидным необходимость поиска альтернативного решения, которое могло бы упростить некоторые шаги и позволило бы программисту сосредоточиться на более важных вещах.
В оставшейся части этого рецепта мы шаг за шагом построим достаточно продвинутое решение для интерпретации бинарных данных. Наша цель - предоставить программисту возможность передать высокоуровневую спецификацию формата файла, а все детали чтения и распаковки данных переместить в "подкапотную" часть. Заранее предупреждаем, что нижеследующий код будет самым продвинутым примером. Он использует различные приемы объектно-ориентированного программирования и метапрограммирования. Рекомендуем вам внимательно прочитать раздел "Обсуждение", обращая внимание на ссылки на другие рецепты.
Во-первых, при чтении бинарных данных наиболее типичный случай - это присутствие в файле заголовков и других структур данных. Хотя модуль struct может распаковать эти данные в кортеж, еще один способ представить такую информацию - это использование класса. Вот пример кода:
>>> import struct >>> class StructField: ''' Дескриптор, представляющий простое поле структуры ''' def __init__(self, format, offset): self.format = format self.offset = offset def __get__(self, instance, cls): if instance is None: return self else: r = struct.unpack_from(self.format, instance._buffer, self.offset) return r[0] if len(r) == 1 else r >>> class Structure: def __init__(self, bytedata): self._buffer = memoryview(bytedata) >>>
Этот код использует дескриптор для представления каждого поля структуры. Каждый дескриптор содержит совместимый со struct формат кода вместе с байтовым сдвигом используемого буфера памяти. В методе __get()__ функция struct.unpack_from() используется для распаковки значения из буфера без необходимости делать дополнительные срезы или копии.
Класс Structure просто служит базовым классом (суперклассом), который принимает некие байтовые данные и сохраняет их в буфере памяти, используемом дескриптором StructField. Функция memoryview() в этом классе служит целям, которые мы проясним позднее.
Этот код позволит вам определить структуру как высокоуровневый класс, который отражает информацию, найденную в таблицах, которые описывают ожидаемый формат файла. Например:
>>> class PolyHeader(Structure): file_code = StructField('<i', 0) min_x = StructField('<d', 4) min_y = StructField('<d', 12) max_x = StructField('<d', 20) max_y = StructField('<d', 28) num_polys = StructField('<i', 36) >>>
Вот пример использования этого класса для чтения заголовка из данных о многоугольниках, которые мы записали ранее:
>>> 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, определение сдвигов и т. п.). В получившемся классе также отсутствуют привычные удобные моменты, такие как предоставление способа вычислить общий размер структуры.
На следующем шаге мы продолжим изучение этого вопроса.