Guia de codificação de caracteres

Guia de codificação de caracteres

1. Visão geral

Neste tutorial, discutiremos os fundamentos da codificação de caracteres e como lidamos com isso em Java.

2. Importância da codificação de caracteres

Muitas vezes temos que lidar com textos pertencentes a vários idiomas com diversos scripts de escrita, como latim ou árabe. Todo caractere em qualquer idioma precisa, de alguma forma, ser mapeado para um conjunto de zeros e uns. Realmente, é uma maravilha que os computadores possam processar todos os nossos idiomas corretamente.

Para fazer isso corretamente,we need to think about character encoding. Não fazer isso pode levar à perda de dados e até vulnerabilidades de segurança.

Para entender isso melhor, vamos definir um método para decodificar um texto em Java:

String decodeText(String input, String encoding) throws IOException {
    return
      new BufferedReader(
        new InputStreamReader(
          new ByteArrayInputStream(input.getBytes()),
          Charset.forName(encoding)))
        .readLine();
}

Observe que o texto de entrada que alimentamos aqui usa a codificação da plataforma padrão.

If we run this method with input as “The façade pattern is a software design pattern.” and encoding as “US-ASCII”, a saída será:

The fa��ade pattern is a software design pattern.

Bem, não exatamente o que esperávamos.

O que poderia ter dado errado? Tentaremos entender e corrigir isso no restante deste tutorial.

3. Fundamentos

Antes de nos aprofundarmos, porém, vamos revisar rapidamente três termos:encoding,charsets ecode point.

3.1. Codificação

Os computadores só podem entender representações binárias como1e0. O processamento de qualquer outra coisa requer algum tipo de mapeamento do texto do mundo real para sua representação binária. This mapping is what we know as character encoding or simply just as encoding.

Por exemplo, a primeira letra da nossa mensagem, “T”, em US-ASCIIencodes to “01010100”.

3.2. Charsets

O mapeamento de caracteres para suas representações binárias pode variar bastante em termos dos caracteres que eles incluem. O número de caracteres incluídos em um mapeamento pode variar de apenas alguns para todos os caracteres em uso prático. The set of characters that are included in a mapping definition is formally called a charset.

3.3. ponto de código

Um ponto de código é uma abstração que separa um caractere de sua codificação real. A code point is an integer reference to a particular character.

Podemos representar o próprio inteiro em bases decimais simples ou bases alternativas como hexadecimal ou octal. Usamos bases alternativas para facilitar a referência de grandes números.

Por exemplo, a primeira letra da nossa mensagem, T, em Unicode, possui um ponto de código "U + 0054" (ou 84 em decimal).

4. Compreendendo os esquemas de codificação

Uma codificação de caracteres pode assumir várias formas, dependendo do número de caracteres que codifica.

O número de caracteres codificados tem uma relação direta com o comprimento de cada representação, que normalmente é medido como o número de bytes. Having more characters to encode essentially means needing lengthier binary representations.

Vamos examinar alguns dos esquemas de codificação populares na prática hoje.

4.1. Codificação de Byte Único

Um dos primeiros esquemas de codificação, chamado ASCII (Código Padrão Americano para Troca de Informações), usa um esquema de codificação de byte único. Isso significa essencialmente queeach character in ASCII is represented with seven-bit binary numbers. Isso ainda deixa um bit livre em cada byte!

O conjunto de 128 caracteres ASCII abrange alfabetos ingleses em maiúsculas e minúsculas, dígitos e alguns caracteres especiais e de controle.

Vamos definir um método simples em Java para exibir a representação binária de um caractere em um esquema de codificação específico:

String convertToBinary(String input, String encoding)
      throws UnsupportedEncodingException {
    byte[] encoded_input = Charset.forName(encoding)
      .encode(input)
      .array();
    return IntStream.range(0, encoded_input.length)
        .map(i -> encoded_input[i])
        .mapToObj(e -> Integer.toBinaryString(e ^ 255))
        .map(e -> String.format("%1$" + Byte.SIZE + "s", e).replace(" ", "0"))
        .collect(Collectors.joining(" "));
}

