Unicode, UTF-8 e Codificação de Caracteres: O que Todo Desenvolvedor Precisa Saber
Você copia um nome de um formulário web e ele é salvo no banco de dados como é. Você abre um CSV no Excel e todos os caracteres acentuados estão errados. Um usuário cola um emoji em um campo de texto e todo o seu insert MySQL falha com um erro críptico. Você adiciona um BOM a um arquivo para que o Excel o leia corretamente, e agora seu script shell quebra na primeira linha.
Esses são bugs de codificação de caracteres. Estão entre os problemas mais desorientadores que um desenvolvedor pode enfrentar, pois os sintomas parecem aleatórios — a mesma string pode ser exibida corretamente em um contexto e se transformar em lixo em outro. A causa é quase sempre a mesma: dois softwares discordam sobre como os bytes se mapeiam para caracteres.
Este guia explica esse mapeamento desde o início: o que são codificações de caracteres, como Unicode e UTF-8 funcionam, e como diagnosticar e corrigir os bugs mais comuns do mundo real.
A Era Pré-Unicode: ASCII e o Caos do ASCII Estendido
Todo caractere armazenado em um computador é fundamentalmente um número. Uma codificação de caracteres é a tabela de pesquisa que mapeia números para caracteres e vice-versa.
A primeira codificação amplamente adotada foi o ASCII (American Standard Code for Information Interchange), publicado em 1963. O ASCII usa 7 bits, dando-lhe 128 posições (0–127). Isso cobre o alfabeto inglês em maiúsculas e minúsculas, os dez dígitos, pontuação comum e 33 caracteres de controle não imprimíveis como nova linha e tabulação.
128 caracteres são suficientes para o inglês. Está longe de ser suficiente para o francês, alemão, espanhol, árabe, chinês, japonês ou os milhares de outras línguas escritas no mundo.
O Problema do ASCII Estendido
O 8.º bit em um byte estava disponível, o que significava 128 posições adicionais (128–255). Diferentes organizações e sistemas operacionais correram para preencher essas posições de maneiras diferentes:
- Windows-1252 (também chamado CP1252): Usado pelo Microsoft Windows na Europa Ocidental. Coloca caracteres como
é,ñe€na metade superior. - ISO-8859-1 (Latin-1): Um padrão internacional para línguas da Europa Ocidental. Sobrepõe-se significativamente com Windows-1252, mas não é idêntico — a faixa 128–159 são caracteres de controle em Latin-1, mas caracteres imprimíveis em Windows-1252.
- ISO-8859-5: Cobre o cirílico.
- ISO-8859-6: Cobre o árabe.
- Shift JIS, EUC-JP, Big5: Esquemas multibyte completamente diferentes para línguas do Leste Asiático.
O resultado foi que um arquivo criado em um sistema seria exibido como lixo em outro, a menos que ambos concordassem com a mesma codificação — e não havia maneira confiável de sinalizar qual codificação um arquivo usava. Os softwares tentavam adivinhar e erravam constantemente.
Esse é o problema que o Unicode foi criado para resolver.
Unicode: Um Conjunto de Caracteres para Todas as Línguas
Unicode não é uma codificação. É um conjunto de caracteres — uma enorme tabela de pesquisa que atribui um número único a cada caractere em cada sistema de escrita em uso, mais milhares de símbolos, emojis e escritas históricas.
Cada entrada nessa tabela é chamada de ponto de código, escrito como U+ seguido de um número hexadecimal. Alguns exemplos:
O padrão Unicode atualmente define mais de 140.000 caracteres e é atualizado regularmente.
Planos Unicode
Os pontos de código Unicode vão de U+0000 a U+10FFFF, dando pouco mais de 1,1 milhão de posições possíveis. Esse espaço é dividido em 17 planos de 65.536 pontos de código cada.
- Plano 0 — Plano Multilíngue Básico (BMP): U+0000 a U+FFFF. Contém praticamente todos os caracteres de uso comum hoje: latino, grego, cirílico, árabe, hebraico, CJK e mais.
- Plano 1 — Plano Multilíngue Suplementar (SMP): U+10000 a U+1FFFF. Emojis, escritas históricas, notação musical.
- Planos 2–16: Ideógrafos CJK unificados adicionais, escritas históricas raras e espaço não atribuído.
Os emojis que quebram tabelas MySQL vivem no Plano 1. Esse detalhe importa, como veremos em breve.
UTF-8: Como o Unicode é Realmente Armazenado
O Unicode define qual número cada caractere recebe. Não especifica como armazenar esses números como bytes no disco ou na memória. Esse é o trabalho de uma codificação Unicode.
UTF-8 é de longe a codificação Unicode mais amplamente usada na web e em softwares modernos. É de largura variável, o que significa que caracteres diferentes ocupam diferentes números de bytes:
Como as Sequências de Bytes UTF-8 Funcionam
UTF-8 é auto-sincronizante. O primeiro byte de qualquer caractere indica quantos bytes se seguem:
- 0xxxxxxx — Caractere de 1 byte (ASCII). Qualquer arquivo ASCII válido é UTF-8 válido.
- 110xxxxx 10xxxxxx — Caractere de 2 bytes.
- 1110xxxx 10xxxxxx 10xxxxxx — Caractere de 3 bytes.
- 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx — Caractere de 4 bytes.
Bytes de continuação sempre começam com 10, então você pode imediatamente dizer se qualquer byte dado é o início de um novo caractere ou o meio de um. Isso torna possível resincronizar após ler um stream parcial.
Exemplo concreto — codificando é (U+00E9):
U+00E9 em binário é 11101001. Isso cai na faixa de 2 bytes. O modelo 110xxxxx 10xxxxxx produz 11000011 10101001 — bytes 0xC3 0xA9 — que é exatamente o que UTF-8 armazena para é.
Por que UTF-8 Domina
UTF-8 tem três propriedades que o fizeram vencer:
- Compatibilidade com ASCII. Qualquer arquivo somente ASCII é UTF-8 válido. Migrar sistemas existentes não exigia tocar em arquivos sem conteúdo não-ASCII.
- Eficiência de espaço para scripts latinos. Texto em inglês usa um byte por caractere, como ASCII. Apenas caracteres fora do ASCII custam mais bytes.
- Sem ambiguidade de ordem de bytes. A estrutura de bytes é inequívoca sem nenhum cabeçalho. (Compare com UTF-16, que requer uma marca de ordem de bytes.)
UTF-16 e UTF-32: Quando Você os Encontrará
UTF-8 não é a única codificação Unicode, e alguns ambientes usam outras.
UTF-16 codifica caracteres BMP como dois bytes (uma unidade de código de 16 bits) e caracteres do plano suplementar como quatro bytes (duas unidades de código de 16 bits, chamadas de um par substituto). Os internos do Windows, strings Java, strings JavaScript (no nível do motor) e strings .NET usam UTF-16 internamente.
UTF-32 usa exatamente quatro bytes por caractere, sempre. Desperdiça espaço, mas torna o acesso aleatório por índice de caractere trivialmente rápido. Alguns internos de banco de dados o usam.
A Marca de Ordem de Bytes (BOM)
UTF-16 tem um problema de endianness. Uma sequência de dois bytes pode ser armazenada como big-endian (0x00 0x41 para A) ou little-endian (0x41 0x00). Um receptor precisa saber qual ordem foi usada.
A solução é a Marca de Ordem de Bytes: ponto de código U+FEFF colocado no início do arquivo. Em UTF-16 big-endian, aparece como os bytes FE FF. Em UTF-16 little-endian, aparece como FF FE. Um leitor que vê um ou outro conhece a ordem dos bytes.
UTF-8 tecnicamente também tem um BOM (EF BB BF), mas ele é sem sentido para UTF-8 porque UTF-8 não tem ambiguidade de ordem de bytes. É aqui que causa problemas:
- Excel e arquivos CSV. Excel usa o BOM UTF-8 como sinal para interpretar um CSV como UTF-8. Sem ele, o Excel usa como padrão a codificação do locale do sistema (geralmente Windows-1252), distorcendo caracteres acentuados. Muitos desenvolvedores adicionam o BOM especificamente para que o Excel funcione corretamente.
- Scripts shell. Um script shell que começa com
#!/bin/bashdeve ter essa sequência como os primeiros bytes. Se um BOM UTF-8 preceder o shebang, o kernel vêEF BB BF 23 21e não reconhece o shebang, fazendo o script falhar ou executar com o interpretador errado.
A regra geral: adicione o BOM para arquivos CSV destinados ao Excel, nunca o adicione a scripts shell ou arquivos JSON.
Bugs de Codificação Comuns no Mundo Real
Mojibake: Lendo UTF-8 como Latin-1
Mojibake (文字化け) é o termo japonês para texto embaralhado causado por uma incompatibilidade de codificação. A variante mais comum na web moderna: uma string UTF-8 é lida como se fosse Latin-1 (ISO-8859-1 ou Windows-1252).
O caractere é é U+00E9. Em UTF-8, é armazenado como os dois bytes 0xC3 0xA9. Se um programa lê esses dois bytes como Latin-1, vê dois caracteres separados: 0xC3 é à e 0xA9 é ©. A string café se torna café.
Esse padrão específico de corrupção — é para é, è para è, à para à — é o sinal inconfundível de UTF-8 sendo interpretado como Latin-1. Você o verá mais frequentemente em:
- Bancos de dados onde o charset da conexão não foi definido como UTF-8 na inserção.
- Emails enviados por servidores de email mais antigos.
- Arquivos copiados entre sistemas com configurações de locale incompatíveis.
A correção é identificar onde a incompatibilidade ocorre e corrigir a declaração de codificação nessa camada — não corrigir a string corrompida depois do fato.
O Caractere de Substituição (U+FFFD)
Quando um decodificador encontra uma sequência de bytes que não é válida na codificação esperada, precisa fazer algo. O padrão Unicode recomenda substituir o caractere de substituição U+FFFD, exibido como <0xFFFD>. Se você vir caixas ? ou o caractere <0xFFFD> na saída, um decodificador encontrou bytes que não pôde interpretar.
Causas comuns:
- Truncar uma sequência UTF-8 multibyte. Se você cortar uma string em um limite de byte que cai dentro de um caractere CJK de 3 bytes, a sequência parcial é inválida.
- Passar um arquivo codificado em Latin-1 para um decodificador UTF-8 estrito.
- Receber dados de uma fonte que mistura codificações.
O utf8 vs utf8mb4 do MySQL: O Problema do Emoji
Este é um dos bugs de codificação mais conhecidos no desenvolvimento web. O MySQL tem dois conjuntos de caracteres chamados utf8 e utf8mb4.
O utf8 do MySQL não é UTF-8 real. Suporta apenas pontos de código até U+FFFF — exatamente o Plano Multilíngue Básico — usando no máximo três bytes por caractere. Emojis vivem no Plano 1 (U+10000 e acima) e exigem quatro bytes. Quando você tenta inserir um caractere UTF-8 de quatro bytes em uma coluna definida como utf8, o MySQL silenciosamente trunca o valor ou lança um erro.
utf8mb4 é UTF-8 real com suporte completo de quatro bytes. Foi introduzido especificamente para lidar com emojis e outros caracteres do Plano Suplementar.
A abordagem correta: sempre use utf8mb4 para novas tabelas e bancos de dados. Também defina o charset e a collation da conexão explicitamente:
ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
ALTER TABLE posts CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;E defina o parâmetro de conexão na configuração da sua aplicação:
charset=utf8mb4
Usar utf8 no MySQL é uma armadilha legada. Trate-o como obsoleto.
Comprimento de String vs. Contagem de Caracteres em Python
Python 2 armazenava strings como sequências de bytes por padrão. len("café") retornava 5 para uma string UTF-8 — porque é ocupa dois bytes — não 4. O tratamento correto de multibyte exigia literais unicode explícitos u"" e disciplina cuidadosa de encode/decode.
Python 3 corrigiu isso: str é uma sequência de pontos de código Unicode, então len("café") retorna 4. Os dados de bytes foram movidos para o tipo bytes com chamadas explícitas .encode() e .decode().
Uma sutileza permanece. O len() do Python 3 conta pontos de código, não clusters de grafemas. O caractere é pode ser representado como um único ponto de código (U+00E9) ou como e (U+0065) mais um acento agudo combinante (U+0301). Ambos renderizam identicamente, mas produzem valores de len() diferentes. Aplicações multilíngues que expõem uma contagem de caracteres aos usuários devem usar uma biblioteca de segmentação Unicode em vez de len() direto.
Quando você inspeciona dados JSON contendo strings multilíngues, pode notar isso — o que parece um único caractere pode ser internamente dois pontos de código.
Tabela de Comparação de Codificações
Como Detectar e Corrigir Problemas de Codificação
Detecção
Em Python:
import chardet
with open("mystery_file.txt", "rb") as f:
raw = f.read()
result = chardet.detect(raw)
print(result) # {'encoding': 'Windows-1252', 'confidence': 0.73, 'language': ''}A biblioteca chardet usa análise estatística para adivinhar a codificação a partir de padrões de bytes. A pontuação de confiança é honesta — a detecção de codificação é heurística, não determinística.
No navegador: Abra o DevTools, vá para a aba Network, encontre a resposta de uma página HTML e verifique o cabeçalho Content-Type. Deve ler text/html; charset=utf-8. Se estiver ausente ou incorreto, o navegador vai adivinhar, frequentemente incorretamente.
Em um banco de dados: Verifique o conjunto de caracteres nos níveis de banco de dados, tabela e coluna:
SHOW VARIABLES LIKE 'character_set%';
SHOW CREATE TABLE posts;Corrigindo um Banco de Dados com Mojibake
Se os dados foram inseridos como UTF-8, mas armazenados como Latin-1 (um erro de migração comum), às vezes é possível recuperá-los:
-- Lê os dados corrompidos como binário, depois interpreta como UTF-8
UPDATE posts
SET body = CONVERT(CAST(CONVERT(body USING latin1) AS BINARY) USING utf8mb4)
WHERE body REGEXP '[Ã]';Isso só funciona se os bytes originais estiverem intactos e foram apenas mal rotulados. Se os dados estavam verdadeiramente corrompidos — bytes foram descartados ou substituídos — a recuperação não é possível sem um backup.
Prevenção
- Sempre declare a codificação explicitamente. Nunca confie nos padrões.
- Defina
<meta charset="UTF-8">em cada documento HTML. - Defina
Content-Type: application/json; charset=utf-8nas respostas de API. - Especifique o charset da conexão ao abrir conexões de banco de dados.
- Use UTF-8 em seu editor e configure seu sistema de controle de versão para impô-lo em arquivos de texto.
Para tarefas de codificação além de conjuntos de caracteres — como converter dados binários para representações seguras para texto — a ferramenta codificar e decodificar texto do FileCrank lida com a conversão Base64 sem nenhuma configuração. O artigo de codificação existente cobre Base64, codificação URL e entidades HTML se você precisar. Se estiver trabalhando com arquivos de configuração e precisar verificar que strings multilíngues parecem corretas, você pode inspecionar dados JSON diretamente no navegador.
O que Levar Consigo
Bugs de codificação de caracteres parecem misteriosos porque o modo de falha — caracteres distorcidos ou ausentes — aparece longe da causa real. Mas o mecanismo subjacente é simples: dois programas estão usando tabelas de pesquisa diferentes para converter bytes em caracteres.
Mantenha estas três regras em mente:
- Sempre use UTF-8 em todo lugar a menos que um sistema específico force outro. Defina explicitamente; nunca confie nos padrões.
- No MySQL, use
utf8mb4, nãoutf8. A diferença importa no momento em que um usuário envia um emoji. - Quando você vir mojibake, encontre o limite. Procure onde os bytes cruzam um sistema que não conhece sua codificação. Corrija a declaração nesse ponto, não os sintomas mais adiante.
Entender a mecânica — pontos de código, sequências de bytes, declarações de codificação — significa que você pode raciocinar sobre um novo bug de codificação sistematicamente em vez de adivinhar. Na próxima vez que você vir é em sua saída, saberá imediatamente o que aconteceu e onde procurar.
Todas as ferramentas do FileCrank são executadas completamente no seu navegador. Nenhum arquivo é enviado a um servidor. Se você estiver trabalhando com texto que contém conteúdo multilíngue, experimente o Visualizador de JSON ou a ferramenta de Codificação/Decodificação Base64 — ambas lidam com Unicode corretamente sem nenhuma configuração.