quarta-feira, 1 de agosto de 2012

Por que UTF-8 e não ASCII para o Português? (PARTE II)

Continuação do post, originalmente feita na lista Python-Brasil:

Vou tentar de novo, a thread já falou de 3 coisas diferentes:
1. Codificação a usar em programas Python: por que UTF-8 é altamente recomendável
2. Codificações em geral e problemas causados e resolvidos por ela
3. Um bug do Python no Windows, quando o prompt é configurado para página 65001

Vou tentar explicar para todo mundo, pois é um tópico recorrente.

Mas antes de voltar nestes tópicos, temos que voltar a arquivos.

Tentativa 2:

Para entender a codificação de caracteres, temos que entender do que se trata.
Quem programa conhece o código ASCII, que mapeia cada caractere do alfabeto latino, códigos de controle e alguns símbolos em 7 bits.
É 7 bits, por isso vai de 0 a 127. Isso funcionava bem na década de 60... quando se enxugava bit para salvar tempo de transmissão de dados e armazenamento, muito antes dos torrents e afins :-D. A internacionalização disto ainda não estava em foco, aliás, ASCII significa Código Padrão Americano para Intercâmbio de Informação. Americano, diga-se estado-unidense.

Nossos computadores hoje usam 8 bits por byte, mas nem sempre foi assim. A IBM e depois a Microsoft entre outras empresas aproveitaram o bit extra para completar 8 bits e adicionaram mais 128 caracteres, uma vez que cada bit adicionado dobra a capacidade de representação, potência de 2, etc. Esses caracteres foram usados para representar aqueles caracteres como bordas, símbolos e alguns acentos. Quem usou DOS, lembra bem disso.

Como 256 símbolos não são suficientes para representar todos os caracteres de todas as línguas, a IBM e outros fabricantes, criaram páginas de código específicas para cada país ou língua. Assim, a página de código 437 (cp437) continha símbolos de desenho e caracteres acentuados como o ç e o ñ, usados em  línguas como o francês e o espanhol, que atendem às necessidades da América do Norte e algumas línguas européias. Um exemplo de língua não atendida completamente é o português, pois na página 437 não tem ã nem õ. Esse problema foi resolvido com a página 850, que troca alguns caracteres de desenho e símbolos pouco usados por acentos de várias línguas do ocidente europeu.
Depois de muita história, voltemos a como isso muda nossos bytes.

No código ASCII, a letra A maiúscula é representada pelo número 65 em decimal ou 0x41 em hexadecimal. O B é a letra seguinte, então deram o número 66 ou 0x42.

Se você tem um arquivo com apenas duas letras AB uma após a outra, ele vai ocupar (seus dados) dois bytes no disco. O conteúdo binário dos dados do arquivo em disco são a sequência de bytes 0x41 e 0x42 (AB ou 65 e 66). É muito importante entender esta codificação antes de continuar lendo. Se você não entende que um A é guardado como o número 65, esqueça UTF-8... será preciso reler ou pedir ajuda a um amigo. Antigamente, curso de informática começava com sistema binário e tabela ASCII, hoje o primeiro programa já baixa páginas na Internet... mas a teoria de base é deixada para trás.

Tanto na página 437 quanto na 850, toda a tabela ASCII, ou seja, seus 127 caracteres foram preservados. Assim, nosso arquivo AB é mostrado do mesmo jeito em ambas as páginas. A diferença começa aparecer quando usamos os caracteres diferentes entre elas.

Agora imagine que adicionamos um Ã, usando a página 850, pois escrevemos num computador configurado para português:
ABÃ
No disco teríamos 3 bytes:
0x41 0x42 0xC3
Na página 850, o à é traduzido para o símbolo 199, ou 0xC3 em hexadecimal.
Agora, imagine que enviamos esse arquivo para um amigo americano, que abre num computador que usa a página 437. O conteúdo no disco continua o mesmo: 0x41 0x42 0xC3, mas o que ele vê na tela é:
AB
Para onde foi nosso Ã? Para lugar algum... ela estará lá, se usarmos a mesma página de código, ou seja a 850, que usamos para escrever.
Com apenas 3 bytes, já podemos ver o que pode acontecer... agora imagine com arquivos inteiros !
Como os computadores se espalharam pelo mundo, várias páginas foram criadas para o russo, grego, etc. Imagine então escrever um arquivo com partes em grego, partes em russo e chinês... uma catástrofe.

O que uma tabela de codificação faz é mapear um valor numérico para um símbolo gráfico ou caractere. Você escolhe a tabela que quer usar, mas para fazer uma tradução entre tabelas precisa saber qual a tabela usada para codificar os dados atuais e para qual tabela você quer traduzir.
Outro problema é que línguas como o chinês precisam de mais de 256 símbolos para um texto normal, uma vez que o alfabeto deles é muito maior que o nosso. Surgem então tabelas de múltiplos bytes, onde mais de um byte era usado para cada caractere. Ainda assim, você precisava saber qual tabela multibyte foi usada... repetindo a confusão. Quem já trabalhou com Windows em C++ usando MBCS sabe a dor que isso causa...

