sexta-feira, 2 de abril de 2010

O Mínimo Absoluto que Cada Desenvolvedor de Software Deve Obrigatoriamente Saber Sobre Unicode e Conjuntos de Caracteres (Sem Desculpas!)



Esse é um artigo sobre Unicode, charsets, etc. que eu achei esses dias.
Ele é bem simples de se entender, e é uma boa base pra alguém que não conheça nada sobre o assunto (como eu!).
Bom, eu achei ele tão interessante que traduzi, e, obviamente, estou disponibilizando aqui...
Qualquer erro de tradução, favor avisar... xD

O artigo original foi escrito por Joel Spolsky, e pode ser acessado em http://www.joelonsoftware.com/articles/Unicode.html.

Você já se perguntou o que é aquela misteriosa tag Content-Type? Você sabe, aquela que deve-se colocar em HTML e você nunca soube realmente para que serviam?

Você alguma vez recebeu um e-mail de algum amigo na Bulgária com o assunto igual a "???? ?????? ??? ????"?

Eu fiquei consternado ao descobrir quantos desenvolvedores de software não estão completamente a par do misterioso mundo dos conjuntos de caracteres, codificações, Unicode, etc. Alguns anos atrás, um testador do FogBUGZ estava se perguntando se ele poderia tratar e-mails escritos em japonês. Japonês? Eles têm e-mail em japonês? Não faço idéia. Quando eu olhei de perto o controle ActiveX que eles estavam usando para processar mensagens de e-mail MIME, descobri que ele estava fazendo tudo errado com os conjuntos de caracteres, e então tivemos de escrever um código heróico para desfazer a conversão errada que havia sido feita e refazê-la corretamente. Quando olhei outra biblioteca comercial, ela também tinha uma implementação de caracteres totalmente falha. Entrei em contato com o desenvolvedor daquele pacote e ele meio que pensou que eles "não tinham nada a ver com isso". Como muitos programadores, ele apenas desejou que isso terminasse de alguma outra maneira.

Mas isso não vai acontecer. Quanto eu descobri que a popular ferramenta de desenvolvimento PHP tinha quase que completa ignorância sobre problemas com conjuntos de caracteres, usando alegremente 8 bits para cada caracter e tornando praticamente impossível o desenvolvimento de boas aplicações web internacionais,  eu pensei,  enough is enough.

Então eu tenho um pronunciamento a fazer: se você é um programador trabalhando em 2003 e não conhece o básico sobre caracteres, conjuntos de caracteres, codificações e Unicode, eu vou pegar você, e vou puni-lo fazendo-o descascar cebolas por 6 meses em um submarino. Eu juro que vou.

E mais uma coisa:

NÃO É TÃO DIFÍCIL ASSIM.

Nesse artigo eu irei introduzi-lo ao que todo programador profissional deveria saber. Todas aquelas coisa sobre "texto plano = ascii = caracteres são 8 bits" não estão apenas erradas, estão irremediavelmente erradas, e se você ainda programa dessa maneira, não é muito melhor do que um médico que não acredita em germes. Por favor, não escreva nenhuma outra linha de código antes de terminar de ler este artigo.

Antes de eu começar, devo avisá-lo que se você é uma das raras pessoas que conhece internacionalização, você irá achar minha explicação um pouco simplificada demais. Eu estou apenas tentando fazer um alinhamento mínimo para que todos possam entender o que está acontecendo e poder escrever algum código que tenha alguma esperança de funcionar com texto em qualquer linguagem que não o subgrupo de inglês que não inclui palavras com acentos. E devo avisá-lo que o tratamento de caracteres é apenas uma pequena parte do necessário para se criar um software que funcione internacionalmente, mas eu só posso escrever uma coisa de cada vez, assim, hoje serão conjuntos de caracteres.


Uma Perspectiva Histórica

A maneira mais fácil de entender esse assunto é avançando cronologicamente.
Você provavelmente está pensando que irei falar de conjunto de caracteres muito antigos como EBCDIC aqui.

