PHP e Unicode – O caminho das pedras

Esta verdadeira aula de Unicode foi gentilmente escrita pelo meu grande amigo Gustavo Ciello.

Se existem assuntos que deixam alguns programadores de cabelo em pé só de ouvir falar, um destes, com certeza, é Unicode. Se você é um dos que tem medo do Unicode e desenvolve utilizando PHP, este é o artigo certo para deixar este medo de lado.

Por que utilizar Unicode?

O Unicode é um padrão definido para que as suas aplicações possam falar qualquer língua. Está desenvolvendo uma aplicação para a nuvem ? Já imaginou se, de repente, sua app explode em um país que usa escrita diferente, como a Coréia do Sul, e demanda uma tradução urgente? E na Ucrânia? Marrocos? Internet é isso..
Se você desenvolver já pensando de maneira Unicode, tudo fica mais fácil quando se trata de internacionalização e localização (i18n e i10n para os íntimos, respectivamente). Mas antes de sair usando, é preciso entender algumas coisas para não ficar boiando em um mar de informações desconexas.

Afinal, o que são Codepages? E ISO-8859-1? E Latin 1?

Para entender essa bagunça, um pouco de história: Quando os homens eram homens e desenvolviam seus próprios drivers, ficou decidido que cada letra do alfabeto (em suas variantes minúsculas e maiúsculas), números e alguns outros símbolos úteis seriam representados utilizando um número inteiro. Utilizando 7 bits (do número 0 até o 127), era possível representar todos os caracteres possíveis… em inglês! Como estes primeiros sistemas tem origem nos EUA, e lá não se usam acentos nem outros caracteres engraçadinhos, o negócio navegava de vento em popa e a definição ficou conhecida como ASCII <http://pt.wikipedia.org/wiki/ASCII>.

Enquanto isso, nas outras partes do mundo, as pessoas ainda precisavam usar os acentos e letras estranhas aos padrões americanos, e para tal tiveram uma idéia: Já que os computadores definem 8 bits para representar 1 byte, e o padrão ASCII só necessitava de 7, sobravam ainda as 127 posições “altas”, entre o 128 e 255, e que foram utilizadas para armazenar os caracters adicionais. Logo aconteceu o inevitável e várias pessoas fizeram isso ao mesmo tempo tornando os caracteres acima do 127 uma loteria: Se você morasse na Rússia, um número qualquer como o 193 poderia ser uma letra completamente diferente de uma pessoa que morasse no Brasil, por exemplo.

Para resolver essa bagunça, foi criado o conceito de Codepage (ou Character Set), um mapa de caracteres relaciona as 256 posições possíveis com seus caracteres específicos. No Brasil costumamos usar o mapa ISO-8859-1 <http://pt.wikipedia.org/wiki/ISO_8859-1> (ou Latin 1), que nos dá acesso aos caracteres acentuados e mais alguns outros. É importante lembrar, os caracteres entre 0 e 127 continuavam os mesmos, portanto o pessoal que não usa palavras acentuadas e letras diferentes estava completamente por fora da bagunça.

O pessoal que usa alfabetos mais complexos, como o chinês, estava ainda mais encrencado: Os 8 bits não davam nem pro cheiro para caber todas os caracteres possíveis. Para tais, foram criadas alternativas como o DBCS <http://en.wikipedia.org/wiki/DBCS>, o qual eu vou me abster de explicar e resumir em uma sigla: Gambiarras.

A salvação: Unicode

Prevendo o fim do mundo, já pelo final dos anos 80, um povo da Xerox e da Apple viu que essa coisa não ía dar pé MESMO, e decidiram começar a mapear todas as combinações possíveis de caracteres, para que pudessem atender ao mundo todo. De uma idéia a coisa evoluiu e muitas outras empresas entraram na jogada (como a IBM e a Microsoft), inclusive universidades e qualquer outra pessoa em sã consciência sobre o problema. Foi criado então o Unicode Consortium, responsável por definir um padrão definitivo.

Esse pessoal baixou a cabeça e botou a mão na massa, mapeando praticamente todo o tipo de representação visual escrita em uso no mundo. Incluem-se alfabetos asiáticos, alfabeto cirílico (Russo e similares), alfabeto latino (o nosso), alfabeto árabe e muitos outros. Ainda há trabalho pela frente, onde eles pretendem mapear runas (!), hieróglifos egípcios, escrita cuneiforme e outras bizarrices.

