UTF-8 é um projeto brilhante
(iamvishnu.com)- UTF-8 é um método de codificação de comprimento variável que representa milhões de caracteres mantendo a compatibilidade retroativa com ASCII
- A mesma faixa de 7 bits do ASCII (
U+0000~U+007F) usa 1 byte sem alteração, então um arquivo ASCII já é um arquivo UTF-8 válido - Os demais caracteres são representados em sequências de 2 a 4 bytes, e o padrão de bits do byte inicial define o comprimento, enquanto os bytes seguintes começam com
10para indicar que são bytes de continuação - Graças a esse design, o UTF-8 lida com um conjunto universal de caracteres e ao mesmo tempo é totalmente compatível com sistemas ASCII existentes, tornando-se a codificação de caracteres mais usada
- Outras codificações Unicode, como UTF-16 e UTF-32, não oferecem essa compatibilidade com ASCII
A excelência do design do UTF-8
- Quando tive meu primeiro contato com a codificação UTF-8, fiquei muito impressionado com a estrutura compatível com o ASCII existente enquanto ela abrange milhões de caracteres de diferentes idiomas e sistemas de escrita em um único esquema
- Em essência, o UTF-8 utiliza até 32 bits, mas o ASCII usa apenas 7 bits
- Os princípios de design do UTF-8 são os seguintes
- Todo arquivo codificado em ASCII é um arquivo UTF-8 válido
- Todo arquivo UTF-8 que contenha apenas caracteres ASCII é um arquivo ASCII válido
- A ideia de integrar um sistema antigo limitado a apenas 128 caracteres com um esquema que abrange milhões de caracteres é extremamente inovadora
Conceito básico do UTF-8
- UTF-8 é uma codificação de caracteres de comprimento variável (variable-width encoding) projetada para representar todos os caracteres do conjunto de caracteres Unicode
- Cada caractere é codificado em 1 a 4 bytes
- Os primeiros 128 caracteres (
U+0000~U+007F) são armazenados em um único byte, garantindo compatibilidade retroativa com ASCII - Os demais caracteres são codificados em dois, três ou quatro bytes
- Os bits iniciais do primeiro byte determinam o número total de bytes necessário para a codificação
| Padrão de 1 byte | Número de bytes | Padrão da sequência completa de bytes |
|---|---|---|
| 0xxxxxxx | 1 | 0xxxxxxx (ASCII comum) |
| 110xxxxx | 2 | 110xxxxx 10xxxxxx |
| 1110xxxx | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
| 11110xxx | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
- O 2º, 3º e 4º bytes das sequências multibyte sempre começam com
10, o que marca claramente que são bytes de continuação - Ao combinar os bits restantes do byte principal e dos bytes de continuação, forma-se um code point
- Um code point é um identificador único de caractere Unicode, expresso com o prefixo "U+" e em hexadecimal
- Ex.: o code point de "A" é
U+0041
- O fluxo para interpretar um caractere a partir dos bytes codificados em UTF-8 é o seguinte
- 1. Leia o byte; se ele começar com 0, trate-o como um caractere de um único byte (ASCII), use os 7 bits restantes para representar o caractere e avance para o próximo byte
- 2. Se não começar com 0, então
- Se for 110, leia mais um byte para formar um caractere de 2 bytes
- Se for 1110, leia os próximos 2 bytes para formar um caractere de 3 bytes
- Se for 11110, leia mais 3 bytes para formar um caractere de 4 bytes
- 3. Nos bytes determinados, combine os bits restantes, excluindo os bits iniciais, e use o resultado como o valor binário do code point
- 4. Encontre o code point no conjunto de caracteres Unicode e exiba-o na tela
- 5. Repita com o próximo byte
Exemplo: o caractere em hindi "अ"
- Representação em UTF-8:
11100000 10100100 10000101(3 bytes) - Primeiro byte (
11100000) → indica que é um caractere de 3 bytes - Combinação dos bits válidos dos três bytes →
00001001 00000101= hexadecimal0x0905 - O code point
U+0905representa o caractere devanágari "अ"
Exemplos de arquivo
-
1.
Hey👋 Buddy- Composto por 13 bytes no total
- Caracteres ASCII (H, e, y, B, u, d, d, y, espaço) → 1 byte cada
- 👋 (U+1F44B) → 4 bytes
11110000 10011111 10010001 10001011
- Este arquivo é um arquivo UTF-8 válido, mas como contém um caractere não ASCII (emoji), não é retrocompatível com ASCII
- Composto por 13 bytes no total
-
2.
Hey Buddy- Total de 9 bytes, todos dentro da faixa ASCII
- Portanto, este arquivo é ao mesmo tempo um arquivo ASCII válido e um arquivo UTF-8 válido
Comparação com outras codificações
- Existem algumas codificações que oferecem compatibilidade com ASCII, mas não são tão usadas quanto o UTF-8
- GB18030 (padrão chinês) etc. também oferecem compatibilidade com ASCII, mas não são amplamente usados
- A família ISO/IEC 8859 é uma extensão de byte único (máximo de 256 caracteres), portanto tem limitações
- UTF-16/UTF-32 não têm compatibilidade com ASCII
- 'A' (U+0041): em UTF-16 é
00 41, em UTF-32 é00 00 00 41
- 'A' (U+0041): em UTF-16 é
Bônus: UTF-8 Playground
- Uma ferramenta interativa para explorar visualmente o processo de codificação em UTF-8
- https://utf8-playground.netlify.app/
1 comentários
Comentários do Hacker News
Como os bytes de continuação em UTF-8 sempre começam com
10, mesmo que você salte para um byte arbitrário dá para verificar imediatamente se aquela posição é o início de um caractere ou um byte de continuação, então é fácil encontrar o próximo ou o anterior início de caractere. Se fosse uma codificação como a de inteiros de tamanho variável do EBML (com inversão de 1/0 para manter compatibilidade com ASCII de byte único), seria difícil saber de imediato, a partir de uma posição arbitrária, onde começa o caractere. Veja mais em RFC8794 section 4.4Exato, essa é uma grande vantagem do UTF-8. Dá para navegar livremente para frente e para trás em uma string UTF-8 sem precisar ler desde o começo. No caso do Python, para permitir indexação de strings por caractere, o CPython usa wide characters. Houve uma época em que era possível escolher caracteres de 2 ou 4 bytes, e depois isso passou a alternar automaticamente em tempo de execução. Mas ainda assim são wide characters, não UTF-8. Por exemplo, um único emoji pode quadruplicar o tamanho da string. Eu preferiria usar UTF-8 internamente e tornar o tipo de índice um objeto opaco, de modo que somar ou subtrair pequenos inteiros movesse para frente ou para trás dentro da string. A conversão para inteiro ou o subscrito direto é que calculariam o índice na string. Com essa abordagem, regex etc. também poderiam usar objetos de índice opacos e funcionar bem sobre a representação UTF-8
Acho que LEB128/VLQ é melhor que o esquema de inteiros de tamanho variável do EBML. A distinção é feita pelo MSB do byte:
0significa fim da sequência, e o próximo byte inicia uma nova sequência;1significa continuar voltando até encontrar um MSB0. Há implementações eficientes otimizadas com SIMD. A diferença entre LEB128 e VLQ é só endianness. ASCII seria0xxxxxxx, caracteres estendidos seriam1xxxxxxx 0xxxxxxx,1xxxxxxx 1xxxxxxx 0xxxxxxxetc., com até0x1FFFFFem 3 bytes, mais do que o Unicode precisa. Não é auto-sincronizável, mas é mais compacto. ASCII continua com 1 byte, e pontos de código abaixo de U+3FFF, como símbolos matemáticos ou japonês, poderiam ser representados em 2 bytes, o que ajuda a reduzir o tamanho do códigoAcho que isso só vale sob a premissa de que o texto não foi corrompido nem adulterado de forma maliciosa. Já houve inúmeras vulnerabilidades de segurança ao fazer parsing ou escaping de sequências UTF-8 inválidas. Veja, por exemplo, o problema CVE-2025-1094 no PostgreSQL e também a lista de CVEs relacionados a UTF-8
Isso não é necessariamente verdade. Em UTF-8 inválido, um byte de continuação também pode virar um caractere. Por exemplo, se entrar
0b01100001 0b10000000 0b01100001, saem três caracteres:a�a. Para saber se um caractere exibido começa ali, é preciso olhar os 1 a 3 bytes anterioresSe o tamanho máximo multibyte é de 4 bytes, basta olhar no máximo 3 bytes para trás para saber se a posição atual é um byte de continuação. Se não aparecer um byte inicial, então você sabe que é um caractere de um único byte. Suspeito que isso tenha sido projetado com fins de recuperação: mesmo que a biblioteca não reconheça UTF-8 corretamente, ela pode ignorar bytes inválidos no começo e no fim de um slice cortado e ainda extrair uma string minimamente razoável
Acho o UTF-8 realmente brilhante. A chave está na decisão de o ASCII usar apenas 7 bits. Mesmo em 1963, escolher 7 bits já era um pouco peculiar. Fico curioso se isso foi só um acaso histórico, se os projetistas do ASCII pensaram em usar mais um bit para incluir símbolos extras, ou se já estavam pensando em code pages e extensibilidade
Não sei o motivo exato, mas antigamente 8 bits nem sempre estavam disponíveis. Era comum usar 7 bits + 1 bit de paridade ou de flag (por isso o e-mail ainda usa quoted-printable para codificar 8 bits usando apenas 7 bits). Quando algo consegue transportar os 8 bits intactos, chama-se 8-bit clean. Nesse contexto, o UTF-8 acaba sendo um ótimo aproveitamento desse oitavo bit que sobrava no ASCII. Também vale ver a explicação sobre 8-bit clean
Não sou especialista, mas já li sobre a história do ASCII. O ASCII tem raízes nos códigos de teletipo, que por sua vez evoluíram dos códigos telegráficos. O código Morse era de comprimento variável, o que dificultava a implementação mecânica. Daí surgiu o código Baudot de 5 bits. A ideia era simplificar as máquinas com um código de tamanho fixo e também reduzir a fadiga dos operadores. É por causa do Baudot que ainda chamamos a taxa de símbolos de baud. Depois, com a entrada por fita perfurada usando máquinas de escrever, houve mais flexibilidade, e símbolos especiais como Carriage Return e Line Feed foram adicionados. A indústria de computadores inicial adotou cartões perfurados como entrada, e a IBM desenvolveu um novo sistema de 8 bits para processar cartões mais rapidamente, que acabou sendo baseado em ASCII. No fim, os códigos binários foram sendo expandidos conforme a tecnologia avançava. O ASCII é um produto transitório, anterior até à convenção do byte de 8 bits
Na prática, o bit que sobrava era reaproveitado para paridade
A extensão de 8 bits do ASCII (família ISO 8859-x) foi amplamente usada por décadas e ainda é usada nas code pages padrão do Windows. Mesmo que o ASCII tivesse sido de 8 bits desde o começo, os caracteres centrais provavelmente ainda ficariam nos primeiros 128, então continuaria adequado ao UTF-8. Se houve um acaso histórico, não foi o ASCII ser de 7 bits, mas o fato de o desenvolvimento da computação naquela época ocorrer majoritariamente no mundo anglófono, e o inglês caber bem em 7 bits
Sete bits em si não é algo tão estranho. O Baudot era de 5 bits, isso ficou pequeno e surgiram códigos de 6 bits, e depois veio o ASCII de 7 bits. A IBM padronizou o byte de 8 bits no System/360 (com código EBCDIC), mas outros fabricantes de computadores não tinham comprimento fixo de byte. Pode parecer estranho hoje, mas caracteres e palavras de máquina não necessariamente se encaixavam de forma certinha
Concordo que o UTF-8 é ainda melhor projetado do que se espera. Mas o Unicode tem um problema de escopo amplo demais. Fica a dúvida sobre o que deveria ser incluído no Unicode. Intuitivamente, parece que seria “todos os caracteres impressos distintos usados pela humanidade para comunicação”, mas na prática não é isso.
A distinção não é clara. Há pontos de código que existem para combinação
Não é específico. Um mesmo caractere pode ser escrito de várias formas. Caracteres visualmente iguais podem ter pontos de código e significados diferentes
Nem tudo é imprimível. Existem caracteres de controle. Entraram por compatibilidade com ASCII, mas também foram surgindo caracteres de controle próprios Ainda não parece haver pontos Unicode animados. Pelo menos o que é imprimível pode ser colocado no papel. Mas não sei se essa invariância vai durar no futuro. A propósito, entre as codificações UTF que o autor não mencionou está o UTF-7. Ele é parecido com o UTF-8, mas foi criado sob a suposição de que, nos ambientes de rede dos anos 80, usar o último bit não era seguro. Já recebi um e-mail codificado em UTF-7 por acaso. Até hoje não sei como aquilo foi enviado
O UTF-7 foi criado principalmente para ambientes de transmissão não 8-bit clean, como e-mail. Hoje está obsoleto e nem codifica supplemental planes de forma nativa (só por surrogate pairs do UTF-16). Existe até UTF-9, mas é uma paródia apresentada em um RFC de 1º de abril, para ambientes de 36 bits como o PDP-10
Sempre tive uma dúvida: é possível codificar um ponto de código Unicode com sequências de bytes desnecessariamente longas. O UTF-8 proíbe isso e só permite a sequência mais curta. Por exemplo,
00000001e11000000 10000001representariam a mesma coisa. Então não daria para projetar isso de outro jeito, de modo que simplesmente não existissem codificações ilegais? Por exemplo, fazendo o início de uma sequência de 2 bytes corresponder ao último valor válido, de modo que11000000 10000001passasse a significar128+1, e0-127continuasse em 1 byte. Aí não haveria códigos ilegais, e em casos de borda a string ficaria um pouco menor. Fico pensando se isso não foi considerado por causa do custo de hardware da época. (Atualização: a sequência de bits correta é10000001, corrigi)c2 80e nãoc0 80(o primeiro valor acima de7f). Acho que os motivos são estes: a) Se overlong encoding fosse permitido, isso abriria brechas de segurança em casos em que alguém verificasse apenas sequências curtas b) A codificação/decodificação padrão de UTF-8 pode ser feita só com masking e bitshift. O esquema proposto exigiria também subtração Isso foi discutido em um debate por e-mail em 1992, e no FSS-UTF havia constantes aditivas (veja abaixo)O ponto central é preservar a self-synchronicity dos padrões de bytes. Se você não mantiver bytes de continuação como
10, como em11000000 10000001, perde a propriedade de conseguir encontrar sempre as fronteiras entre pontos de código em um fluxo UTF-8 truncado. Se ainda acrescentar operações de soma e subtração, o decodificador fica mais lento. Hoje dá para fazer tudo só com operações de bitComo comentou quectophoton, os bytes de continuação precisam sempre começar com
10para que o parser consiga encontrar fronteiras de pontos de código a partir de qualquer posição. Isso foi levado em conta no projeto do UTF-8 no início dos anos 90, quando havia muitos ambientes de transmissão não confiáveisCom o esquema proposto, os cálculos de codificação e decodificação ficariam mais complexos e lentos. Hoje basta fazer alguns shifts de bits, mas isso era especialmente importante na época, com os computadores mais lentos dos anos 90
Se quiser ler mais sobre o projeto do UTF-8, veja o one-pager do Russ Cox e o resumo histórico do Rob Pike
O UTF-8 é ótimo, e seria excelente se fosse usado em todo lugar (estou olhando para você, JavaScript). Mas a única desvantagem real é que o padrão não deixa totalmente claro como interpretar sequências de bytes inválidas. Um projeto que “definisse obrigatoriamente uma interpretação para toda sequência de bytes” teria sido ainda mais perfeito. Acho que algo como a especificação do HTML5 mostra que isso pode funcionar bem na prática
Tenho uma relação de amor e ódio com retrocompatibilidade. Não gosto da confusão, mas vejo com bons olhos quando há disposição de avançar mesmo quebrando algumas coisas. Ao mesmo tempo, acho muito satisfatórios exemplos como UTF-8 ou EAN, que preservam compatibilidade e ainda assim foram projetados com inteligência. Sinceramente, o UTF-8 parece não ter sacrificado quase nada em nome da compatibilidade
Se eu fosse mudar alguma coisa, talvez trocasse alguns caracteres de controle por caracteres mais comuns para economizar um pouco de espaço (assumindo até quebrar compatibilidade com Unicode). Mas, como formato de codificação multibyte, por si só ele já me parece quase ótimo
Gostei muito do link do playground de UTF-8 (utf8-playground.netlify.app). Seria bom se a UI também permitisse inserir pontos de código diretamente (por enquanto parecia possível só pela URL). (Atualização: isso já foi resolvido; o PR foi aceito)
Se você quiser se aprofundar mais nesse tema e gosta do estilo Advent of Code, o i18n-puzzles tem vários quebra-cabeças sobre codificação de texto. Ajuda bastante a internalizar completamente como UTF-8, UTF-16 etc. funcionam
Gostei muito do texto. Eu também recomendo UTF-8, mas acho que ele só é realmente bom quando usado com BOM. Caso contrário, o aplicativo não tem como saber que é UTF-8 e pode nem perceber que deve salvar em UTF-8. Por exemplo, no Windows, se você cria um novo arquivo de texto e ele contém apenas um BOM enquanto está vazio, qualquer aplicativo consegue detectar automaticamente que deve editar e salvar depois como UTF-8. Sem BOM, mesmo que o aplicativo tente detectar a codificação automaticamente, isso nunca é totalmente confiável, e a confusão aumenta quando você adiciona caracteres especiais como acentos (o editor pode inferir a língua errada, ou o Notepad pode mudar a codificação padrão após uma atualização). Então concordo em usar UTF-8, mas o BOM deveria obrigatoriamente ser o padrão no sistema operacional e nos aplicativos