2.29. Struct e dados binários

O módulo struct empacota valores Python numa disposição binária fixa e desempacota bytes de volta em valores Python. Recorra a ele quando trabalhar com um formato de ficheiro binário, um protocolo de rede, ou um dispositivo que troca registos de tamanho fixo.

Duas funções cobrem a maioria dos casos:

  • struct.pack() – recebe valores Python e uma string de formato, e devolve um objeto bytes com a disposição exata.

  • struct.unpack() – recebe uma string de formato e um objeto bytes, e devolve um tuple de valores Python.

2.29.1. Strings de formato

Uma string de formato lista um código por campo no registo. Os códigos descrevem tanto o tamanho como a interpretação de cada campo.

O int do Python não tem tamanho fixo – cresce para conter qualquer valor que lhe seja atribuído. Os formatos binários têm tamanhos fixos: cada campo inteiro utiliza um número acordado de bytes. O struct converte entre inteiros Python de tamanho ilimitado e estas representações de tamanho fixo.

A largura de um inteiro é o número de bits que utiliza. Um byte corresponde a oito bits. O código em minúsculas é a variante com sinal; o código em maiúsculas é o sem sinal (apenas valores não negativos):

  • b / B8 bits (um byte). -128..127 com sinal, 0..255 sem sinal.

  • h / H16 bits (dois bytes). -32768..32767 com sinal, 0..65535 sem sinal.

  • i / I32 bits (quatro bytes). Cerca de ±dois mil milhões com sinal, quatro mil milhões sem sinal.

  • q / Q64 bits (oito bytes). Efetivamente ilimitado para uso corrente.

Escolha uma largura que cubra confortavelmente o intervalo esperado. Empacotar um valor fora do intervalo declarado pode resultar em truncagem silenciosa ou levantar struct.error, consoante a versão.

Os restantes códigos comuns destinam-se a números de vírgula flutuante e strings de bytes:

  • f – vírgula flutuante de 32 bits (precisão simples; cerca de sete dígitos decimais). O float normal do MicroPython já tem este tamanho, pelo que empacotá-lo em f não provoca perda de precisão.

  • d – vírgula flutuante de 64 bits (precisão dupla; cerca de quinze dígitos decimais). Empacotar um float MicroPython de 32 bits em d alarga-o para oito bytes, mas não acrescenta precisão.

  • s – string de bytes de comprimento fixo, precedida de um contador (8s para um campo de oito bytes).

2.29.2. Ordem dos bytes

Um inteiro multi-byte pode ser armazenado em memória de duas formas. O número 0x12345678 num campo de 32 bits é disposto assim:

  • Little-endian – byte menos significativo primeiro: 78 56 34 12.

  • Big-endian – byte mais significativo primeiro: 12 34 56 78.

Ambos codificam o mesmo valor; apenas diferem no extremo do campo que contém o byte de menor peso. Um ficheiro escrito por um sistema fica corrompido ao ser lido pelo outro se a ordem dos bytes não coincidir.

O caracter inicial da string de formato define a ordem:

  • < – little-endian. Comum em x86 e ARM.

  • > – big-endian. Comum em protocolos de rede.

  • ! – ordem de rede, equivalente a >.

Sem um caracter inicial, é utilizada a ordem e o alinhamento nativos; definir < ou > explicitamente elimina essa ambiguidade e é geralmente o pretendido ao ler um ficheiro ou comunicar com outra máquina.

Nota

A OpenMV Cam é little-endian – tal como o PC anfitrião. Use < nas strings de formato para ficheiros locais da câmara e para dados binários que circulam para ou a partir do PC. Use > (ou !) para protocolos de rede e para qualquer formato cuja especificação exija big-endian.

Six bytes laid out in a row, with the first two bytes grouped as an "H" field (16-bit unsigned) and the next four as an "I" field (32-bit unsigned), each labelled with their little-endian byte order.

"<HI" empacota um valor de 16 bits seguido de um valor de 32 bits em seis bytes little-endian.

2.29.3. Empacotamento

import struct

blob = struct.pack("<HI", 320, 1000000)
print(blob, len(blob))

Resultado:

b'@\x01@B\x0f\x00' 6

O formato <HI produz seis bytes: dois para o campo H e quatro para o campo I, todos little-endian. Passe exatamente o número de valores que o formato espera – uma discrepância levanta struct.error.

2.29.4. Desempacotamento

width, count = struct.unpack("<HI", blob)
print(width, count)

Resultado:

320 1000000

struct.unpack() devolve sempre um tuple, mesmo quando o formato descreve um único campo. Desempacote-o na mesma linha para maior legibilidade.

2.29.5. Strings de bytes de comprimento fixo

O código s lê ou escreve um bloco de bytes literalmente. O contador vai antes do s4s significa «quatro bytes tratados como uma única string de bytes». É a forma habitual de incorporar um valor mágico, uma etiqueta de tamanho fixo, ou um campo de nome com preenchimento num registo:

header = struct.pack("<4sHI", b"OMV0", 320, 1000000)
print(header)

Resultado:

b'OMV0@\x01@B\x0f\x00'

Os primeiros quatro bytes são o valor mágico literal b"OMV0"; os dois seguintes são o campo H (320); os últimos quatro são o campo I (1000000). O desempacotamento devolve os bytes como um objeto bytes:

magic, width, count = struct.unpack("<4sHI", header)
print(magic, width, count)

Resultado:

b'OMV0' 320 1000000

Se o valor de origem for mais curto do que o contador declarado, o resultado é preenchido à direita com \x00; se for mais longo, os bytes a mais são silenciosamente descartados:

struct.pack("4s", b"hi")        # b'hi\x00\x00'
struct.pack("4s", b"toolong")   # b'tool'

O contador é um comprimento em bytes, não em caracteres – s trabalha com bytes brutos, pelo que uma string UTF-8 com caracteres multi-byte precisa de ser .encode()“d e o comprimento contado em bytes primeiro.

2.29.6. Dimensionamento e leituras parciais

struct.calcsize() devolve o número de bytes que uma string de formato consome:

struct.calcsize("<HI")     # 6

Ao ler um fluxo de registos de um ficheiro, leia exatamente esse número de bytes por registo:

record_size = struct.calcsize("<HI")
with open("data.bin", "rb") as f:
    while True:
        chunk = f.read(record_size)
        if len(chunk) < record_size:
            break
        width, count = struct.unpack("<HI", chunk)
        print(width, count)

Uma leitura curta no final do ficheiro produz um bloco menor que record_size – trate isso como a condição de fim de fluxo em vez de tentar desempacotar um registo parcial.