🧠FFVAcademy
📡

Serialização, endianness, UTF-8: os bytes que viajam

11 min de leitura·+55 XP

Dados precisam ser convertidos para bytes para viajar pela rede ou ser armazenados. Serialização parece trivial até você debugar uma corrupção de dados entre sistemas com endianness diferente, ou descobrir que um encoding errado transformou "olá" em "ol\xc3\xa1" no banco.

Unicode e encodings: separar conceito de representação

# Unicode: code points abstratos
s = "olá mundo"
print(len(s))           # 9 caracteres (code points)
print(s[2])             # "á" — código U+00E1

# UTF-8: encoding de comprimento variável (1-4 bytes por code point)
b_utf8 = s.encode("utf-8")
print(b_utf8)           # b'olá mundo' — "á" = 2 bytes (0xC3 0xA1)
print(len(b_utf8))      # 10 bytes (9 chars + 1 extra byte para "á")

# Latin-1 (ISO-8859-1): 1 byte por char, apenas 256 caracteres
b_latin1 = s.encode("latin-1")
print(b_latin1)         # b'olá mundo' — "á" = 1 byte (0xE1)
print(len(b_latin1))    # 9 bytes

# UTF-16: 2 bytes por char (BOM indica endianness)
b_utf16 = s.encode("utf-16")
print(b_utf16[:4])      # b'ÿþ' = BOM little-endian + primeiros bytes

# Detectar encoding:
import chardet
resultado = chardet.detect(b_utf8)
print(resultado)   # {'encoding': 'utf-8', 'confidence': 0.99}

# Unicode normalization: mesma string, bytes diferentes!
# "á" pode ser: U+00E1 (precomposta) ou U+0061 + U+0301 (a + combining accent)
import unicodedata
s1 = "á"       # "á" precomposta
s2 = "á"      # "a" + acento combinando
print(s1 == s2)     # False! mesma aparência, bytes diferentes
print(s1 == unicodedata.normalize("NFC", s2))  # True (normaliza para NFC)

Endianness: a ordem dos bytes

import struct

# Big-endian (network byte order) vs little-endian
numero = 0x12345678

# Big-endian: byte mais significativo primeiro
be = struct.pack(">I", numero)   # > = big-endian, I = unsigned int 4 bytes
print(be.hex())   # 12345678

# Little-endian: byte menos significativo primeiro
le = struct.pack("<I", numero)   # < = little-endian
print(le.hex())   # 78563412  ← invertido!

# x86/ARM são little-endian:
import sys
print(sys.byteorder)   # 'little'

# Rede TCP/IP usa big-endian (network byte order):
import socket
# Converter int para network byte order:
print(socket.htons(0x1234).to_bytes(2, 'big').hex())  # 1234 (já big-endian)

# struct.pack: pacotes binários para protocolo de rede
# ">HHI" = big-endian, unsigned short, unsigned short, unsigned int
header = struct.pack(">HHI", 0x0001, 0x0002, 0x12345678)
print(header.hex())   # 000100021234567

# Desempacotar:
version, msg_type, length = struct.unpack(">HHI", header)
print(version, msg_type, length)   # 1 2 305419896

Formatos de serialização: comparação real

FormatoTipoTamanho relativoParse speedSchema?Legível?
JSONTexto100% (baseline)LentoNãoSim
XMLTexto150-300%Muito lentoXSD opcionalSim
MessagePackBinário~50%RápidoNãoNão
CBORBinário~50%RápidoNãoNão
ProtobufBinário~30%Muito rápidoSim (.proto)Não
AvroBinário~30%Muito rápidoSim (JSON schema)Não
ParquetBinário colunar~10-20%Muito rápidoSimNão
import json, struct, time

dados = {"id": 12345, "nome": "Fernando", "ativo": True, "score": 98.7}

# JSON: texto legível mas parsing lento
json_bytes = json.dumps(dados).encode()
print(f"JSON: {len(json_bytes)} bytes")    # ~50 bytes

# struct: binário fixo, sem nomes de campo
# ">IH?f" = big-endian: int(4), short(2), bool(1), float(4) = 11 bytes
binario = struct.pack(">IH?f", 12345, len("Fernando"), True, 98.7)
print(f"struct: {len(binario)} bytes")     # 11 bytes — 5x menor

# msgpack: binário flexível com nomes de campo
# pip install msgpack
import msgpack
mp_bytes = msgpack.packb(dados)
print(f"msgpack: {len(mp_bytes)} bytes")   # ~30 bytes — sem overhead de texto

# Protobuf (pip install protobuf + arquivo .proto):
# message Dados { int32 id = 1; string nome = 2; bool ativo = 3; float score = 4; }
# pb_bytes = Dados(id=12345, nome="Fernando", ativo=True, score=98.7).SerializeToString()
# ~20 bytes — mais compacto ainda
Decisão prática: JSON para APIs públicas e debugging (legibilidade > performance). MessagePack/CBOR para cache e APIs internas (binário sem schema). Protobuf/gRPC para microserviços de alta frequência (schema + performance). Parquet/Arrow para analytics e data pipelines (colunar = compressão máxima). Sempre use UTF-8 para texto — é o padrão universal.
💡
Próximo: Tempo distribuído — NTP, clock skew, e por que nunca usar wall clock para medir duração.
🧩

Quiz rápido

3 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito

Continue lendo