Entendendo o Unicode e UTF-8

Artigo que apresenta detalhes sobre as codificações de caracteres Unicode, como UTF-8, UTF-16, UTF-32. Além de descrever as diferenças com ASCII e ISO-8859-1.

Se você trabalha com programação Web, provavelmente já utilizou a codificação de caracteres Latinos (ISO-8859-1) e conhece ou já deve ter ouvido falar de Unicode. Este artigo mostra o que é Unicode, quais os benefícios e como trabalhar com ela.


O que é uma codificação de caracteres?

Codificação de caracteres nada mais é que um mecanismo para armazenar símbolos em forma binária em um computador. Normalmente este mecanismo é feito através de uma tabela de valores numéricos que representam símbolos. Então a codificação especifica como converter um símbolo em código e um código em símbolo.

A tabela ASCII é uma das mais antigas e famosas tabelas de codificação de caracteres em computação. Nesta tabela, cada símbolo é representado por um número dentro do intervalo de 0 a 127. Um número entre 0 e 127 pode ser representado na forma binária com apenas 7 bits. Como a unidade básica dos computadores modernos é o Byte, que é composto de 8 bits, então a codificação ASCII define que cada símbolo é armazenado em um byte, sendo que o primeiro bit dos 8 disponíveis é ignorado (sempre é zero). Assim, o primeiro bit pode ser usado para checar, por exemplo, a integridade de uma sequência de caracteres transmitidos. Caso o bit não vale zero, então algo provavelmente chegou errado.

Alguns exemplos de símbolos representados pela tabela ASCII são:

Exemplo de símbolos ASCII
Int Binário Símbolo
... ... ...
40 00101000 (
41 00101001 )
42 00101010 *
43 00101011 +
... ... ...
48 00110000 0
49 00110001 1
50 00110010 2
... ... ...
65 01000001 A
66 01000010 B
... ... ...
97 01100000 a
98 01100001 b
... ... ...

Ela engloba alguns símbolos matemáticos básicos, alguns símbolos gerais, alguns caracteres de controle, números, letras maiúsculas e minúsculas. Note que o símbolo "1" não é representado pelo inteiro 1 (binário 00000001), mas pelo 49.

A codificação ASCII é muito útil para a língua inglesa, que não acentua palavras. Para a realidade brasileira, ASCII não é muito amigável. A codificação Latin-1 (ISO-8859-1) é uma codificação parecida com a ASCII, mas define apenas caracteres imprimíveis, logo, é uma codificação com foco nas pessoas e não nos computadores. Como a tabela possui apenas 191 símbolos, também pode ser representada com um Byte, já que um Byte pode armazenar o valor inteiro de 0 a 255 (binário de 00000000 a 11111111). Alguns exemplos de símbolos da codificação ISO-8859-1 são:

Exemplo de símbolos ISO-8859-1
Int Binário Símbolo
... ... ...
40 00101000 (
41 00101001 )
42 00101010 *
43 00101011 +
... ... ...
48 00110000 0
49 00110001 1
50 00110010 2
... ... ...
65 01000001 A
66 01000010 B
... ... ...
97 01100000 a
98 01100001 b
... ... ...
231 11100111 ç
232 11101000 è
233 11101001 é
... ... ...

Note que a maioria dos símbolos da tabela ASCII são preservados na tabela ISO-8859-1, porém, a tabela ISO-8859-1 tem novos símbolos. Além disso, em caracteres ISO-8859-1, o primeiro bit não vale sempre zero como na tabela ASCII.


O que é Unicode?

Unicode é uma tabela de símbolos mapeados com propósito universal. O objetivo da tabela Unicode é mapear todos os tipos de símbolos, especialmente aqueles usados nos sistemas de escrita no mundo atualmente. Além deles, também existem símbolos matemáticos, formas geométricas, etc. Unicode possui mais de 100 mil símbolos mapeados e são gerenciados pelo Unicode Consortium. Além da tabela de símbolos, o Unicode prevê algumas regras como, por exemplo, collation, que especifica como dois símbolos devem ser comparados (para ordenação alfabética, por exemplo). A especificação da ISO 10646 prevê a mesma relação de códigos com símbolos que a Unicode, porém, não especifica as regras adicionais, descritas na Unicode apenas.

Você deve estar se perguntando agora: se um byte consegue armazenar apenas 256 valores diferentes, como é que a tabela Unicode consegue mapear cada símbolo em um código binário próprio?

A resposta é simples: uma codificação não precisa necessariamente usar um byte para representar um símbolo e vise-versa. É aí que entra o algoritmo de codificação e decodificação, como o UTF-8 (UCS), UTF-16 (UCS-2), UTF-32 (UCS-4), etc. Se você gosta de RFCs, vai se interessar por RFC3629 (UTF-8) e RFC 2781 (UTF-16).

Vamos deixar claro as partes tomando como exemplo a tabela ASCII. A tabela ASCII define símbolos e códigos correspondentes. A codificação ASCII define que a forma como o símbolo será convertido em código é através da utilização de 8 bits, sendo o primeiro sempre zero. Poderíamos, portanto, criar uma codificação alternativa chamada ASCII-2 onde a forma de converter um símbolo em código seria através da utilização de 8 bits, mas sendo o último bit sendo sempre 1 e utilizando os demais bits para armazenar o valor binário do código numérico do símbolo.

Então, a tabela Unicode é uma tabela que associa um número (ou codepoint) a um símbolo correspondente. Por exemplo:

Exemplo de símbolos Unicode
Int Símbolo
... ...
40 (
41 )
42 *
43 +
... ...
48 0
49 1
50 2
... ...
65 A
66 B
... ...
97 a
98 b
... ...
231 ç
... ...
8592 ← (seta para esquerda)
... ...
28381 滝 ("taki" em japonês)
... ...

No link a seguir, terá um conjunto de símbolos e seus respectivos códigos Unicode: www.unicodetables.com

Note que os símbolos da tabela ASCII são preservados na tabela Unicode. O "ç", também é semelhante ao da tabela ISO-8859-1. Mas possui símbolos como a seta a esquerda, cujo código é 8592, ou ideogramas japoneses como o "taki", cujo código é 28381.


Codificação UTF-32

Bom, sabendo o que é Unicode e o que é codificação, vamos falar sobre o tipo de codificação mais simples baseado em Unicode: o UTF-32. UTF significa "Unicode Transformation Formats" e possui diferentes formas de codificar/decodificar os símbolos da tabela Unicode em formato computacional.

A codificação UTF-32 é simples pois cada símbolo é sempre representado com 4 bytes. Com 4 bytes, é possível representar 4.294.967.296 valores diferentes, que é o valor de 2 elevado a 32.

Esta codificação, embora simples, é pouco usada, já que gasta muitos bytes para representar um único símbolo. Por outro lado, o tamanho é fixo, então é mais fácil obter o N-ésimo símbolo de uma sequência de símbolos (basta saltar para o 4 x N byte da sequência).

Para escrever "AB" em ASCII, seriam necessários 2 bytes assim:

01000001 01000010

Em UTF-32, para escrever "AB", seriam necessários 8 bytes assim:

00000000 00000000 00000000 01000001
00000000 00000000 00000000 01000010

Note que o desperdício é alto, mas a forma de codificar/decodificar é muito simples.


Codificação UTF-8

Finalmente chegamos à codificação UTF-8. Esta codificação utiliza um número variável de bytes para representar símbolos. Dependendo do símbolo, ele pode gastar 1, 2, 3 ou 4 bytes.

Os símbolos com 1 byte são idênticos aos da tabela ASCII. Isso possibilita compatibilidade entre as duas codificações. Em outras palavras, os símbolos cujos códigos estão entre 0 e 127 são representados da mesma forma que os símbolos ASCII. Logo, se um byte é capturado e ele começa com zero, então o byte representa um símbolo da tabela ASCII. Se o byte começa com "um", então os N bytes seguintes representam um símbolo da tabela Unicode.

Os símbolos com 2 bytes, são aqueles que possuem o formato de bits assim:

110xxxxx 10xxxxxx

Onde estão os "x" ficam os bits que, em sequência, representariam um número binário que na notação inteira indicaria o código do caractere.

Bom, então o código Unicode 128 (que é posterior ao 127) é representado por dois bytes da seguinte forma:

11000010 10000000
   ^^^^^   ^^^^^^
   00010   000000

Note que se pegarmos os bits onde estavam os "x" e colocarmos em sequência, teremos a sequência binária "00010000000", que na notação decimal vale 128.

O símbolo "ç", cujo código é 231 (em binário é "11100111") seria representado da seguinte forma:

11000011 10100111
   ^^^^^   ^^^^^^
   00011   100111
Hommer assustado com caractere exibido incorretamente

Você já deve ter passado por problemas de codificação. Se fizermos o seguinte código PHP e salvarmos o arquivo como UTF-8, teremos um problema:

<?php
header('Content-type: text/html; charset=ISO-8859-1');
echo 'ç';
?>

O código acima irá enviar um arquivo com dois bytes (aqueles mostrados anteriormente para representar o "ç"), só que o cabeçalho HTTP enviado ao navegador diz que o conteúdo está codificado em ISO-8859-1. Como cada símbolo ISO-8859-1 ocupa um byte, o navegador pegará o código binário "11000011" (primeiro byte) e exibirá o símbolo correspondente, depois pegará o código binário "10100111" (segundo byte) e exibirá o símbolo correspondente.

Seria exibido "ç".

Isso acontece pois o código binário "11000011" vale 195 (decimal) e, na tabela ISO-8859-1, representa o símbolo "Ã". Já o código binário "10100111" vale 167 (decimal) e, na tabela ISO-8859-1, representa o símbolo "§". Logo, o símbolo "ç" possui o mesmo código em ISO-8859-1 e em Unicode, mas a codificação UTF-8 "monta" os bits de uma forma diferente da codificação ISO-8859-1.

Para os símbolos que precisam de 3 bytes, é utilizada a máscara de bits:

1110xxxx 10xxxxxx 10xxxxxx
(16 bits efetivos)

E para os símbolos que precisam de 4 bytes, é utilizada a máscara de bits:

11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
(21 bits efetivos)

Observe que estas máscaras possuem uma lógica. Os símbolos de 1 byte sempre começam com o bit 0.

Os símbolos de 2 bytes, sempre começam com os bits 110xxxxx.

Os símbolos de 3 bytes, sempre começam com os bits 1110xxxx.

Os símbolos de 4 bytes, sempre começam com os bits 11110xxx.

E para os símbolos de 2 a 4 bytes, os bytes posteriores ao primeiro sempre começam com "10xxxxxx".

Como UTF-8 usa 1 ou 2 bytes para caracteres latinos, o tamanho dos textos gerados em UTF-8 costuma ser muito menor que um texto UTF-32.


UTF-16

Já a codificação UTF-16 usa 2 ou 4 bytes. Existe uma conta própria que se conseguir determinar se o símbolo precisará de 2 ou 4 bytes. Como é uma codificação menos comum, não é muito valioso conhecer os detalhes de como ela funciona.


PHP e Unicode

PHP 5 não dá suporte nativo às codificações Unicode. Porém, uma das grandes promessas para PHP 6 será a divisão do tipo string em 2: string e binary (o nome pode ser outro). O futuro tipo string será usado para representar textos em alguma codificação e será suportada a utilização de unicode. Já o tipo binary será usado para armazenar sequência de bytes, assim como é o tipo "string" atual.

Atualmente, se fizermos o seguinte código em PHP e salvarmos em UTF-8...

<?php
$texto = 'açAEIOU';
$letra = $texto[3];
$sub   = substr($texto, 0, 3);
$len   = strlen($texto);

