Простой Python — страница 28 из 66

Работать с текстовыми данными может быть трудно, но работать с бинарными может быть… интересно. Вам нужно знать о таких концепциях, как порядок следования байтов (как процессор вашего компьютера разбивает данные на байты) и знаковые биты для целых чисел. Вам может понадобиться закопаться в бинарные форматы файлов или сетевых пакетов, чтобы извлечь или даже изменить данные. В этом разделе я покажу вам основы работы с бинарными данными в Python.

bytes и bytearray

В Python 3 появились следующие последовательности восьмибитных целых чисел, имеющих возможные значения от 0 до 255. Они могут быть двух типов:

• bytes неизменяем, как кортеж байтов;

• bytearray изменяем, как список байтов.

Начнем мы с создания списка с именем blist и в следующем примере создадим переменную типа bytes с именем the_bytes и переменную bytearray с именем the_byte_array:

>> blist = [1, 2, 3, 255]

>>> the_bytes = bytes(blist)

>>> the_bytes

b'\x01\x02\x03\xff'

>>> the_byte_array = bytearray(blist)

>>> the_byte_array

bytearray(b'\x01\x02\x03\xff')


Представление значения типа bytes начинается с символа b и кавычки, за которыми следуют шестнадцатеричные последовательности вроде \x02 или символы ASCII, заканчивается конструкция соответствующим символом кавычки. Python преобразует шестнадцатеричные последовательности или символы ASCII в маленькие целые числа, но показывает байтовые значения, которые корректно записаны с точки зрения кодировки ASCII:

>>> b'\x61'

b'a'

>>> b'\x01abc\xff'

b'\x01abc\xff'


В следующем примере показано, что вы не можете изменить переменную типа bytes:

>>> the_bytes[1] = 127

Traceback (most recent call last):

··File "", line 1, in 

TypeError: 'bytes' object does not support item assignment

Но переменная типа bytearray слишком мягкая и легко изменяемая:

>>> the_byte_array = bytearray(blist)

>>> the_byte_array

bytearray(b'\x01\x02\x03\xff')

>>> the_byte_array[1] = 127

>>> the_byte_array

bytearray(b'\x01\x7f\x03\xff')

Каждая из этих переменных может содержать результат, состоящий из 256 элементов, имеющих значения от 0 до 255:

>>> the_bytes = bytes(range(0, 256))

>>> the_byte_array = bytearray(range(0, 256))

При выводе на экран содержимого переменных типа bytes или bytearray Python использует формат \x xx для непечатаемых байтов и их эквиваленты ASCII для печатаемых (плюс некоторых распространенных управляющих последовательностей вроде \n вместо \x0a). Так выглядит на экране представление значения переменной the_bytes (переформатированное вручную для того, чтобы показать по 16 байт на строку):

>>> the_bytes

b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f

\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f

!"#$%&\'()*+,-./

0123456789:;<=>?

@ABCDEFGHIJKLMNO

PQRSTUVWXYZ[\\]^_