Agora, o caractere ‘T 'possui um ponto de código 84 em US-ASCII (ASCII é referido como US-ASCII em Java).

E se usarmos nosso método utilitário, podemos ver sua representação binária:

assertEquals(convertToBinary("T", "US-ASCII"), "01010100");

Como esperado, é uma representação binária de sete bits para o caractere 'T'.

The original ASCII left the most significant bit of every byte unused. Ao mesmo tempo, o ASCII deixou muitos caracteres não representados, especialmente para idiomas diferentes do inglês.

Isso levou a um esforço para utilizar esse bit não utilizado e incluir 128 caracteres adicionais.

There were several variations of the ASCII encoding scheme proposed and adopted over the time. Eles foram vagamente chamados de “extensões ASCII”.

Muitas das extensões ASCII tiveram diferentes níveis de sucesso, mas, obviamente, isso não foi bom o suficiente para uma adoção mais ampla, pois muitos caracteres ainda não estavam representados.

One of the more popular ASCII extensions was ISO-8859-1, também conhecido como “ISO Latin 1”.

4.2. Codificação Multi-Byte

À medida que a necessidade de acomodar mais e mais caracteres, os esquemas de codificação de byte único, como o ASCII, não eram sustentáveis.

Isso deu origem a esquemas de codificação de vários bytes, que têm uma capacidade muito melhor, embora com o custo de requisitos de espaço maiores.

BIG5 e SHIFT-JIS são exemplos demulti-byte character encoding schemes which started to use one as well as two bytes to represent wider charsets. A maioria deles foi criada para a necessidade de representar scripts chineses e similares, com um número significativamente maior de caracteres.

Vamos agora chamar o métodoconvertToBinary cominput como ‘語 ', um caractere chinês, eencoding como“ Big5 ”:

assertEquals(convertToBinary("語", "Big5"), "10111011 01111001");

A saída acima mostra que a codificação Big5 usa dois bytes para representar o caractere ‘語 '.

Umcomprehensive list das codificações de caracteres, junto com seus aliases, é mantido pela Autoridade Internacional de Números.

5. Unicode

Não é difícil entender que, embora a codificação seja importante, a decodificação é igualmente vital para entender as representações. This is only possible in practice if a consistent or compatible encoding scheme is used widely.

Diferentes esquemas de codificação desenvolvidos isoladamente e praticados em geografias locais começaram a se tornar desafiadores.

Esse desafio deu origem aa singular encoding standard called Unicode which has the capacity for every possible character in the world. Isso inclui os caracteres que estão em uso e até os que estão extintos!

Bem, isso deve exigir vários bytes para armazenar cada caractere? Honestamente, sim, mas o Unicode tem uma solução engenhosa.

Unicode as a standard defines code points for every possible character in the world. O ponto de código para o caractere 'T' em Unicode é 84 em decimal. Geralmente, nos referimos a isso como "U + 0054" em Unicode, que nada mais é do que U + seguido pelo número hexadecimal.

Usamos hexadecimal como base para pontos de código em Unicode, pois existem 1.114.112 pontos, que é um número bastante grande para se comunicar convenientemente em decimal!

How these code points are encoded into bits is left to specific encoding schemes within Unicode. Abordaremos alguns desses esquemas de codificação nas subseções abaixo.

5.1. UTF-32

UTF-32 éan encoding scheme for Unicode that employs four bytes to represent every code point definido por Unicode. Obviamente, é ineficiente em espaço usar quatro bytes para cada caractere.

Vamos ver como um caractere simples como 'T' é representado em UTF-32. Usaremos o métodoconvertToBinary introduzido anteriormente:

assertEquals(convertToBinary("T", "UTF-32"), "00000000 00000000 00000000 01010100");

