Stanislaw PusepPublicado em 01/03/2011
Em primeiro lugar: qual é o objetivo desse artigo, já que a documentação oficial do Perl é acompanhada de perlunitut e perlunifaq? Simples: um amplo espectro de erros relacionados à codificação observados em nossos scripts, desde um inofensivo Wide character in print at ... e até o enigmático Parsing of undecoded UTF-8 will give garbage when decoding entities at .... Se, por um lado, para quem quer que o tenha implementado, o suporte de unicode do Perl é trivial, para nós, reles mortais, aquelas linhas de código da documentação oficial mais parecem fórmulas mágicas que copiamos e colamos até que encontremos uma que dê um resultado aceitável no nosso caso específico (atire a primeira pedra...). Não que isso seja um critério de complexidade, mas o fato do módulo Encode ocupar mais de 10MB instalado já indica que aí tem coisa.
Agora, cronologicamente: o suporte nativo a unicode surgiu na versão 5.6 do Perl, lançada em meados de 2000. Passou por várias correções e refinamentos, sendo que eu, pessoalmente, só confiaria em unicode do Perl 5.10, lançado 7 anos (!) depois. Outra comparação duvidosa: PHP que, no momento da escrita desse artigo, está na versão 5.3.5, não possui suporte nativo a unicode. E, convenhamos, a sua extensão mbstring
faz um excelente trabalho.
Então por que toda essa confusão no Perl? Vamos por partes.
The unicode Standard 6.0
possui cerca de 109 mil caracteres.
Entretanto, de fato, o padrão UCS-2
, adotado no Windows NT, era quase exatamente isso: 2 bytes por caractere, com 63,488 possibilidades no total.
Depois, no Windows 2000, passou-se a usar o UTF-16
, que na maioria dos casos tinha 2 bytes por caractere, mas podia ter mais (assim como o utf8
aparenta "representar os caracteres com acentos com 2 bytes"). utf8
sempre tem 2 bytes
utf8
, deve-se empregar o pragma use utf8
utf8
.
Aliás, programar Perl em um sistema utf8
e não usar esse pragma é a origem dos outros 49% da confusão: por razões históricas, o Perl "entende" os scripts como latin1
por default.
Portanto, enquanto você enxerga "®" no seu código, o Perl enxergará "®".
No melhor caso, isso não muda absolutamente nada, por que todo o resto do seu sistema "espera" por utf8
e está pouco se lixando para o que o Perl "acha".
Já no pior caso, um script aonde regexp /\bPreço:\s+(\d+)/i
é crítica deixa de funcionar em um sistema configurado como iso-8859-1
. use Encode
, use encoding '...'
, use open '...'
, etc.
use bytes
e/ou binmode(FH, ':bytes')!
:)
#!/usr/bin/perl -w
use strict;
use utf8;
use Data::Dumper;
use Text::Unaccent;
my $texto = 'À noite, vovô Kowalsky vê o ímã cair no pé do pingüim queixoso e vovó põe açúcar no chá de tâmaras do jabuti feliz.';
my @token;
push @token, unac_string('utf8', lc $_) foreach (split /\W+/, $texto);
print Dumper \@token;
#!/usr/bin/perl -w
use strict;
use Data::Dumper;
my $texto = 'À noite, vovô Kowalsky vê o ímã cair no pé do pingüim queixoso e vovó põe açúcar no chá de tâmaras do jabuti feliz.';
$texto =~ y/ÇçÑñÃÕãõÂÊÎÔÛâêîôûÀÈÌÒÙàèìòùÁÉÍÓÚáéíóúÄËÏÖÜäëïöü/ccnnaoaoaeiouaeiouaeiouaeiouaeiouaeiouaeiouaeiou/;
my @token = split /\W+/, lc $texto;
print Dumper \@token;
utf8
. Se existisse a necessidade do segundo tratar dados em utf8
, antes de usar a transliteração teria que dar um jeito de converter manualmente (sequência de vários s///g
) os caracteres de utf8
para latin1
, o que seria deveras laboroso e ineficiente (imagina se precisasse de contemplar os quase 200 caracteres do iso-8859-1
?!). iso-8859-1
, iso-8859-15
e win-1252
são tudo a mesma coisa
:P
:)
iso-8859-1
e latin1
, são sinônimos. Trocando em miúdos: um alfabeto ideal (no sentido platônico), com potencial para representar todo e qualquer sistema de escrita real ou fictício que já existiu ou virá a existir, desde o tibetano arcaico e até vogon.
Para isso, foi reservado um índice não de 256 e nem de 65,536, mas de 2**31 posições (code points). Dentro desse espaço, existe uma divisão por categorias e scripts, além de um mapeamento de equivalência (por exemplo: unicode "sabe" que "©" é aproximadamente equivalente a "C", e "ö" é similar a "o").
Porém, o mais importante a saber é que unicode ainda pode ser abstraído como um índice unidimensional: quem mexe com XML (e HTML) já viu entities no formato ⁱ
, que renderiza como "¹", e nada mais é do que o caractere unicode com índice decimal 8305, e, respectivamente, hexadecimal "\x{2071}"
. Um formato padronizado, independente da linguagem (XML ou Perl, no caso) é U+2071
.
Voltando ao Perl; este trata os dados textuais como unicode, e armazena internamente como utf8
.
Cada caractere de unicode pode ter índice de até 31 bits, mas, historicamente, linguagens de programação e markup usam apenas os primeiros 7 bits do ASCII, suficientes para representar os textos em inglês.
Então, utf8
nada mais é do que uma codificação capaz de acomodar todos os 31 bits do unicode, porém mantendo o backward compatibility com ASCII. Simplificando, funciona da seguinte forma
(sendo que "¹" representa bit setado, "0" - não-setado, e "x" é o espaço reservado para o índice do caractere codificado):
BYTE 1 BYTE 2 BYTE 3 BYTE 4 BYTE 5 BYTE 6
0xxxxxxx
110xxxxx 10xxxxxx
1110xxxx 10xxxxxx 10xxxxxx
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
Assim, se o índice do caractere a ser codificado couber em 7 bits, será utilizado apenas 1 byte, mantendo a compatibilidade com ASCII. Se couber em 13 bits, serão 2 bytes; 16 bits - 3 bytes; e assim por diante.
Para dar uma pista de que o arquivo não é ASCII, existe o BOM (Byte Order Mark): sequência de 3 bytes (0xEF, 0xBB, 0xBF)
no começo do arquivo, no caso do utf8
. "Coincidentemente", essa sequência traduz para caractere unicode U+FEFF
, também conhecido como zero-width non-breaking space.
OBSERVAÇÃO: apesar do espaço reservado ser de 2**31, nem todos os índices decodificam para caracteres unicode válidos!!!
Voltando ao exemplo anterior, o caractere U+2071
("¹"), representado em utf8
, é sequencia de bytes (0xE2, 0x81, 0xB1)
. Eis um aparente paradoxo: string "ímã" tem 3 caracteres, mas, codificada em utf8
, ocupa 5 bytes! Outro ponto importante é que utf8
é interpretável em uma só direção, já que é o primeiro byte da sequencia que determina o tamanho do bloco todo. Então, para que a coisa toda dê certo, e substr("ímã", 1, 1)
retorne "m", Perl armazena metadados juntamente com strings em utf8
. Vejamos:
#!/usr/bin/perl
use utf8;
use Devel::Peek;
my $str = "ímã";
Dump $str;
Retorna:
SV = PV(0x1c61b78) at 0x1c80850
REFCNT = 1
FLAGS = (PADMY,POK,pPOK,UTF8)
PV = 0x1c7b150 "\303\255m\303\243"\0 [UTF8 "\x{ed}m\x{e3}"]
CUR = 5
LEN = 8
Para comparação, se $str = "teste"
, temos:
SV = PV(0x75fb78) at 0x77e850
REFCNT = 1
FLAGS = (PADMY,POK,pPOK)
PV = 0x779150 "teste"\0
CUR = 5
LEN = 8
Obviamente, este flag de utf8
é legível e configurável a partir do código-fonte; apesar de perlunifaq condenar isso (com razão ou não, não vem ao caso).
Então, concluindo sobre a relação do Perl com utf8
. O Perl a partir da versão 5.8 armazena e trata strings unicode internamente codificadas com utf8
. Entretanto, externamente, o Perl, por default e visando backward compatibility, emprega... latin1
no código-fonte e "literal bytes" para I/O! Ou seja: é de se esperar encrenca em OS moderno, tal como Ubuntu, a menos que os devidos cuidados sejam tomados.
Recapitulando: Perl, apesar de empregar unicode (e com grande êxito) e armazenar strings como utf8
internamente, é um tanto quanto inconsistente ao se comunicar com o "mundo exterior".
O próprio interpretador espera que o código-fonte esteja em latin1
, a menos que seja empregado o pragma use utf8
(o que não implica que I/O deixe de ser visto como "literal bytes"!!!).
Portanto, a codificação do texto precisa ser especificada explicitamente.
Frequentemente, a entrada de dados se dá por meio dos filehandles. Se o seu Perl foi compilado com suporte a perlio (e quem, em sã consciência, não o faria?), este é o jeito mais natural de codificar (até mesmo encriptar!) os dados. É importante destacar que o Perl não seleciona layer de utf8
automaticamente mesmo que o arquivo a ser aberto contenha o Byte Order Mark. Então, para abrir um arquivo em utf8
:
open(my $fh, '<:encoding(UTF-8)', 'lista.txt');
utf8
em especial possui um atalho:
open(my $fh, '<:utf8', 'lista.txt');
Se o arquivo tem o BOM ou qualquer outra marcação de codificação (como no caso do XML), é possível abrir o arquivo, verificar a codificação só depois aplicar layer de codificação:
open(my $fh, '<:bytes', # $header="<$fh" ':bytes' ':utf8'! 'lista.xml'); do inverso my o é>;
# exemplo tosquíssimo de detector de codificação para XML relativamente bem-formatado:
if ($header =~ /\butf-?8\b/i) {
binmode $fh, ':utf8';
} elsif ($header =~ /\b(iso-?8859-?1|latin1)\b/i) {
binmode $fh, ':latin1';
}
Algumas vezes, é interessante configurar um layer de codificação padrão. Para isso, temos o pragma open:
use open IO => ':encoding(utf8)';
Por outro lado, muitas vezes sabe-se que a entrada é sempre em utf8
, enquanto a saída depende da configuração do locale do sistema em questão. Neste caso:
use open IN => ':utf8';
use open OUT => ':locale';
Aliás, o pragma open só atua em open()/readpipe()/afins que se situam no mesmo escopo léxico. Para propagar o efeito para os handles STDIN/STDOUT/STDERR, é necessário acrescentar:
use open ':std';
E, por fim, temos a clássica situação em que precisamos mexer com um código-macarronada herdado de um sistema arcaico. Para ajudar, o Perl tem o argumento -C
que controla o emprego dos layers. Por exemplo: perl -CSDA script_das_trevas.pl --buscar=açaí
vai forçar STD(IN|OUT|ERR) e todos os demais filehandles a serem utf8
, além de interpretar @ARGV
como utf8
. Ver perlrun para maiores detalhes, mas lembre-se: isso é uma gambiarra.
Já que nem sempre é possível empregar PerlIO (por exemplo, o clássico erro Parsing of undecoded UTF-8 will give garbage when decoding entities at ... se deve ao fato do LWP::UserAgent pegar HTML como octets, mas HTML::Parser esperar como entrada unicode), pode-se usar o Encode para fazer a conversão diretamente em memória, "ad hoc". Sabendo a codificação de uma string "crua", ela primeiro deve ser "únicodificada":
$string = decode('iso-8859-1', $octets);
No caso, $octets
é o que veio de fora e $string
será uma cópia com qual o Perl pode trabalhar normalmente, fazendo match com /\w+/
, ou ucfirst($string)
, ou whatever.
O processo inverso seria:
$octets = encode('iso-8859-1', $string);
E eis que surge um problema bastante comum e chato: digamos que você baixou uma página HTML pelo protocolo HTTP. Se o servidor remoto teve a bondade de especificar a codificação na tag "Content-type", seja nos headers, seja nos <META
> ótimo.
Caso contrário, forma-se o caso do ovo e da galinha: para processar o dado, precisa saber a codificação, e, para saber a codificação, precisa processar o dado.
Muitos citam o Encode::Guess nessa hora, entretanto, ele é bastante incompatível com a realidade dos falantes do idioma português. Isso por que uma string em utf8
é considerada pelo autor do módulo como ambígua: pode ser tanto utf8
quanto latin1
. De um modo geral, faz sentido: "®" pode ser tanto a letra  seguida de símbolo de marca registrada, quanto apenas marca registrada. Mas, convenhamos, é pouco provável o emprego de "®" em um texto human-readable. Por outro lado, "çã" está definitivamente fora do padrão utf8
.
Então, segue aqui o script que exemplifica a heurística da diferenciação entre latin1
e utf8
. Neste caso particular, é um típico "html2text.pl":
#!/usr/bin/perl
use strict;
# Para ter certeza absoluta de que nenhum warning de 'Wide character' escapou
use warnings 'all';
# Somente indica que este arquivo .pl está na codificação UTF-8!!!
use utf8;
# Ignora codificação de entrada
use open IN => ':raw';
# Usa a codificação de saída padrão do sistema
use open OUT => ':locale';
use Encode;
use HTML::Entities;
use Regexp::Common qw(balanced comment);
# Lê arquivo inteiro de uma vez, ao invés de ler linha por linha
local $/ = undef;
while (my $buf = <>) {
# Se não for UTF-8 válido, assume ISO-8859-1
my $encoding = detect_utf8(\$buf) ? 'utf8' : 'iso-8859-1';
# Processa a codificação
$buf = decode($encoding, $buf);
# Trata tags HTML
$buf =~ s%$RE{comment}{HTML}%%gos;
$buf =~ s%<(script|style)\b[^>]*?>.*?% %gis;
$buf =~ s%$RE{balanced}{-parens=>'<>'}% %gios;
$buf = decode_entities($buf);
# Extrai somente as palavras, normaliza e imprime
print "\L$1 " while $buf =~ m%([\w\-]+)%g;
}
print "\n";
# detect_utf8(\$string)
# Recebe referência para escalar com string a ser analisada e retorna:
# 0 - $string tem caracteres de 8 bits, não valida como UTF-8;
# 1 - $string tem somente caracteres de 7 bits;
# 2 - $string tem caracteres de 8 bits, valida como UTF-8.
# Algoritmo original em PHP: http://www.php.net/manual/en/function.utf8-encode.php#85293
# Fórmula da conversão: http://index.t.tiscali.nl/t876506/utf8tbl.html#algo
sub detect_utf8 {
use bytes;
my $str = shift;
my $d = 0;
my $c = 0;
my $b = 0;
my $bits = 0;
my $len = length ${$str};
for (my $i = 0; $i <$len; $c="ord(substr(${$str}," $i++) $i, ($c 1)); if {>= 128) {
$d++;
if ($c >= 254) {
return 0;
} elsif ($c >= 252) {
$bits = 6;
} elsif ($c >= 248) {
$bits = 5;
} elsif ($c >= 240) {
$bits = 4;
} elsif ($c >= 224) {
$bits = 3;
} elsif ($c >= 192) {
$bits = 2;
} else {
return 0;
}
if (($i + $bits) > $len) {
return 0;
}
while ($bits > 1) {
$i++;
$b = ord(substr(${$str}, $i, 1));
if (($b <128) ($b ||> 191)) {
return 0;
}
$bits--;
}
}
}
return $d ? 2 : 1;
}
128)>$len;>(script|style)\b[^>
A função detect_utf8()
, que "emprestei" dos comentários da página de documentação online do PHP, faz uma verificação aproximada se o "protocolo" de utf8
de guardar 31 bits por caractere é respeitado. Se não é, das duas uma: os octets são ascii
, se nenhum exceder 7 bits por byte; ou a codificação é "qualquer outra coisa". Se estamos trabalhando com textos em português, a chance da "outra coisa" ser latin1
é de 99.9%. Se não for suficiente, é possível combinar o poder do Encode::Guess com esta muleta; ou mesmo elaborar uma heurística que leve em consideração a frequência da ocorrência das letras em um texto. Esse último caso é realmente o último caso: além da complexidade, a quantidade de falsos-positivos chega a ser irritante. Quem mais se lembra como era navegar na Web lá em 1996? Ao menos para mim, apareciam letrinhas árabes no lugar dos acentos :P
, também conhecido como non-breaking space, NÃO É A MESMA COISA QUE UM ESPAÇO!!! Ou seja: não dá match com /\s/
. Então, infelizmente, tem que tratá-lo como \xa0
. utf8
:
use DBI;
my $dbh = DBI->connect("DBI:mysql:${database}:${hostname}", $username, $password)
or die "Erro de conexão: $DBI::errstr";
$dbh->{'mysql_enable_utf8'} = 1;
$dbh->do('SET NAMES utf8');
latin1
por padrão. Se o seu código-fonte está em utf8
, precisa explicitar essa codificação também para seções POD:
=encoding utf8
perldoc -t
ajuda, apesar de perder "frescuras" da formatação. perl -MEncode -le 'print for Encode->encodings(":all")'
latin1
não está aí, apesar do iso-8859-1
estar. Isto por que é um alias; assim como utf-8
é alias para utf8
. LANG=pt_BR.ISO-8859-1 perl extractor.pl
LANG=en_US.UTF-8 perl extractor.pl
locale -a
revelou que só tenho en_US. Aí que entra locale-gen, citando a página manual do mesmo:
Compiled locale files take about 50MB of disk space, and most users only need few locales. In order to save disk space, compiled locale files are not distributed in the locales package, but selected locales are automatically generated when this package is installed by running the locale-gen program.
sudo locale-gen pt_BR.ISO-8859-1
cat enc-iso8859-1.txt | iconv -f l1 -t utf8 | perl ...
latin1
para utf8
(l1
é abreviação para latin1
).
;)
curl http://www.uol.com.br | tidy --input-encoding latin1 --output-encoding utf8 | perl ...
latin1
na sua index.t, essa linha "normalizou" para utf8
. E, se o HTML for particularmente chato, --output-encoding ascii
converterá os caracteres em numeric entities, os quais parsers do Perl tratam particularmente bem.
utf8
, mesmo que entradas/saídas sejam qualquer outra coisa. Motivo: qualquer coisa converte para utf8
; já a recíproca não é válida. #!/usr/bin/perl
use strict;
use utf8;
use warnings 'all';
use open IO => ':locale';
utf8
. utf8
, é melhor evitar copiar & colar a torto e a direito, especialmente para dentro das expressões regulares! Pois veja só:
s/[\---__-]/-/g;
U+002D
HYPHEN-MINUS U+2010
HYPHEN U+2011
NON-BREAKING HYPHEN U+2012
FIGURE DASH U+2013
EN DASH U+2212
MINUS SIGN s/[\x2d\x{2010}-\x{2013}\x{2212}]/-/g;
:P
EBCDIC
que tive oportunidade de usar), e propositalmente omiti as coisas que, na prática, atrapalharam mais do que ajudaram (como Encode::_utf8_on()
).Stanislaw Pusep stas@sysd.org