`abcdefghijklmno

pqrstuvwxyz{|}~\x7f

\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f

\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f

\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf

\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf

\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf

\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf

\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef

\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'

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

Преобразуем бинарные данные с помощью модуля struct

Как вы уже видели, в Python содержится множество инструментов для манипулирования текстом. Инструменты для бинарных данных гораздо менее распространены. Стандартная библиотека содержит модуль struct, который обрабатывает данные аналогично структурам в С или С++. С помощью этого модуля вы можете преобразовать бинарные данные в структуры данных Python и наоборот.

Посмотрим, как он работает с данными из файла с расширением PNG — распространенного формата изображений, который вы можете встретить наряду с GIF и JPEG. Мы напишем небольшую программу, которая извлекает ширину и высоту изображения из фрагмента данных PNG.

Используем логотип издательства O’Reilly — изображение маленького долгопята с глазками-бусинами (рис. 7.1).


Рис. 7.1. Долгопят от издательства O’Reilly


Файл этого изображения с расширением PNG доступен в «Википедии». Мы не будем рассматривать чтение файла вплоть до главы 8, поэтому я загрузил этот файл, написал небольшую программу, которая выводит его значения как байты, и просто напечатал значения первых 30 байт как значения переменной типа bytes по имени data для примера, который следует далее. (Спецификация формата PNG предполагает, что ширина и высота хранятся в первых 24 байтах, поэтому нам пока что больше данных и не нужно.)

>>> import struct

>>> valid_png_header = b'\x89PNG\r\n\x1a\n'

>>> data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR' + \

…·····b'\x00\x00\x00\x9a\x00\x00\x00\x8d\x08\x02\x00\x00\x00\xc0'

>>> if data[:8] == valid_png_header:

…·····width, height = struct.unpack('>LL', data[16:24])

…·····print('Valid PNG, width', width, 'height', height)

… else:

…·····print('Not a valid PNG')

Valid PNG, width 154 height 141

Этот код делает следующее.

• Переменная data содержит первые 30 байт файла PNG. Для того чтобы разместить ее на странице, я объединил две байтовые строки с помощью операторов + и \.

• Переменная valid_png_header содержит восьмибайтовую последовательность, которая обозначает начало корректного PNG-файла.

• Значение переменной width извлекается из 16–20-го байтов, а переменной height — из байтов 21–24.

>LL — это строка формата, которая указывает функции unpack(), как интерпретировать входные последовательности байтов и преобразовать их в типы данных Python. Рассмотрим ее детальнее:

• символ < означает, что целые числа хранятся в формате big-endian (обратный порядок байтов);

• каждый символ L определяет четырехбайтное целое число типа unsigned long.

Вы можете проверить значение каждого четырехбайтного набора непосредственно:

>>> data[16:20]

b'\x00\x00\x00\x9a'

>>> data[20:24]0x9a

b'\x00\x00\x00\x8d'

У целых чисел с обратным порядком байтов главный байт располагается слева. Поскольку значения ширины и длины меньше 255, они умещаются в последний байт каждой последовательности. Вы можете убедиться в том, что эти шестнадцатеричные значения соответствуют ожидаемым десятичным значениям:

>>> 0x9a

154

>>> 0x8d

141

Если вы хотите отправить их в противоположном направлении и преобразовать данные Python в байты, используйте функцию pack() модуля struct:

>>> import struct

>>> struct.pack('>L', 154)

b'\x00\x00\x00\x9a'

>>> struct.pack('>L', 141)

b'\x00\x00\x00\x8d'

В табл. 7.5 и 7.6 показаны спецификаторы формата для функций pack() и unpack(). Спецификаторы порядка байтов располагаются первыми в строке формата.


Таблица 7.5. Спецификаторы порядка байтов
СпецификаторПорядок байтов
<Прямой порядок
>Обратный порядок

Таблица 7.6. Спецификаторы формата
СпецификаторОписаниеКоличество байтов
xПропустить байт1
bЗнаковый байт1
BБеззнаковый байт1
hЗнаковое короткое целое число2
HБеззнаковое короткое целое число2
iЗнаковое целое число4
IБеззнаковое целое число4
lЗнаковое длинное целое число4
LБеззнаковое длинное целое число4
QБеззнаковое очень длинное целое число8
fЧисло с плавающей точкой4
dЧисло с плавающей точкой двойной точности8
pСчетчик и символы1 + count
sСимволыcount

Спецификаторы типа следуют за символом, указывающим порядок байтов. Перед любым спецификатором может следовать число, которое указывает количество; запись 5B аналогична записи BBBBB.

Вы можете использовать префикс счетчика вместо конструкции >LL:

>>> struct.unpack('>2L', data[16:24])

(154, 141)

Мы использовали разбиение data[16:24], чтобы получить непосредственно интересующие нас байты. Мы также могли добавить спецификатор x, чтобы пропустить неинтересные части:

>>> struct.unpack('>16x2L6x', data)

(154, 141)

Эта строка означает:

• использовать формат с обратным порядком байтов (>);

• пропустить 16 байт (16x);

• прочесть 8 байт — два беззнаковых длинных целых числа (2L);

• пропустить последние 6 байт (6x).

Другие инструменты для работы с бинарными данными

Некоторые сторонние пакеты с открытым исходным кодом часто предлагают следующие более декларативные способы определения и извлечения бинарных данных:

• bitstring (http://bit.ly/py-bitstring);

• construct (http://bit.ly/py-construct);

• hachoir (http://bit.ly/hachoir-pkg);

• binio (http://spika.net/py/binio/).

В приложении Г содержатся инструкции о том, как загрузить и установить внешние пакеты вроде этих. Для следующего примера вам нужно установить пакет construct. Вот все, что вам необходимо сделать:

$ pip install construct

Вот так можно извлечь измерения PNG из нашей строки байтов data с помощью пакета construct:

>>> from construct import Struct, Magic, UBInt32, Const, String

>>> # адаптировано из кода по адресу https://github.com/construct

>>> fmt = Struct('png',

…·····Magic(b'\x89PNG\r\n\x1a\n'),

…·····UBInt32('length'),

…·····Const(String('type', 4), b'IHDR'),

…·····UBInt32('width'),

…·····UBInt32('height')

…·····)

>>> data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR' + \

…·····b'\x00\x00\x00\x9a\x00\x00\x00\x8d\x08\x02\x00\x00\x00\xc0'

>>> result = fmt.parse(data)

>>> print(result)

Container:

····length = 13

····type = b'IHDR'

····width = 154

····height = 141

>>> print(result.width, result.height)

154, 141

Преобразование байтов/строк с помощью функции binascii()

Стандартный модуль binascii содержит функции, которые позволяют вам конвертировать данные в бинарный вид и в различные представления строк: шестнадцатеричное (с основанием 16), с основанием 64, uuencoded и др. Например, в следующем сниппете выведем на экран восьмибайтовый заголовок PNG как последовательность шестнадцатеричных значений вместо смеси символов ASCII и управляющих последовательностей вида \x xx, которые Python использует для отображения байтовых переменных:

>>> import binascii

>>> valid_png_header = b'\x89PNG\r\n\x1a\n'

>>> print(binascii.hexlify(valid_png_header))

b'89504e470d0a1a0a'

В другую сторону это тоже работает:

>>> print(binascii.unhexlify(b'89504e470d0a1a0a'))

b'\x89PNG\r\n\x1a\n'

Битовые операторы

Python предоставляет целочисленные операторы, работающие на уровне битов, их аналоги имеются в языке С. В табл. 7.7 показаны они все, а также примеры их использования для целых чисел a (в десятичной системе счисления 5, в двоичной — 0b0101) и b (в десятичной системе счисления 1, в двоичной — 0b0001).


Таблица 7.7. Целочисленные операции для уровня битов
ОператорОписаниеПримерДесятичный результатДвоичный результат
&Логическое Иa & b10b0001
|Логическое ИЛИa | b50b0101
^Исключающее ИЛИa ^ b40b0100
~Инверсия битов— a— 6Двоичное представление зависит от размера типа int
<<Сдвиг влевоa << 1100b1010
>>Сдвиг вправоa << 120b0010

Эти операторы похожи на операторы для работы с множествами, показанные в главе 3. Оператор & возвращает биты, которые одинаковы в обоих аргументах, а оператор | возвращает биты, которые установлены в обоих аргументах. Оператор ^ возвращает биты, которые установлены в одном или в другом аргументе, но не в них обоих. Оператор ~ обращает порядок байтов в одном аргументе, он также изменяет знак, поскольку старший бит целого числа указывает на его знак (1 означает «минус») в арифметике дополнительных кодов, которая используется во всех современных компьютерах. Операторы << и >> просто смещают биты влево или вправо. Сдвиг влево на один бит аналогичен умножению на 2, а сдвиг вправо — делению на 2.

Упражнения