A saída acima mostra o uso de quatro bytes para representar o caractere ‘T ', onde os três primeiros bytes são apenas espaço desperdiçado.

5.2. UTF-8

UTF-8 éanother encoding scheme for Unicode which employs a variable length of bytes to encode. Embora ele use um único byte para codificar caracteres em geral, ele pode usar um número maior de bytes, se necessário, economizando espaço.

Vamos chamar novamente o métodoconvertToBinary com entrada como 'T' e codificação como “UTF-8”:

assertEquals(convertToBinary("T", "UTF-8"), "01010100");

A saída é exatamente semelhante ao ASCII usando apenas um byte. De fato, o UTF-8 é completamente compatível com o ASCII.

Vamos chamar novamente o métodoconvertToBinary com entrada como '語' e codificação como “UTF-8”:

assertEquals(convertToBinary("語", "UTF-8"), "11101000 10101010 10011110");

Como podemos ver aqui, o UTF-8 usa três bytes para representar o caractere ‘語 '. This is known as variable-width encoding.

UTF-8, devido à sua eficiência de espaço, é a codificação mais comum usada na web.

6. Suporte de codificação em Java

Java suporta uma ampla variedade de codificações e suas conversões entre si. A classeCharset define umset of standard encodings que toda implementação da plataforma Java é obrigada a suportar.

Isso inclui US-ASCII, ISO-8859-1, UTF-8 e UTF-16, para citar alguns. A particular implementation of Java may optionally support additional encodings.

Existem algumas sutilezas na maneira como o Java escolhe um conjunto de caracteres para trabalhar. Vamos examiná-los em mais detalhes.

6.1. Charset padrão

A plataforma Java depende muito de uma propriedade chamadathe default charset. The Java Virtual Machine (JVM) determines the default charset during start-up.

Isso depende do código do idioma e do conjunto de caracteres do sistema operacional subjacente no qual a JVM está sendo executada. Por exemplo, no MacOS, o conjunto de caracteres padrão é UTF-8.

Vamos ver como podemos determinar o conjunto de caracteres padrão:

Charset.defaultCharset().displayName();

Se executarmos esse trecho de código em uma máquina Windows, obteremos a saída:

windows-1252

Agora, “windows-1252” é o conjunto de caracteres padrão da plataforma Windows em inglês, que neste caso determinou o conjunto de caracteres padrão da JVM em execução no Windows.

6.2. Quem usa o conjunto de caracteres padrão?

Muitas das APIs Java usam o conjunto de caracteres padrão, conforme determinado pela JVM. Para nomear alguns:

Então, isso significa que se executássemos nosso exemplo sem especificar o conjunto de caracteres:

new BufferedReader(new InputStreamReader(new ByteArrayInputStream(input.getBytes()))).readLine();

então usaria o conjunto de caracteres padrão para decodificá-lo.

E há várias APIs que fazem essa mesma escolha por padrão.

O conjunto de caracteres padrão, portanto, assume uma importância que não podemos ignorar com segurança.

6.3. Problemas com o Charset padrão

Como vimos, o conjunto de caracteres padrão em Java é determinado dinamicamente quando a JVM é iniciada. Isso torna a plataforma menos confiável ou propensa a erros quando usada em diferentes sistemas operacionais.

Por exemplo, se executarmos

new BufferedReader(new InputStreamReader(new ByteArrayInputStream(input.getBytes()))).readLine();

no macOS, ele usará UTF-8.

Se tentarmos o mesmo snippet no Windows, ele usará o Windows-1252 para decodificar o mesmo texto.

Ou imagine gravar um arquivo no macOS e depois ler o mesmo arquivo no Windows.

Não é difícil entender que, devido aos diferentes esquemas de codificação, isso pode levar à perda ou corrupção de dados.

6.4. Podemos substituir o Charset padrão?

