Manipulação e Conversão de Dados Binários em Python com `bytearray` e `struct`

Python oferece um conjunto robusto de ferramentas para manipulação de dados binários em baixo nível. O tipo nativo bytearray permite trabalhar com sequências mutáveis de bytes, enquanto módulos como struct facilitam a conversão entre tipos de dados Python e suas representações binárias compactadas. Este artigo explora métodos para analisar bits individuais dentro de um bytearray, converter sequências de bytes em inteiros e lidar com a codificação de números de ponto flutuante.

Análise Bit a Bit em bytearray

A necessidade de examinar o valor de bits específicos dentro de uma sequência de bytes é comum em protocolos de comunicação ou parsing de formatos de arquivo. Um bytearray pode ser tratado como uma coleção de bits, e operações bit a bit podem ser usadas para extrair informações.

Considere um bytearray e a função para obter o valor booleano de um bit em uma posição específica:


dados_binarios = bytearray(b'\x5A\xC3\xF0\x0F') # Exemplo: 01011010 11000011 11110000 00001111

def obter_estado_bit(buffer: bytearray, indice_bit: int) -> bool:
    """
    Retorna o valor booleano (True se 1, False se 0) de um bit
    em uma posição específica dentro de um bytearray.
    """
    if indice_bit < 0:
        raise ValueError("O índice do bit não pode ser negativo.")
    
    indice_byte = indice_bit // 8
    offset_bit_no_byte = indice_bit % 8

    if indice_byte >= len(buffer):
        raise IndexError(f"Índice de bit {indice_bit} fora do alcance do bytearray (tamanho {len(buffer)*8} bits).")
    
    byte_alvo = buffer[indice_byte]
    # Desloca o bit desejado para a posição 0 e aplica uma máscara com 0x01 (binário 00000001)
    # para isolar seu valor.
    valor_bit = (byte_alvo >> offset_bit_no_byte) & 0x01
    return bool(valor_bit)

print("Análise completa de bits no bytearray:")
for i in range(len(dados_binarios) * 8): # Itera por todos os bits
    print(f"Bit {i}: {obter_estado_bit(dados_binarios, i)}")

Neste exemplo, indice_byte calcula qual byte contém o bit desejado, e offset_bit_no_byte detremina a posição do bit dentro desse byte (de 0 a 7). A operação >> desloca os bits para a direita, e & 0x01 isola o bit menos significativo.

Conversão de Bytes para Inteiros com int.from_bytes

A função estática int.from_bytes() permite converter uma sequência de bytes diretamente em um inteiro. Ela é crucial para desserializar dados binários em valores numéricos.

Os parâmetros principais são:

  • bytes: A sequência de bytes (bytes ou bytearray) a ser convertida.
  • byteorder: Especifica a ordem dos bytes ('big' para big-endian, 'little' para little-endian).
  • signed: Um booleano que indica se o inteiro deve ser interpretado como assinado (True) ou não assinado (False).

O parâmetro signed é particularmente importante, pois determina como os valores são interpretados quando o bit mais significativo do número resultante é 1. Se signed=True, o número pode ser negativo; se signed=False, ele será sempre não negativo.


sequencia_bytes = b'\x00\x01\x80\x00' # Exemplo de 4 bytes

# Exemplo 1: Big-endian, não assinado
# 0x00018000 = 98304
valor_unsigned_be = int.from_bytes(sequencia_bytes, byteorder='big', signed=False)
print(f"Bytes: {sequencia_bytes.hex()}")
print(f"Int (Big Endian, Unsigned): {valor_unsigned_be}")

# Exemplo 2: Little-endian, não assinado
# 0x00800100 = 8388880
valor_unsigned_le = int.from_bytes(sequencia_bytes, byteorder='little', signed=False)
print(f"Int (Little Endian, Unsigned): {valor_unsigned_le}")