Bem, eu não irei. EBCDIC não é relevante para sua vida. Não precisamos voltar no tempo tanto assim.
ASCII table
De volta aos tempos semi-antigos, quando o Unix estava sendo inventado e K&R estavam escrevendo The C Programming Language, tudo era bastante simples. EBCDIC estava saindo. Os únicos caracteres que realmente importavam eram as boas letras não acentuadas do inglês, e tínhamos um código para elas chamado ASCII que era capaz de representar qualquer caractere usando um número entre 32 e 127. Espaço era igual a 32, a letra "A" era 65, etc. Isto podia ser convenientemente armazenado em 7 bits. A maioria dos computadores daquela época usava bytes de 8 bits, então você não só apenas podia guardar cada possível caractere ASCII, mas também tinha um bit excedente, que, caso você fosse perturbado, poderia usar para seus propósitos doentios: os seres obtusos do WordStar usaram o bit mais alto para indicar a última letra de uma palavra, condenando o WordStar a editar apenas textos em inglês. Códigos abaixo de 32 eram chamados não-imprimíveis e eram usados em xingamentos. Brincadeira. Eles eram usados como caracteres de controle, como o 7 que fazia seu computador bipar e o 12 que fazia a página atual da impressora sair da impressora e uma nova folha ser pega.

E estava tudo bem, desde que você falasse inglês.

Pelo fato de os bytes terem espaço para 8 bits, um monte de gente começou a pensar, "Nossa, nós podemos usar os códigos 128-255 para nossos próprios assuntos". O problema foi que um monte de gente teve essa idéia ao mesmo tempo, e elas tinham suas próprias idéias sobre o que deveria ficar no espaço entre 128 a 255. O IBM-PC tinha uma coisa que veio a ser conhecida com o conjunto de caracteres OEM que disponibilizava alguns caracteres acentuados para línguas européias e um monte de caracteres para desenhar linhas... barras horizontais, barras verticais com pequenos penduricalhos balançando do lado direito, etc., e você poderia usar esses caracteres de desenhos de linhas para fazer caixas e linhas bem apresentáveis na tela, que você ainda podia ver rodando no computador 8088 em suas lavadoras a seco. De fato quando as pessoas começaram a comprar PCs fora dos Estados Unidos todos os tipos de diferentes conjuntos de caracteres OEM foram inventados, e todos eles usavam os maiores 128 caracteres para seus próprios objetivos. Por exemplo, em alguns computadores o código 130 iria ser exibido como "é", mas em computadores vendidos em Israel havia a letra hebréia Gimel (ג), daí quando americanos enviassem seus currículos (résumés, em inglês) para Israel eles chegariam como rגsumגs. Em muitos casos, como em russo, haviam um monte de idéias diferentes sobre o que fazer com os 128 maiores caracteres, então você não podia nem mesmo trocar documentos em russo de forma confiável.

Eventualmente esse OEM livre para todos foi codificado no padrão ANSI. No padrão ANSI, todos concordaram sobre o que fazer abaixo de 128, que era quase o mesmo que ASCII, mas haviam muitas maneiras diferentes de lidar com os caracteres acima de 128, dependendo de onde você vivesse. Esses diferentes sistemas eram chamados de páginas de códigos. Então, por exemplo, em Israel, o DOS usava uma página de código chamada 862, enquanto usuários gregos usavam a 737. Elas eram iguais nos caracteres abaixo de 128, mas diferentes acima de 128, onde todas as letras diferentes ficavam. As versões nacionais do MS-DOS tinham dúzias dessas páginas de códigos, lidando com tudo, desde inglês até islandês, e eles tinha algumas páginas de código "multilíngua" que podiam entender Esperanto e Galiciano no mesmo computador! Wow! Mas ter, por exemplo, hebreu e grego no mesmo computador era uma total impossibilidade a menos que você escreve seu próprio programa customizado que mostrasse tudo usando gráficos bitmap, pois Hebreu e Grego requeriam diferentes páginas de códigos com diferentes interpretações dos números altos.

Enquanto isso, na Ásia, coisas mais malucas estavam acontecendo, levando em conta o fato que os alfabetos asiáticos têm milhares de letras, que nunca caberiam em 8 bits. Isso era normalmente resolvido pelo confuso sistema chamado DBCS, o "conjunto de caracteres com duplo byte" no qual algumas letras eram armazenadas em um byte e outras em dois. Era fácil se mover para frente em uma string, mas quase impossível se mover para trás. Os programadores eram encorajados a não usar s++ e s-- para se moverem para frente e para trás, mas ao invés  disso chamar funções como AnsiNext e AnsiPrev do Windows que sabiam como lidar com toda essa confusão.

Mas ainda assim, a maioria das pessoas apenas achava que um byte era um caractere e um caractere era 8 bits e desde que você nunca movesse uma string de um computador para outro, ou falasse mais de uma língua, isso meio que iria sempre funcionar. Mas é claro, assim que a Internet aconteceu, mover strings de um computador a outro se tornou um pouco mais comum, e toda uma confusão surgiu. Afortunadamente, o Unicode foi inventado.


Unicode