A determinação do conjunto de caracteres padrão em Java leva a duas propriedades do sistema:

  • file.encoding: o valor desta propriedade do sistema é o nome do conjunto de caracteres padrão

  • sun.jnu.encoding: o valor desta propriedade do sistema é o nome do conjunto de caracteres usado ao codificar / decodificar caminhos de arquivo

Agora, é intuitivo substituir essas propriedades do sistema por meio de argumentos de linha de comando:

-Dfile.encoding="UTF-8"
-Dsun.jnu.encoding="UTF-8"

No entanto, é importante observar que essas propriedades são somente leitura em Java. Their usage as above is not present in the documentation. A substituição dessas propriedades do sistema pode não ter um comportamento desejado ou previsível.

Portanto,we should avoid overriding the default charset in Java.

6.5. Por que o Java não está resolvendo isso?

Há umJava Enhancement Proposal (JEP) which prescribes using “UTF-8” as the default charset in Java em vez de basear-se no conjunto de caracteres do local e do sistema operacional.

Esse JEP está em um estado preliminar a partir de agora e, quando (espero!) For aprovado, resolverá a maioria dos problemas que discutimos anteriormente.

Observe que as APIs mais recentes, como aquelas emjava.nio.file.Files, não usam o conjunto de caracteres padrão. Os métodos nessas APIs leem ou gravam fluxos de caracteres com charset como UTF-8 em vez do charset padrão.

6.6. Resolvendo este problema em nossos programas

Devemos normalmentechoose to specify a charset when dealing with text instead of relying on the default settings. Podemos declarar explicitamente a codificação que queremos usar nas classes que lidam com conversões de caracteres em bytes.

Felizmente, nosso exemplo já está especificando o conjunto de caracteres. We just need to select the right one and let Java do the rest.

Devemos perceber até agora que caracteres acentuados como ‘ç 'não estão presentes no esquema de codificação ASCII e, portanto, precisamos de uma codificação que os inclua. Talvez, UTF-8?

Vamos tentar, agora vamos executar o métododecodeText com a mesma entrada, mas codificando como “UTF-8”:

The façade pattern is a software-design pattern.

Bingo! Podemos ver a saída que esperávamos ver agora.

Aqui, definimos a codificação que consideramos mais adequada às nossas necessidades no construtor deInputStreamReader. Normalmente, esse é o método mais seguro de lidar com caracteres e conversões de bytes em Java.

Da mesma forma,OutputStreamWritere muitas outras APIs oferecem suporte à configuração de um esquema de codificação por meio de seu construtor.

6.7. MalformedInputException

Quando decodificamos uma sequência de bytes, existem casos em que não é legal para oCharset fornecido, ou então não é um Unicode legal de dezesseis bits. Em outras palavras, a sequência de bytes fornecida não possui mapeamento nosCharset especificados.

Existem três estratégias predefinidas (ouCodingErrorAction) quando a sequência de entrada tem entrada malformada:

  • IGNORE irá ignorar caracteres malformados e retomar a operação de codificação

  • REPLACE irá substituir os caracteres malformados no buffer de saída e retomar a operação de codificação

  • REPORT lançará umMalformedInputException

OmalformedInputAction padrão paraCharsetDecoder is REPORT,e omalformedInputAction padrão do decodificador padrão emInputStreamReader éREPLACE.

Vamos definir uma função de decodificação que recebe umCharset especificado, um tipoCodingErrorAction e uma string a ser decodificada:

String decodeText(String input, Charset charset,
  CodingErrorAction codingErrorAction) throws IOException {
    CharsetDecoder charsetDecoder = charset.newDecoder();
    charsetDecoder.onMalformedInput(codingErrorAction);
    return new BufferedReader(
      new InputStreamReader(
        new ByteArrayInputStream(input.getBytes()), charsetDecoder)).readLine();
}

Portanto, se decodificarmos “O padrão de fachada é um padrão de design de software”. comUS_ASCII, a saída para cada estratégia seria diferente. Primeiro, usamosCodingErrorAction.IGNORE que ignora caracteres ilegais:

Assertions.assertEquals(
  "The faade pattern is a software design pattern.",
  CharacterEncodingExamples.decodeText(
    "The façade pattern is a software design pattern.",
    StandardCharsets.US_ASCII,
    CodingErrorAction.IGNORE));

Para o segundo teste, usamosCodingErrorAction.REPLACE que coloca � em vez dos caracteres ilegais:

Assertions.assertEquals(
  "The fa��ade pattern is a software design pattern.",
  CharacterEncodingExamples.decodeText(
    "The façade pattern is a software design pattern.",
    StandardCharsets.US_ASCII,
    CodingErrorAction.REPLACE));

Para o terceiro teste, usamosCodingErrorAction.REPORT que leva a arremessarMalformedInputException:

Assertions.assertThrows(
  MalformedInputException.class,
    () -> CharacterEncodingExamples.decodeText(
      "The façade pattern is a software design pattern.",
      StandardCharsets.US_ASCII,
      CodingErrorAction.REPORT));

7. Outros lugares onde a codificação é importante

Não precisamos apenas considerar a codificação de caracteres durante a programação. Os textos podem dar errado terminalmente em muitos outros lugares.

Omost common cause of problems in these cases is the conversion of text from one encoding scheme to another, possivelmente introduzindo perda de dados.

Vamos examinar rapidamente alguns lugares onde podemos encontrar problemas ao codificar ou decodificar o texto.

7.1. Editores de texto

Na maioria dos casos, um editor de texto é onde os textos se originam. Existem numerosos editores de texto na escolha popular, incluindo vi, Bloco de Notas e MS Word. A maioria desses editores de texto nos permite selecionar o esquema de codificação. Portanto, devemos sempre garantir que eles sejam apropriados para o texto que estamos manipulando.

7.2. Sistema de arquivo

Depois de criar textos em um editor, precisamos armazená-los em algum sistema de arquivos. O sistema de arquivos depende do sistema operacional no qual está sendo executado. A maioria dos sistemas operacionais possui suporte inerente a vários esquemas de codificação. No entanto, ainda pode haver casos em que uma conversão de codificação leva à perda de dados.

7.3. Rede

Os textos quando transferidos através de uma rede usando um protocolo como FTP (File Transfer Protocol) também envolvem a conversão entre codificações de caracteres. Para qualquer coisa codificada em Unicode, é mais seguro transferir como binário para minimizar o risco de perda na conversão. No entanto, a transferência de texto em uma rede é uma das causas menos frequentes de corrupção de dados.

7.4. Bases de dados

A maioria dos bancos de dados populares, como Oracle e MySQL, suporta a escolha do esquema de codificação de caracteres na instalação ou criação de bancos de dados. Devemos escolher isso de acordo com os textos que esperamos armazenar no banco de dados. Este é um dos locais mais frequentes em que a corrupção de dados de texto ocorre devido à conversão de codificações.

7.5. Navegadores

Finalmente, na maioria dos aplicativos da web, criamos textos e os passamos por diferentes camadas com a intenção de visualizá-los em uma interface do usuário, como um navegador. Aqui também é imperativo escolhermos a codificação correta de caracteres que pode exibir os caracteres corretamente. Navegadores mais populares como o Chrome, Edge permitem escolher a codificação de caracteres através de suas configurações.

8. Conclusão

Neste artigo, discutimos como a codificação pode ser um problema durante a programação.

Discutimos ainda os fundamentos, incluindo codificação e conjuntos de caracteres. Além disso, passamos por diferentes esquemas de codificação e seus usos.

Também pegamos um exemplo de uso incorreto de codificação de caracteres em Java e vimos como fazer isso direito. Por fim, discutimos alguns outros cenários de erro comuns relacionados à codificação de caracteres.

Como sempre, o código dos exemplos está disponívelover on GitHub.