Uma das soluções para múltiplas linguas é criar um tabelaço que resolveria todos os problemas, foi criado o UNICODE. Desta forma, todas as línguas seriam representadas. O problema é que para conter todos os símbolos, vários bytes teriam que ser utilizados até para caracteres latinos.
Assim, cada letra, numa simplificação seria representada por 2 bytes (simplificação, porque 2 bytes não são suficientes, pois temos mais de 65536 caracteres no Unicode !). Continuando, nosso A em unicode é representado como 0x00 0x41 e o B como 0x00 0x42. É cada letra passa a ser representada por dois bytes e um deles é o temido 0x00 ! O Ã ficou na posição 0x00 0xC3. No disco:
ABÃ
ficaram assim:
0x00 0x41 0x00 0x42 0x00 0xC3

Agora usamos 6 bytes para 3 caracteres. Ainda nem falamos de byte order ou de BOM... isso fica para outro dia :-D

Com 6 bytes para 3 letras, logo apareceram problemas de armazenamento de dados, pois os arquivos começaram a dobrar de tamanho e a tomar 2x mais tempo para serem transmitidos... em teoria. Uma forma mais enxuta de representar estes caracteres foi desenvolvida: o UTF-8.
Usando a mesma tabela base do Unicode, mas introduzindo um esquema de troca de páginas, ABÃ em UTF-8 são escritos no disco como:
0x40 0x41 0xC3 0x83

O Ã foi traduzido como 0xC3 0x83 !
Passamos de 6 para 4 bytes, sem perder a capacidade de escrever em praticamente qualquer língua!

O que acontece no Python. Um arquivo py de apenas uma linha para imprimir ABÃ pode ser escrito como:
print "ABÃ"

No disco ele será gravado se usarmos um editor utf-8 para escrevê-lo:
0x70 0x72 0x69 0x6E 0x74 0x20 0x22 0x41 0x42 0xC3 0x83 0x22 0x0D 0x0A

São esses bytes que o Python.exe vai ler.
Em UTF-8 estes bytes seriam traduzidos para:
0x70 p
0x72 r
0x69 i
0x6E n
0x74 t
0x20 -> espaço em branco
0x22 "
0x41 A
0x42 B
0xC3 --> primeiro byte do Ã
0x83 --> segundo byte do Ã
0x22 "
0x0D --> CR
0x0A --> LF


Mas o interpretador Python não sabe disso !

C:\Users\nilo>\Python27\python.exe Desktop\test.py
  File "Desktop\test.py", line 1
SyntaxError: Non-ASCII character '\xc3' in file Desktop\test.py on line 1, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details

Ele diz: Non-ASCII e depois \xc3 que é outra forma de dizer 0xC3
Por que? No Python 2, o arquivo foi lido como ASCII, tendo apenas  símbolos de 0 a 127. 0xC3 é 199, ou seja, fora da tabela ASCII, daí o erro.
Neste caso, para resolver temos que colocar o # coding: utf-8
O programa fica assim:

# coding: utf-8
print "ABÃ"

Que em hexa é:
0x23 0x20 0x63 0x6F 0x64 0x69 0x6E 0x67 0x3A 0x20 0x75 0x74 0x66 0x2D 0x38 0x0D 0x0A # coding: utf-8
0x70 0x72 0x69 0x6E 0x74 0x20 0x22 0x41 0x42 0xC3 0x83 0x22 0x0D 0x0A                print "ABÃ"


Veja que a segunda linha continua exatamente a mesma coisa.
Mas ao executar-mos temos:

C:\Users\nilo>\Python27\python.exe Desktop\test.py
ABÃ


Não deu erro, mas também não imprimiu o que queríamos. Vejamos o que deu errado.
Primeiro a página de código do meu console:
C:\Users\nilo>chcp
Active code page: 850


Ué... mas a página 850 suporta o Ã. Por que o Python não imprimiu corretamente?
Simplesmente porque o cabeçalho # coding: utf-8 apenas indica em que codificação você escreveu o programa. Isso faz com ele consiga ler seu código, mesmo com acentos, desde que você tenha também usado um editor de textos em UTF-8, como diz o cabeçalho. Se você usar um cabeçalho diferente da real codificação do arquivo, os bytes em disco não vão mudar e os caracteres serão traduzidos usando tabelas incorretas. Isso é muito difícil de perceber apenas olhando, por isso eu recomendo o editor Hex. Com o tempo fica claro de fazer até no PsPad.