O Unicode foi um bravo esforço para criar um único conjunto de caracteres que incluíam cada sistema de escrita razoável no planeta e alguns de faz-de-conta como Klingon, também. Algumas pessoas ainda têm a falsa idéia de que Unicode é apenas um código de 16 bits onde cada caractere tem 16 bits e existem 65.536 caracteres possíveis. Na verdade, isso não está correto. Este é simplesmente o mito mais comum sobre Unicode, então se você pensava isso, não se sinta mal.

De fato, Unicode tem uma maneira diferente de pensar sobre caracteres, e você deve entender a maneira Unicode de pensar nas coisas ou nada fará sentido.

Até agora, nós assumimos que uma letra é codificada em alguns bits que podem ser gravados em disco ou em memória.

A -> 0100 0001

Em Unicode, uma letra é codificada em algo chamado código único (code point, no original), que é apenas um conceito teórico. Como este código único é representado em memória ou disco é uma história completamente diferente

Em Unicode, a letra A é um ideal platônico. Está apenas flutuando no paraíso:

A

Este A platônico é diferente de B, e diferente de a, mas a mesma coisa que A e A e A.

A idéia de que um A na fonte Times New Roman é o mesmo caractere que o A na fonte Helvetica, mas diferente de um "a" minúsculo não parece muito controversa, mas em algumas linguagens apenas descobrir o que uma letra é pode causar controvérsia. A letra alemã ß é uma letra real ou uma maneira enfeitada de escrever ss? Se a forma de uma letra muda no fim de uma palavra, ela se torna uma letra diferente? Hebreus dizem sim, árabes dizem não. De qualquer maneira, os caras espertos do consórcio Unicode têm entendido isso durante a última década, acompanhados de uma grande porção de debates políticos, e você não precisa se preocupar com isso. Eles já entenderam tudo.

Cada letra platônica em cada alfabeto é atribuída a um número mágico pelo consórcio Unicode que é escrito dessa maneira: U+0639. Este número mágico é chamado de código único. O U+ significa "Unicode" e os números são hexadecimais. U+0639 é a letra arábica Ain. A letra inglesa A seria U+0041. Você pode achar todas elas usando o utilitário charmap no Windows 2000/XP ou visitando o web site do Unicode.

Não há um real limite no número de letras que o Unicode pode definir e de fato eles foram além de 65.536, então nem todas as letras Unicode podem realmente ser espremidas em dois bytes, mas isso era um mito de qualquer maneira.

OK, então nós temos uma string:

Hello

que, em Unicode, corresponde a cinco códigos únicos:

U+0048 U+0065 U+006C U+006C U+006F.

Apenas um conjunto de códigos. Números, na verdade. Nós ainda não dissemos nada sobre como armazená-los em memória ou representá-lo em uma mensagem de e-mail.


Codificações

É aqui que as codificações entram.

A idéia mais antiga para a codificação Unicode, que levou ao mito sobre os dois bytes, era, ei, vamos apenas guardar esses números em 2 bytes cada. Então Hello se torna

00 48 00 65 00 6C 00 6C 00 6F

Certo? Não tão rápido! Também poderia ser:

48 00 65 00 6C 00 6C 00 6F 00 ?

Bem, tecnicamente, sim, eu acredito que poderia, e de fato, os primeiros implementadores queria poder armazenar os códigos Unicode em modo high-endian ou low-endian, qualquer um que fosse mais rápido na sua CPU, e então, dias se passaram e havia duas maneiras de armazenar Unicode. Então as pessoas foram forçadas à bizarra convenção de armazenar um FE FF no começo de cada string Unicode; isto é chamado de Unicode Byte Order Mark e se você trocar seus bytes altos e baixos esse código se torna FF FE e a pessoa lendo sua string saberá que ela deve trocar todos os outros bytes. Eca. Nem toda string Unicode por aí tem um byte order mark no começo.


Por um tempo pareceu que isso seria bom o bastante, mas os programadores reclamavam. "Olhem todos esses zeros!", diziam eles, pois eles eram americanos e estavam olhando textos em inglês que raramente usavam códigos acima de U+00FF. Além disso, eles eram hippies liberais da Califórnia. Se eles fossem texanos eles não se importariam em aceitar o dobro número de bytes. Mas aqueles fracotes californianos não podiam tolerar a idéia de dobrar a quantidade de armazenamento para strings, e de qualquer maneira, já existiam muitos documentos por aí usando várias codificações como ANSI e DBCS e quem iria converter todos eles? Eu? Por essa razão muitas pessoas decidiram ignorar o Unicode por vários anos e enquanto isso as coisas pioraram.