... o valor da variável $letra receberia o valor "A", a variável $sub receberia o valor "aç" e a variável $len receberia o tamanho 8.

Isso acontece porque o operador colchetes obtem o N-ésimo byte da string e não o N-ésimo símbolo. Da mesma forma, a função substr trabalha com bytes e não com símbolos. E a função strlen devolve o número de bytes e não o número de símbolos. Como o símbolo "ç" ocupa dois bytes em UTF-8, estas funções tem seu significado afetado.

Portanto, para trabalhar com textos em UTF-8, é preciso ferramentas externas que simulem as operações básicas sobre textos. Uma alternativa simples é usar a extensão PRCE para obter substring, já que a extensão já suporte codificação UTF-8.

Para um script PHP informar para o navegador que está utilizando a codificação UTF-8, basta usar a função header e informar o valor da diretiva Content-type com o valor adequado. Por exemplo:

<?php
header('Content-Type: text/html; charset=UTF-8');
...

Uma outra alternativa é simular o comportamento do cabeçalho HTTP através da tag HTML "meta":

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    ...

Bancos de Dados e Unicode

Vários bancos de dados também dão suporte a UTF-8. Em MySQL e PostgreSQL, para criar um BD UTF-8, basta especificar na instrução CREATE DATABASE:

MySQL:

CREATE DATABASE nome_bd CHARACTER SET UTF8;

PostgreSQL:

CREATE DATABASE nome_bd ENCODING UNICODE;

Além disso, a aplicação PHP precisa informar que os dados trafegados de I/O (entrada e saída) estarão em UTF-8. Para cada Banco de dados existe um comando específico para tal operação. Nas funções nativas para MySQL e PostgreSQL, existem as funções para isso:

MySQL:

mysql_set_charset('UTF8', $conexao);

PostgreSQL:

pg_set_client_encoding($conexao, 'UNICODE');

Para PDO, é necessário usar a SQL adequada para esta operação, que pode variar de BD para BD. Alguns exemplos abaixo:

MySQL:

SET NAMES UTF8;

PostgreSQL:

SET NAMES 'UNICODE';

Porém, se o seu banco está em ISO-8859-1 e você gostaria de exibir o conteúdo na forma de UTF-8, o PHP pode te dar uma ajudinha com as funções utf8_encode e utf8_decode, que serve para converter uma sequência em ISO-8859-1 para UTF-8 e vice-versa. Mas tome cuidado: aplicar utf8_encode sobre um texto que já está em UTF-8 implica em analizar os bytes da sequência como se fossem isso e convertê-los para UTF-8, causando dupla codificação, que é, provavelmente, indesejável.


Conclusão

Note que várias tecnologias estão tendenciando seu desenvolvimento para utilizar Unicode e, aprarentemente, os esforços são maiores na codificação UTF-8, por exemplo, a Web semântica, Ajax, XML, etc. O propósito principal da Unicode é a internacionalização de sistemas.

Unicode ainda está em desenvolvimento. Ainda há o que melhorar, mas é uma tecnologia que tende a se tornar padrão. Que tal começar a trabalhar com Unicode?

6 comentários

Anônimo disse...

estou com um problema neste tema, passei por aqui e em outros artigos seus e gostei de todos parabéns.
usei o seu exemplo para testar e dito e feito. continuando a procurar, encontrei em http://www.php.net/manual/en/ref.mbstring.php as funções Multibyte String que parece resolvem o problema. Habilitei no php.ini, na seção [mbstring] e passou a funcionar, veja:
$letra2 = mb_substr($texto, 3, 1);
$sub2 = mb_substr($texto, 0, 3);
$len2 = mb_strlen($texto);

Fica a dica.
Washington

crocosauros disse...

15 min de leitura resolveu um dia todo de pesquisa.
Sem palavras. Deixo meu muito obrigado.
Parabéns pela matéria. E mais uma vez muito obrigado.
Sucesso.