34 pontos por GN⁺ 2025-09-13 | 1 comentários | Compartilhar no WhatsApp
  • 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 10 para 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 = hexadecimal 0x0905
  • O code point U+0905 representa 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
  • 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

Bônus: UTF-8 Playground

1 comentários

 
GN⁺ 2025-09-13
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.4

    • Exato, 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: 0 significa fim da sequência, e o próximo byte inicia uma nova sequência; 1 significa continuar voltando até encontrar um MSB 0. Há implementações eficientes otimizadas com SIMD. A diferença entre LEB128 e VLQ é só endianness. ASCII seria 0xxxxxxx, caracteres estendidos seriam 1xxxxxxx 0xxxxxxx, 1xxxxxxx 1xxxxxxx 0xxxxxxx etc., com até 0x1FFFFF em 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ódigo

    • Acho 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 anteriores

    • Se 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, 00000001 e 11000000 10000001 representariam 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 que 11000000 10000001 passasse a significar 128+1, e 0-127 continuasse 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)

    • Várias respostas mencionam o marcador de sincronização, mas a pergunta essencial é por que U+0080 é c2 80 e não c0 80 (o primeiro valor acima de 7f). 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)

    Uma sequência de 2 bytes pode carregar 2^11 códigos, dos quais 0-7f são ilegais. Parece que consideraram isso melhor do que constantes aditivas sem um benefício especial
    Veja o fim de utf-8-history.txt para mais detalhes

    • O ponto central é preservar a self-synchronicity dos padrões de bytes. Se você não mantiver bytes de continuação como 10, como em 11000000 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 bit

    • Como comentou quectophoton, os bytes de continuação precisam sempre começar com 10 para 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áveis

    • Com 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

    • Do ponto de vista de segurança, UTF-8 inválido não deve ser tratado; os dados em si devem ser descartados imediatamente como material perigoso, com tratamento de erro. Caso contrário, você fica exposto a ataques por bypass de validação
  • 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

    • o UTF-8 parece não ter sacrificado quase nada em nome da compatibilidade
      A codificação acima de 21 bits foi bloqueada. Isso foi por compatibilidade com UTF-16 (o mecanismo de surrogates do UTF-16 só vai até 2^21-1). Talvez um dia nos arrependamos desse limite. Não parece haver outro motivo prático relevante para impedir pontos de código acima de 21 bits

    • gosto quando alguém no poder muda algo com ousadia em nome do progresso
      Mas não é nada divertido quando um sistema do qual você depende quebra só porque alguém renomeou um parâmetro ou achou que parte da biblioteca padrão parecia “bagunçada”

    • 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)

    • Obrigado pela contribuição, já foi integrado e está no ar
  • 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