Dessa forma, foi inventado o brilhante conceito de UTF-8. UTF-8 era outro sistema para armazenamento de strings de códigos Unicode - aqueles números U+ mágicos -  em memória usando bytes de 8 bits. Em UTF-8, cada código de 0 a 127 é armazenado em um único byte. Apenas os códigos acima de 127 são guardados usando 2, 3, e de fato, até 6 bytes.

How UTF-8 works

Isso tinha a perfeita consequência de textos em inglês ficarem em UTF-8 exatamente do mesmo jeito que ficavam em ASCII, então os americanos nem mesmo perceberam algo errado. Apenas o resto do mundo teve se virar. Especificamente, Hello, que era igual a U+0048 U+0065 U+006C U+006C U+006F, seria guardado como 48 65 6C 6C 6F, que, contemplem!, é igual ao que era guardado em ASCII, e ANSI, e qualquer conjunto de caracteres OEM no planeta. Agora, se você é corajoso o suficiente para usar letras acentuadas ou letras gregas ou em Klingon, você tera que usar vários bytes para guardar um simples código UTF, mas os americanos nunca reparariam (UTF-8 também tem uma propriedade legal em que velhos processadores de strings ignorantes que queiram usar o byte 0 como null-terminator não truncarão strings).

Até agora eu lhe contei três maneiras de codificar Unicode. Os métodos tradicionais de armazenamento em dois bytes são chamados de UCS-2 (porque são 2 bytes) ou UTF-16 (porque são 16 bits), e você ainda tem que calcular se ele é UCS-2 high-endian ou low-endian. E há o popular e novo padrão UTF-8 que tem a boa propriedade de trabalhar respeitavelmente se você passar pela feliz coincidência de textos em inglês e programas descerebrados que estão completamente desavisados que existe alguma coisa além de ASCII.
Atualmente, existem um monte de outras maneiras de codificar Unicode. Há algo chamado UTF-7, que é muito parecido com UTF-8, mas garante que o maior bit sempre será zero, então, se você precisa enviar Unicode através de algum sistema de segurança de e-mail draconiano que pensa que 7 bits são o suficiente, obrigado você ainda pode ainda pode espremê-lo incólume. Há o UCS-4, que guarda cada código em 4 bytes, que tem a propriedade de cada código poder ser guardado no mesmo número de bytes, mas, céus!, nem mesmo os texanos seriam tão ousados a ponto de gastarem tanta memória.

E de fato, agora que você está vendo as coisas em termos de letras ideais platônicas que são representadas por códigos Unicode, estes códigos Unicode podem ser codificados em qualquer esquema de codificação mais antigo. Por exemplo, você poderia codificar a string Unicode Hello (U+0048 U+0065 U+006C U+006C U+006F) em ASCII, ou no velho OEM grego, ou na codificação ANSI-Hebreu, ou qualquer uma das várias centenas de codificações que foram inventadas até agora, com um complicador: algumas das letras podem não aparecer! Se não houver nenhum equivalente para o código Unicode que você está tentando representar na codificação em que você está tentando representá-lo, você normalmente conseguirá um ponto de interrogação: ? ou, se você for realmente bom, um quadrado. Qual que você conseguiu? -> �

Há centenas de codificações tradicionais que podem armazenar apenas alguns códigos corretamente e transformam todos os outros códigos em interrogações. Algumas codificações populares para textos em inglês são o Windows-1252(O padrão Windows 9x para línguas  da Europa Ocidental) e ISO-8859-1, também conhecido como Latin-1 (também útil para qualquer língua da Europa Ocidental). Mas tente armazenar letras hebréias ou russas nessas codificações e você conseguirá um monte de interrogações. UTF 7, 8, 16 e 32 têm a bela propriedade de serem capazes de armazenar qualquer código corretamente.



O Fato Mais Importante Sobre Codificações

Se você esqueceu completamente tudo o que eu expliquei, por favor, lembre de um fato extremamente importante. Não faz nenhum sentido ter uma string sem conhecer qual codificação ela usa. Você não pode mais enfiar sua cabeça no chão e achar que texto "plano" é ASCII.
Não existe essa coisa de Texto Plano.
Se você tem uma string, em memória, em um arquivo, ou numa mensagem de e-mail, você precisa saber em qual codificação ela está ou não poderá interpretar ou mostrá-la para os usuários corretamente.

Quase todos os problemas estúpidos como "meu website mostra um monte de coisas sem sentido" ou "ela não pode ler meus e-mails quando uso acentos" vêm de um programador ingênuo que não entendeu o simples fato de que se você não disser se uma string está codificada usando UTF-8, ASCII, ISO 8859-1 (Latin 1) ou Windows 1252 (Europeu Ocidental), você simplesmente não poderá mostrá-la corretamente ou nem mesmo saber onde ela termina. Existem centenas de codificações e acima do código 127, todas as apostas são arriscadas.

