2.29. Struct e dados binários#

O módulo struct empacota valores Python em um layout binário de tamanho fixo e desempacota bytes de volta para valores Python. Recorra a ele ao trabalhar com um formato de arquivo binário, um protocolo de rede ou um dispositivo que troca registros de tamanho fixo.

Duas funções cobrem a maioria dos casos:

  • struct.pack() – recebe valores Python e uma string de formato e retorna um objeto bytes com o layout exato.

  • struct.unpack() – recebe uma string de formato e um objeto bytes e retorna uma tupla de valores Python.

2.29.1. Strings de formato#

Uma string de formato lista um código por campo do registro. Os códigos descrevem tanto o tamanho quanto a interpretação de cada campo.

O tipo int do Python não tem tamanho fixo – ele cresce para acomodar qualquer valor que você atribuir. Os formatos binários têm, sim, tamanhos fixos: cada campo inteiro usa um número acordado de bytes. O módulo struct converte entre os ints ilimitados do Python e essas representações de tamanho fixo.

A largura de um inteiro é o número de bits que ele utiliza. Um byte são oito bits. O código em minúscula é a variante com sinal; o código em maiúscula é a variante 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 bilhões com sinal, quatro bilhões sem sinal.

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

Escolha uma largura que cubra com folga o intervalo que você espera. Empacotar um valor fora do intervalo declarado faz com que ele dê a volta (wrap) silenciosamente ou lança struct.error, dependendo da build.

Os demais códigos comuns são para floats e strings de bytes:

  • f – float de 32 bits (precisão simples; cerca de sete dígitos decimais). O tipo float comum do Python no MicroPython já tem esse tamanho, então empacotar um em f não tem perdas.

  • d – float de 64 bits (precisão dupla; cerca de quinze dígitos decimais). Empacotar um float de 32 bits do MicroPython em d o amplia para oito bytes, mas não adiciona precisão.

  • s – string de bytes de tamanho fixo, precedida por uma contagem (8s para um campo de oito bytes).

2.29.2. Ordem dos bytes#

Um inteiro de múltiplos bytes pode ser armazenado na memória de duas formas. O número 0x12345678 em um campo de 32 bits é organizado assim:

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

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

Ambos codificam o mesmo valor; eles só discordam sobre qual extremidade do campo contém o byte de menor ordem. Um arquivo escrito por um sistema fica corrompido quando lido pelo outro se a ordem dos bytes não corresponder.

O caractere inicial da string de formato escolhe a ordem:

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

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

  • ! – ordem de rede, equivalente a >.

Sem um caractere inicial, são usados a ordem de bytes nativa e o alinhamento nativo; definir < ou > explicitamente remove essa ambiguidade e é geralmente o que você quer ao ler um arquivo ou ao se comunicar com outra máquina.

Nota

A OpenMV Cam é little-endian – o mesmo que seu PC host. Use < nas strings de formato para arquivos locais da câmera e para dados binários que trafegam de ou para um desktop. 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))

Saída:

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 incompatibilidade lança struct.error.

2.29.4. Desempacotamento#

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

Saída:

320 1000000

struct.unpack() sempre retorna uma tupla, mesmo quando o formato descreve um único campo. Desempacote-a na mesma linha para melhor legibilidade.

2.29.5. Strings de bytes de tamanho fixo#

O código s lê ou escreve um trecho de bytes literalmente. A contagem vem antes do s4s significa “quatro bytes tratados como uma única string de bytes”. Essa é a forma usual de embutir um valor mágico, uma tag de tamanho fixo ou um campo de nome com preenchimento em um registro:

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

Saída:

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

Os quatro primeiros 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 retorna os bytes de volta como um objeto bytes:

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

Saída:

b'OMV0' 320 1000000

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

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

A contagem é um comprimento em bytes, não uma contagem de caracteres – s lida com bytes brutos, então uma string UTF-8 com caracteres de múltiplos bytes precisa primeiro ser passada por .encode() e contada em bytes.

2.29.6. Dimensionamento e leituras parciais#

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

struct.calcsize("<HI")     # 6

Ao ler um fluxo de registros de um arquivo, leia exatamente essa quantidade de bytes por registro:

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 fim do arquivo produz um trecho menor que record_size – trate isso como a condição de fim de fluxo, em vez de tentar desempacotar um registro parcial.