Com o padrão em desenvolvimento, ficou faltando a resposta para como armazenar os caracteres. Era óbvio que os 8 bits originais não bastariam, mas também já era visível que 16 bits também não dariam muito certo, já que permitem “somente” 65535 caracteres. Foram definidos alguns métodos, mas os que pegaram mesmo foram:

UTF-8: Tem o comprimento variável entre 8, 16, 24 e 32 bits (1, 2, 3 e 4 bytes) por caractere. Dependendo do texto que é armazenado (e de acordo com as tabelas definidas pelo Unicode), o UTF-8 vai aumentando o espaço necessário para armazenar o caractere. Como utiliza somente 8 bits para representar os caracteres do padrão ASCII, e de quebra nas mesmas posições (um “a” em ASCII é o mesmo que um “a” em UTF-8), ele tem duas boas vantagens: Utiliza menos espaço de armazenamento e tem uma certa “compatibilidade” em exibir textos ASCII. É o padrão mais usado na Web hoje.

UTF-16 ou UCS-2: Tem o comprimento variável entre 16 ou 32 bits (2 ou 4 bytes) por caractere. Como o Unicode define um Plano Básico Multilingual (PBM), ou seja, com 16 bits é possível atender a praticamente todos os textos escritos no dia a dia, este padrão utiliza os 16 bits do PBM por default, e estende para 32 bits quando necessário. É um padrão mais “oficial”, utilizado pelo Windows (2000+), Java, .NET, MacOSX e alguns outros. Como o armazenamento mínimo deste padrão é de 2 bytes, é necessário definir a ordem em que eles são representados <http://pt.wikipedia.org/wiki/Extremidade_(ordenação)>, logo o formato UTF-16 tem dois “sub-formatos”: UTF-16LE (Little endian) e UTF-16BE (Big Endian).

O BOM

Como o UTF-16 (ou UCS-2) precisa definir o sistema que foi utilizado par ordenar os bytes, foi criado um padrão chamado BOM – Byte Order Mask – inserido no começo de um arquivo para indicar se ele utiliza Little Endian ou Big Endian. O BOM consiste de dois bytes, logo no início do arquivo, dispostos em um padrão que permite o programa “saber” a codificação esperada. Como a ordem dos bytes não faz diferença no UTF-8, um arquivo codificado assim não precisa possuir um BOM. Porém, existe um BOM para UTF-8, que é tolerado (mas não recomendado) pelo padrão. Ele simplesmente identifica aquele arquivo como UTF-8.

O Unicode e a Internet

A maioria dos navegadores atuais (Inclusive o IE) suportam o formato Unicode, em especial o UTF-8. Grandes sites utilizam o UTF-8 há um bom tempo, e como exemplo posso citar o Google. Para montar uma aplicação que realmente trabalhe com Unicode, é necessário que todas as etapas de tratamento de strings estejam em sincronia. Veremos cada uma das etapas em detalhes: Banco de dados e conexões, codificação dos arquivos, geração de páginas HTML em UTF-8 e funções utilizadas na linguagem.

Utilizando Unicode com Bancos de Dados

Vou me ater aos principais bancos de dados utilizados com PHP hoje: MySQL e PostgreSQL. Outros bancos de dados também suportam variantes do padrão Unicode e as informações disponíveis aqui se aplicam em parte. Tanto MySQL quanto PostgreSQL suportam o padrão Unicode através do UTF-8. Já na criação do banco de dados é necessário definir que ele será Unicode:

MySQL

CREATE DATABASE dados DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;

PostgreSQL

CREATE DATABASE dados ENCODING 'UTF8';

Se você criar o banco como Unicode, todas as tabelas criadas nele terão as mesmas propriedades. Para o MySQL, ainda é possível definir uma tabela UTF-8 em um banco de dados com outra codificação. Utiliza-se:

CREATE TABLE clientes (
  id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
  nome VARCHAR(250) NOT NULL,
  PRIMARY KEY(id)
) TYPE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci;

Uma vez criado o banco de dados, é preciso que o PHP saiba que aquele banco está utilizando Unicode. Para que isto aconteça, é necessário definir o UTF-8 na hora da conexão. Exemplos:

MySQL Nativo

<?php
    $link = mysql_connect('localhost', 'user', 'pass', TRUE);
    mysql_selectdb('dados', $link); 
    mysql_set_charset('utf8', $link); 
?>

MySQL com PDO