# Exemplo 3: Big-endian, assinado (usando apenas 2 bytes, b'\x80\x00')
bytes_assinados = sequencia_bytes[2:4] # b'\x80\x00'
# Em 2 bytes, 0x8000 representa -32768
valor_signed_be = int.from_bytes(bytes_assinados, byteorder='big', signed=True)
print(f"\nBytes: {bytes_assinados.hex()}")
print(f"Int (Big Endian, Signed): {valor_signed_be}")

# O mesmo b'\x80\x00' como não assinado
valor_unsigned_de_signed = int.from_bytes(bytes_assinados, byteorder='big', signed=False)
print(f"Int (Big Endian, Unsigned): {valor_unsigned_de_signed}") # 32768

# Exemplo 4: Um único byte negativo
byte_unico = b'\xFF'
# Com 1 byte, 0xFF é -1 se assinado, 255 se não assinado
int_signed_byte = int.from_bytes(byte_unico, byteorder='big', signed=True)
int_unsigned_byte = int.from_bytes(byte_unico, byteorder='big', signed=False)
print(f"\nBytes: {byte_unico.hex()}")
print(f"Int (Big Endian, Signed): {int_signed_byte}")
print(f"Int (Big Endian, Unsigned): {int_unsigned_byte}")

Ao contrário de linguagens como C ou C# que possuem tipos inteiros de tamanho fixo (uint, int, long), Python lida com inteiros de precisão arbitrária. Isso significa que int.from_bytes() pode converter sequências de bytes de qualquer comprimento em um inteiro Python, e o tamanho do tipo é inferido a partir do número de bytes fornecidos.

Codifiacção e Decodificação de Ponto Flutuante com struct

O módulo struct do Python é essencial para converter valores entre tipos nativos do Python (como inteiros, floats e strings) e suas representações binárias empacotadas, conforme definido por uma string de formato C. Ele é amplamente utilizado em I/O binário, comunicação de rede e parsing de arquivos binários.

A função struct.pack() é usada para converter valores Python em uma sequência de bytes, enquanto struct.unpack() faz o caminho inverso.


import struct

# Valor de ponto flutuante para demonstração
temperatura = 25.75

# Codificando como float de precisão simples (4 bytes, formato 'f')
# Assume a ordem de bytes nativa do sistema por padrão
bytes_float_simples = struct.pack('f', temperatura)
print(f"Valor {temperatura} (float 'f'): {bytes_float_simples.hex()} (Hex)")

# Decodificando de volta para float de precisão simples
valor_decodificado_f = struct.unpack('f', bytes_float_simples)[0]
print(f"Decodificado (float 'f'): {valor_decodificado_f}")

# Codificando como float de precisão dupla (8 bytes, formato 'd')
bytes_float_duplo = struct.pack('d', temperatura)
print(f"\nValor {temperatura} (double 'd'): {bytes_float_duplo.hex()} (Hex)")

# Decodificando de volta para float de precisão dupla
valor_decodificado_d = struct.unpack('d', bytes_float_duplo)[0]
print(f"Decodificado (double 'd'): {valor_decodificado_d}")

# Exemplo com ordem de bytes explícita (Big Endian)
# >f para float big-endian, >d para double big-endian
bytes_float_be = struct.pack('>f', temperatura)
bytes_double_be = struct.pack('>d', temperatura)
print(f"\nValor {temperatura} (float '>f', Big Endian): {bytes_float_be.hex()} (Hex)")
print(f"Valor {temperatura} (double '>d', Big Endian): {bytes_double_be.hex()} (Hex)")

As strings de formato ('f' para float de precisão simples, 'd' para float de precisão dupla) são essenciais para o struct. Prefixos como '>' podem ser usados para especificar a ordem de bytes (big-endian), garantindo a portabilidade da representação binária entre diferentes arquiteturas de sistema.

Tags: Python bytearray int.from_bytes struct operações bit a bit

Publicado em 7-23 05:53