Como você mantém essa informação sobre qual codificação uma string usa? Bem, existem formas padrões de se fazer isso. Para uma mensagem de e-mail, é esperado que haja uma string no cabeçalho na forma
Content-Type: text/plain; charset="UTF-8"
Para uma página da Web, a idéia original era que o servidor Web retornaria um cabeçalho HTTP Content-Type similar juntamente com a página - não no HTML, mas em um dos cabeçalhos de resposta que são enviados antes da página HTML.

Mas isso acarreta alguns problemas. Suponha que você tenha um grande servidor Web com muitos sites e centenas de páginas mantidas por várias pessoas em diferentes línguas e usando qualquer codificação em que a cópia do Microsoft FrontPage que eles tinham achasse adequado para gerar a página. O servidor Web sozinho não sabe realmente em qual codificação cada arquivo foi escrito, então ele não poderia enviar o cabeçalho Content-Type.

Seria conveniente se você colocasse o Content-Type do HTML juntamente com o arquivo HTML, usando algum tipo de tag especial. É claro que isso deixou os puristas malucos... como você poderia ler o arquivo HTML sem saber em qual codificação ele está?! Por sorte, quase todas as codificações em uso comum fazem as mesmas coisa com os caracteres entre 32 e 127, então você sempre consegue chegar nessa parte do HTML sem usar letras diferentes:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
Mas essa meta tag realmente deve ser a primeira coisa na seção <head> por que quando o navegador web vê essa tag ele irá parar de processar a página e começará de novo após reinterpretar a página inteira usando a codificação que você especificou.
O que os navegadores web fazem se não acham nenhum Content-Type, seja nos cabeçalhos HTTP ou nas meta tags?

O Internet Explorer faz algo um tanto quanto interessante: ele tenta adivinhar, baseado na frequência em que vários bytes aparecem em textos típicos em codificações típicas de várias linguagens, qual linguagem e qual codificação está sendo usada. Pelo fato de várias páginas código antigas de 8 bits tenderem a colocar suas letras nacionais em diferentes limites entre 128 e 255, e desde que cada linguagem humana tem diferentes historigramas característicos de uso de letras, isso tem uma chance de funcionar.

É realmente estranho, mas parece funcionar o suficiente para que até mesmo desenvolvedores web ingênuos que nunca souberam que precisavam de um cabeçalho Content-Type olhem para suas páginas em um navegador e vejam tudo ecer corretamenteetam, até que, um dia, eles escrevem algo que não se adequa exatamente à frequência de distribuição de letras de sua língua nativa, e o IE decide que o texto é coreano e o mostra dessa maneira provando, penso eu, a idéia de da Lei de Postel sobre ser "conservador no que você emite e liberal no que aceita" não é um conselho de engenharia muito bom.

De qualquer maneira, o que faz o pobre leitor desse website, que foi escrito em búlgaro, mas parece ser coreano (e nem mesmo um coreano coeso)? Ele usa o menu Exibir | Codificação e tenta um monte de diferentes codificações (existem pelo menos uma dúzia para as línguas da Europa Oriental) até o texto aparecer claramente. Se, e somente se, ele souber disso, o que a maioria não sabe.

Na última versão do rsão do CityDesk, o software de manutenção de web sites publicado pela minha companhia, nós decidimos fazer tudo internamente em UCS-2 (dois bytes) Unicode, que é o que o Visual Basic, o COM, e o Windows NT/2000/XP usam como seus tipos string nativos. Em códigos C++ nós apenas declaramos strings como wchar_t ("wide char") ao invés de char e usamos a função wcs ao invés das funções str  (por exemplo, wcscat e wcslen ao invés de strcat e strlen). Para criar uma string UCS-2 literal em C você apenas coloca um L antes dessa maneira: L"Hello".
Quando o CityDesk publica a página web, ele a converte para a codificação UTF-8, que é bem suportada pelos navegadores web há muitos anos. É dessa maneira que todas as 29 versões de idiomae idioma do Joel on Software são codificadas, e eu ainda não ouvi uma única pessoa que teve algum problema visualizando-as.

Esse artigo está ficando particularmente longo, e eu não posso cobrir tudo o que há para saber sobre codificações de caracteres e Unicode, mas eu acredito que se você chegou até aqui, você sabe o suficiente para voltar a programar, usando antibióticos ao invés de sanguessugas e feitiços, uma tarefa que deixo para você agora.

Nenhum comentário:

Postar um comentário