<?php
    $pdo = new PDO('mysql:host=hostname;dbname=dados', 'user', 'pass', array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));
?>

PostgreSQL Nativo

<?php
    $link = pg_pconnect("host=localhost dbname=dados user=user password=pass");
    pg_set_client_encoding($link, "UNICODE");
?>

PostgreSQL com PDO

<?php
    $pdo = new PDO('pgsql:host=hostname;dbname=dados', 'user', 'pass');
    $pdo->exec("SET NAMES 'UTF8'");
?>

A partir daí, é só utilizar as funções do banco de dados normalmente, lembrando sempre que todas as operações efetuadas a partir deste ponto serão feitas considerando as strings como Unicode.

Salvando arquivos codificados em UTF-8

Para que as coisas que você escreve estejam em Unicode, é preciso que os arquivos estejam codificados para tal. Recomenda-se utilizar UTF-8 sem BOM, tanto porque não é necessário no UTF-8, quanto pois é conhecido que o PHP tem problema quando há o BOM <http://bugs.php.net/bug.php?id=22108>. Para isto você deverá ajustar o seu editor preferido. Eu recomendo fortemente o Notepad++ (no Windows) para esta função, já que ele tem funções de conversão entre os vários encodings.

Notepad++
– É possível saber em qual codificação o arquivo está agora, através de uma informação na barra de status.
– É possível converter entre as várias codificações através do menu Formatar. (vide Figura 1)
– É possível definir uma codificação padrão para os arquivos novos que serão criados através do Notepad++. (vide Figura 2)


Figura 1 – Codificação de novo documento


Figura 2 – Menu formatar

Em outros editores muito utilizados como Dreamweaver, Aptana, TextMate e Eclipse, é possível configurar e converter o formato dos arquivos facilmente. Consulte a documentação do seu editor preferido para saber como.

Páginas HTML que suportam UTF-8

O HTML é um padrão definido sobre textos, logo o navegador precisa saber em que codificação este texto é transmitido. Um fenômeno que acontece em muitos sites mal feitos é a falha nesta informação, levando a exibição de caracteres de forma incorreta. Como as tags e textos utilizados para definir a página seguem um padrão em inglês, a página chega e layout também, mas os textos ficam “bêbados”. Para informar ao navegador a codificação UTF-8, é necessário adicionar uma simples linha ao cabeçalho de cada página:

Para páginas em HTML

<meta http-equiv="content-type" content="text/html; charset=utf-8"></meta>

Para páginas em XHTML

<meta http-equiv="content-type" content="text/html; charset=utf-8" />

Ainda é possível enviar um cabeçalho HTTP (header) que diz em qual codificação o texto será transmitido. Em teoria, é redundante utilizá-lo junto com a tag meta, mas pode ser útil em alguns casos:

<?php header("Content-type: text/html; charset=utf-8"); ?>

Seguindo estes passos, o navegador saberá que todo o texto enviado pela sua aplicação estará codificado em UTF-8 e os seus textos poderão ficar em paz. Para garantir que esta informação está sendo repassada ao navegador, confira no menu codificação (depende de cada navegador) se a detecção automática está selecionada e se o UTF-8 foi o escolhido.

Tratando strings em UTF-8 no PHP

Tudo seriam flores se as funções do PHP aceitassem UTF-8 por padrão, mas elas não aceitam. Funções que tratam de strings, como strlen, esperam strings codificadas em ASCII ou em alguma codepage compatível. Se você enviar strings Unicode para elas, normalmente acontecem problemas.

A função strlen(), por exemplo, conta quantos caracteres há na string. Se você chamar strlen(‘abc’), o resultado será 3, afinal temos 3 caracteres. Mas a função strlen conta quantos bytes há na string, e não tem noção de que caracteres Unicode podem ocupar mais de um byte. Então se você chamar strlen(‘água’), sendo que a string é Unicode, o resultado será 5, pois o caractere “á” utiliza dois bytes. Problemas…

A melhor saída, neste caso, é utilizar uma extensão chamada Multi-Byte String, ou mbstring. Ela é opcional na instalação do PHP, mas está disponível na maioria dos hosts contratados. Você pode verificar se o seu host possui a mbstring instalada através do phpinfo().

Ao usar a mbstring, é necessário definir que você está utilizando UTF-8, com os comandos:

mb_internal_encoding(‘UTF-8′);
mb_http_output(‘UTF-8′);
mb_http_input(‘UTF-8′);
mb_language(‘uni’);
mb_regex_encoding(‘UTF-8′);
ob_start(‘mb_output_handler’);

Você pode colocar os todos os comandos em um arquivo e utilizar include() ou require() para referenciá-lo. Também tenha cuidado ao utilizar novamente a função ob_start() em outras partes do script, lembrando de chamar a função ob_end_flush().

A partir deste ponto, você pode utilizar o UTF-8 normalmente na sua aplicação, sem medo de caracteres estranhos :-)

As funções normalmente disponíveis no PHP para tratamento de strings têm o seu equivalente na mbstring, como mb_strlen e mb_strtolower. Confira aqui a lista completa de funções com o equivalente mb_* <http://php.net/manual/en/book.mbstring.php>

PHP 6

A versão em desenvolvimento do PHP promete compatibilidade nativa com strings unicode, sem precisar das funções da mbstring. A codificação padrão utilizada será UTF-16 e as funções atuais serão atualizadas para o suporte das novas strings. Acredita-se que com a nova versão, o desenvolvimento de aplicações Unicode torne-se um pouco mais fácil :)

Referências

http://www.joelonsoftware.com/articles/Unicode.html
http://allseeing-i.com/How-to-setup-your-PHP-site-to-use-UTF8
http://alessandrosantos.com.br/2009/08/18/php-mysql-e-utf-8-o-guia/
http://webcollab.sourceforge.net/unicode.html
http://phpbrasil.com/artigo/11qDFvxJBUXI/lidando-com-utf-8-com-o-php-e-mysql
http://www.phpwact.org/php/i18n/utf-8
http://www.phpwact.org/php/i18n/charsets
Referência do PHP – http://php.net
Referência MySQL – http://dev.mysql.com/doc/
Referência PostgreSQL – http://www.postgresql.org/docs/

20 respostas para “PHP e Unicode – O caminho das pedras”

  1. Bruno Lacerda disse:

    Show de bola amigo…

    Eu programo PHP a um ano e sempre tenho problemas com os caracteres.

    Obrigado pela contribuição

  2. Kennedy disse:

    Rapaz vi que não sabia quase nada de unicode mesmo… post muito esclarecedor e didático! Já está no meu delicious.

    Falow!

  3. Flávio disse:

    Parabéns, ótimo post !! Esclarecedor

  4. Ótimo artigo! Encoding muitas vezes é um problema, adicionando o Javascript+Ajax na lista de pepinos para lidar com caracteres especiais a coisa fica pior ainda! haha… :)

  5. Douglas disse:

    Muito útil o material. Parabéns!

  6. Mauro George disse:

    Excelente texto.

    No entanto se possível poderia se aprofundar em relação ao MySQL o porque de definir o COLLATE como utf8_general_ci e não como utf8_unicode_ci, ut8_bin entre outros. Ou ainda o porque de preferir usar utf8_general_ci em relação aos demais

  7. DRF Downloads disse:

    Excelente matéria, já tive grandes problemas com charset, seu artigo ajuda a entender mais ainda algo que poderia ser mais simples.

  8. Penso disse:

    Obrigado GUERREIRO DA TECNOLOGIA, vc fez eu repensar o e entender o Unicode agora vo precisar refazer

  9. Sidney disse:

    Excelente artigo.

    Parabéns.

  10. Excelente post. Simplificou bastante…

  11. muito bom, boa leitura a todos…

  12. Josenir disse:

    Mesmo anos depois seu artigo continua sendo útil. Muito útil mesmo !!

    Muito obrigado e um excelente ano de 2012 !!

  13. André disse:

    Ótimo artigo! Parabéns

  14. Bruno Vieira disse:

    Com certeza esse cabeçalho ajudou muito em meu problema

  15. pazmais10 disse:

    Nossa cara, explodi de felicidade quando finalmente meu projeto ficou em unicode, obrigado mesmo, que deus lhes abençoe. Eu to tão feliz pq deu certo uhuuuuuuuu, valeu
    to rindo sozinho aki q felicidade

  16. Bob Wizley disse:

    Excelente artigo. Esclareceu bastante as minhas dúvidas.

  17. Atualmente estou desenvolvendo uma solução no php 5,0, usando framework zend e estou enfrentando sérios problemas com acentuação no bd postgres..

  18. Matheus disse:

    Ótima contribuição. Ajudou muito.

Deixe uma resposta