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

    На этом шаге мы рассмотрим способы решения этой задачи.

Задача

    Вам нужно прочесть сложные бинарные данные, которые содержат коллекцию вложенных записей и/или записей различного размера. Такие данные могут включать изображения, видео, векторные изображения и т. д.

Решение

    Модуль 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, определение сдвигов и т. п.). В получившемся классе также отсутствуют привычные удобные моменты, такие как предоставление способа вычислить общий размер структуры.

    На следующем шаге мы продолжим изучение этого вопроса.




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