Ainda temos que resolver o problema da saída. No Python 2, as strings não são traduzidas de uma tabela para outra. Esta ambiguidade foi corrigida, no Python 3, com o tipo byte... mas ai já é outra história. Se você quer que o Python traduza de uma tabela para outra, use o prefixo u na string, de forma a indicar que é uma string unicode, codificada no formato utf-8, como dito no cabeçalho do programa.
Como strings comuns não tem tradução de página automaticamente, a sequência 0xc3 0x83 é mostrada na tela pela tabela da cp 850, que utiliza apenas um byte por caractere. Logo, dois bytes, dois caracteres. Um para o 0xc3 e outro para 0x83.

Vejamos o programa com o u antes das aspas:
# coding: utf-8
print u"ABÃ"

No disco:
0x23 0x20 0x63 0x6F 0x64 0x69 0x6E 0x67 0x3A 0x20 0x75 0x74 0x66 0x2D 0x38 0x0D 0x0A # coding: utf-8
0x70 0x72 0x69 0x6E 0x74 0x20 0x75 0x22 0x41 0x42 0xC3 0x83 0x22 0x0D 0x0A           print u"ABÃ"

Veja que a única diferença é 0x75 (a letra u), mas o resultado é diferente:

C:\Users\nilo>\Python27\python.exe Desktop\test.py
ABÃ

Agora saiu corretamente! Por que?  Porque o Python sabe que a string é unicode e que a saída do console no meu Windows usa a cp850. Então ele converte os bytes durante a impressão para que sejam apresentados corretamente.

Por isso é importante entender a codificação do seu arquivo e a codificação do console, banco de dados, etc. Você precisa ajudar o programa a se comportar bem.

Vejamos agora o erro do cabeçalho inválido, onde declaramos UTF-8, mas nosso editor grava usando a cp1252 do Windows:
Visualmente o arquivo tem o mesmo conteúdo:
# coding: utf-8
print u"ABÃ"
Mas no disco:
0x23 0x20 0x63 0x6F 0x64 0x69 0x6E 0x67 0x3A 0x20 0x75 0x74 0x66 0x2D 0x38 0x0D 0x0A # coding: utf-8
0x70 0x72 0x69 0x6E 0x74 0x20 0x75 0x22 0x41 0x42 0xC3 0x22 0x0D 0x0A
                print u"ABÃ"

Resulta em:
C:\Users\nilo>\Python27\python.exe Desktop\test.py
  File "Desktop\test.py", line 2
    print u"ABÃ"
SyntaxError: (unicode error) 'utf8' codec can't decode byte 0xc3 in position 0:
unexpected end of data


Por que? Bem, se você comparar a segunda linha em hexadecimal com a do exemplo anterior, verá que na cp1252, o à foi traduzido como 0xC3, ou seja, apenas um byte. Mas declaramos no cabeçalho que estaríamos usando UTF-8! O interpretador Python é um programa e confia no que declaramos. Ele lê o arquivo como se fosse UTF-8 e acha o 0xC3 que não é apenas um caractere, mas o marcador de início de troca de página. Depois de ler o 0xC3 ele espera o outro byte desta página, mas acha as aspas (0x22). 0xC3 0x22 é uma sequência inválida em UTF-8 e o interpretador explode com uma exceção de codificação.

Voltando ao início do tópico:
1. Codificação a usar em programas Python: por que UTF-8 é altamente recomendável
Por que você pode enviar seus programas para outros computadores (linux, mac, windows) e usar acentos, evitando problemas futuros. Mas só funciona se seu cabeçalho expressar a codificação real usado no arquivo. Caso contrário não funciona.

2. Codificações em geral e problemas causados e resolvidos por ela
Acho que o início da mensagem responde essa.

3. Um bug do Python no Windows, quando o prompt é configurado para página 65001
Além das páginas da IBM, a Microsoft tem também as suas. Entre elas a cp1252 e a cp 65001 para o UTF8. Se você configurar e se somente se você configurar seu console para usar a página 650001, utf-8, o resultado é o seguinte:

C:\Users\nilo>chcp 65001
Active code page: 65001

C:\Users\nilo>\Python27\python.exe Desktop\test.py
Traceback (most recent call last):
  File "Desktop\test.py", line 2, in
    print u"ABÃ"
LookupError: unknown encoding: cp65001

C:\Users\nilo>\Python32\python.exe Desktop\test.py
Fatal Python error: Py_Initialize: can't initialize sys standard streams
LookupError: unknown encoding: cp65001

This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.

É só neste caso, bem específico e desnecessário para o português que temos um bug aberto ainda no Python 3.3.
Não é um bug do Windows, pois funciona em Java, C# e C. É apenas a forma que o interpretador trata a cp65001 diferente de utf8.

Nenhum comentário: