Como adivinhar com segurança a codificação entre MacRoman, CP1252, Latin1, UTF-8 e ASCII

99

No trabalho, parece que nenhuma semana passa sem algum conluio, calamidade ou catástrofe relacionada à codificação. O problema geralmente deriva de programadores que pensam que podem processar de forma confiável um arquivo de “texto” sem especificar a codificação. Mas você não pode.

Portanto, decidiu-se, doravante, proibir que os arquivos tenham nomes que terminem em *.txtou *.text. O pensamento é que essas extensões enganam o programador casual em uma complacência maçante em relação às codificações, e isso leva a um manuseio impróprio. Seria quase melhor não ter extensão alguma, porque pelo menos você sabe que não sabe o que tem.

No entanto, não iremos tão longe. Em vez disso, espera-se que você use um nome de arquivo que termine na codificação. Assim, para arquivos de texto, por exemplo, estes seriam algo como README.ascii, README.latin1, README.utf8, etc.

Para arquivos que exigem uma extensão particular, se alguém pode especificar a codificação dentro do próprio arquivo, como em Perl ou Python, então você deve fazer isso. Para arquivos como o código-fonte Java, em que não existe tal recurso interno ao arquivo, você colocará a codificação antes da extensão, como SomeClass-utf8.java.

Para saída, UTF-8 deve ser fortemente preferido.

Mas, para entrada, precisamos descobrir como lidar com os milhares de arquivos em nossa base de código chamada *.txt. Queremos renomear todos eles para se adequarem ao nosso novo padrão. Mas não podemos olhar para todos eles. Portanto, precisamos de uma biblioteca ou programa que realmente funcione.

Eles estão em ASCII, ISO-8859-1, UTF-8, Microsoft CP1252 ou Apple MacRoman. Embora saibamos que podemos dizer se algo é ASCII, e temos uma boa mudança de saber se algo é provavelmente UTF-8, estamos perplexos quanto às codificações de 8 bits. Como estamos executando em um ambiente Unix misto (Solaris, Linux, Darwin) com a maioria dos desktops sendo Macs, temos alguns arquivos MacRoman irritantes. E isso é especialmente um problema.

Já faz algum tempo que procuro uma maneira de determinar programaticamente qual dos

  1. ASCII
  2. ISO-8859-1
  3. CP1252
  4. MacRoman
  5. UTF-8

um arquivo está em e eu não encontrei um programa ou biblioteca que possa distinguir com segurança entre as três codificações de 8 bits diferentes. Provavelmente temos mais de mil arquivos MacRoman sozinhos, então qualquer detector de charset que usamos deve ser capaz de farejá-los. Nada do que vi pode resolver o problema. Eu tinha grandes esperanças para a biblioteca do detector de conjuntos de caracteres ICU , mas ela não pode lidar com o MacRoman. Também examinei módulos para fazer o mesmo tipo de coisa em Perl e Python, mas sempre é a mesma história: sem suporte para detectar MacRoman.

O que estou procurando, portanto, é uma biblioteca ou programa existente que determine com segurança em qual dessas cinco codificações um arquivo está - e de preferência mais do que isso. Em particular, ele deve distinguir entre as três codificações de 3 bits que citei, especialmente a MacRoman . Os arquivos têm mais de 99% de texto no idioma inglês; existem alguns em outras línguas, mas não muitos.

Se for um código de biblioteca, nossa preferência de linguagem é em Perl, C, Java ou Python, e nessa ordem. Se for apenas um programa, não nos importamos em qual linguagem ele está, contanto que venha com o código-fonte completo, seja executado no Unix e seja totalmente livre.

Alguém mais teve esse problema de um zilhão de arquivos de texto legados codificados aleatoriamente? Se sim, como você tentou resolvê-lo e qual foi seu grau de sucesso? Este é o aspecto mais importante da minha pergunta, mas também estou interessado em saber se você acha que encorajar os programadores a nomear (ou renomear) seus arquivos com a codificação real em que esses arquivos estão nos ajudará a evitar o problema no futuro. Alguém já tentou impor isso em uma base institucional, e se sim, foi que bem sucedida ou não, e por quê?

E sim, compreendo perfeitamente por que não se pode garantir uma resposta definitiva dada a natureza do problema. Esse é especialmente o caso de arquivos pequenos, nos quais você não tem dados suficientes para continuar. Felizmente, nossos arquivos raramente são pequenos. Além do READMEarquivo aleatório , a maioria está na faixa de tamanho de 50k a 250k, e muitos são maiores. Qualquer coisa maior do que alguns K de tamanho está garantido em inglês.

O domínio do problema é a mineração de texto biomédica, então às vezes lidamos com corpora extensos e extremamente grandes, como todo o repositório de acesso aberto do PubMedCentral. Um arquivo bastante grande é o BioThesaurus 6.0, com 5,7 gigabytes. Este arquivo é especialmente irritante porque é quase todo UTF-8. No entanto, alguns estúpidos foram e enfiaram algumas linhas nele que estão em alguma codificação de 8 bits - Microsoft CP1252, eu acredito. Demora um pouco antes de você tropeçar naquele. :(

tchrist
fonte
Consulte stackoverflow.com/questions/4255305/… para uma solução
mpenkov

Respostas:

86

Primeiro, os casos fáceis:

ASCII

Se seus dados não contiverem bytes acima de 0x7F, então são ASCII. (Ou uma codificação ISO646 de 7 bits, mas esses são muito obsoletos.)

UTF-8

Se seus dados forem validados como UTF-8, você pode assumir com segurança que é UTF-8. Devido às regras de validação estritas do UTF-8, falsos positivos são extremamente raros.

ISO-8859-1 vs. windows-1252

A única diferença entre essas duas codificações é que ISO-8859-1 tem os caracteres de controle C1, onde windows-1252 tem os caracteres imprimíveis € ‚ƒ„… † ‡ ˆ Š ‹ŒŽ ''“ ”• –—˜ ™ š› œžŸ. Já vi muitos arquivos que usam aspas curvas ou travessões, mas nenhum que usa caracteres de controle C1. Portanto, nem se preocupe com eles, ou ISO-8859-1, apenas detecte o Windows-1252.

Isso agora deixa você com apenas uma pergunta.

Como você distingue o MacRoman do cp1252?

Isso é muito mais complicado.

Caracteres indefinidos

Os bytes 0x81, 0x8D, 0x8F, 0x90, 0x9D não são usados ​​no Windows-1252. Se ocorrerem, suponha que os dados sejam MacRoman.

Personagens idênticos

Os bytes 0xA2 (¢), 0xA3 (£), 0xA9 (©), 0xB1 (±), 0xB5 (µ) são iguais em ambas as codificações. Se esses forem os únicos bytes não ASCII, não importa se você escolhe MacRoman ou cp1252.

Abordagem estatística

Conte frequências de caracteres (NÃO bytes!) Nos dados que você sabe serem UTF-8. Determine os caracteres mais frequentes. Em seguida, use esses dados para determinar se os caracteres cp1252 ou MacRoman são mais comuns.

Por exemplo, em uma pesquisa que acabei de realizar em 100 artigos aleatórios da Wikipedia em inglês, os caracteres não ASCII mais comuns são ·•–é°®’èö—. Com base neste fato,

  • Os bytes 0x92, 0x95, 0x96, 0x97, 0xAE, 0xB0, 0xB7, 0xE8, 0xE9 ou 0xF6 sugerem Windows-1252.
  • Os bytes 0x8E, 0x8F, 0x9A, 0xA1, 0xA5, 0xA8, 0xD0, 0xD1, 0xD5 ou 0xE1 sugerem MacRoman.

Conte os bytes sugerindo cp1252 e os bytes sugerindo MacRoman e vá com o que for maior.

dan04
fonte
6
Aceitei sua resposta porque ninguém melhor se apresentou, e você fez um bom trabalho ao redigir exatamente os problemas que eu vinha tentando resolver. De fato, tenho programas para detectar esses bytes, embora você tenha cerca de duas vezes o número que eu mesmo sugeri.
cristão de
10
Finalmente consegui implementar isso. Acontece que a Wikipedia não é um bom dado de treinamento. De 1k artigos aleatórios en.wikipedia, sem contar a seção IDIOMAS, obtive 50k pontos de código não ASCII, mas a distribuição não é confiável: o ponto do meio e o marcador são muito altos, & c & c & c. Então, usei o corpus de acesso aberto PubMed totalmente UTF8, minerando + 14 milhões de pontos de código unASCII. Eu os uso para construir um modelo de frequência relativa de todas as codificações de 8 bits, mais sofisticadas que as suas, mas com base nessa ideia. Isso prova ser altamente preditivo da codificação para textos biomédicos, o domínio-alvo. Eu deveria publicar isso. Obrigado!
cristão de
5
Ainda não tenho nenhum arquivo MacRoman, mas o uso de CR como delimitadores de linha não seria um teste útil. Isso funcionaria para versões mais antigas do Mac OS, embora eu não saiba sobre o OS9.
Milliways
10

Mozilla nsUniversalDetector (vínculos Perl: Encode :: Detect / Encode :: Detect :: Detector ) é comprovado milhões de vezes.

Daxim
fonte
Mais documentação pode ser encontrada aqui: mozilla.org/projects/intl/detectorsrc.html , a partir daí, sugere que se você se aprofundar nos documentos, poderá encontrar os conjuntos de caracteres suportados
Joel Berger
@Joel: Eu investiguei a fonte. Foi uma pergunta retórica. x-mac-cyrillicé suportado, x-mac-hebrewé discutido detalhadamente nos comentários, x-mac-anything-elsenão é mencionado.
John Machin
@John Machin: estranho que o cirílico e o hebraico recebam um aceno de cabeça, mas nada mais. Eu estava apenas lançando outra fonte de documentação, não tinha lido mais, obrigado por fazer isso!
Joel Berger
7

Minha tentativa de tal heurística (assumindo que você descartou ASCII e UTF-8):

  • Se 0x7f a 0x9f não aparecerem, provavelmente é ISO-8859-1, porque esses são códigos de controle raramente usados.
  • Se 0x91 a 0x94 aparecerem no lote, provavelmente é Windows-1252, porque essas são as "aspas inteligentes", de longe os caracteres mais prováveis ​​de serem usados ​​em texto em inglês. Para ter mais certeza, você pode procurar pares.
  • Caso contrário, é MacRoman, especialmente se você vir muito de 0xd2 a 0xd5 (é onde estão as aspas tipográficas no MacRoman).

Nota:

Para arquivos como o código-fonte Java, onde não existe tal recurso interno ao arquivo, você colocará a codificação antes da extensão, como SomeClass-utf8.java

Não faça isso!!

O compilador Java espera que os nomes dos arquivos correspondam aos nomes das classes, portanto, renomear os arquivos tornará o código-fonte não compilável. O correto seria adivinhar a codificação e usar a native2asciiferramenta para converter todos os caracteres não ASCII em sequências de escape Unicode .

Michael Borgwardt
fonte
7
Stoopid kompilor! Não, não podemos dizer às pessoas que elas só podem usar ASCII; não estamos mais na década de 1960. Não seria um problema se houvesse uma anotação @encoding para que o fato de a fonte estar em uma codificação particular não fosse forçada a ser armazenada externamente ao código-fonte, uma deficiência realmente idiota do Java que nem Perl nem Python sofrem . Deve estar na fonte. Esse não é o nosso principal problema; são milhares de *.textarquivos.
tchrist
3
@tchrist: Na verdade, não seria tão difícil escrever seu próprio processador de anotações para suportar tal anotação. Ainda é uma omissão embaraçosa não tê-lo na API padrão.
Michael Borgwardt
Mesmo que o Java suportasse @encoding, isso não garantiria que a declaração de codificação estivesse correta .
dan04 de
4
@ dan04: Você pode dizer o mesmo sobre a declaração de codificação em XML, HTML ou em qualquer outro lugar. Mas, assim como com esses exemplos, se fosse definido na API padrão, a maioria das ferramentas que funcionam com o código-fonte (especialmente editores e IDEs) iria suportá-lo, o que evitaria de forma bastante confiável que as pessoas criassem acidentalmente arquivos cuja codificação de conteúdo não corresponda a declaração.
Michael Borgwardt
4
"O compilador Java espera que os nomes dos arquivos correspondam aos nomes das classes." Esta regra se aplica apenas se o arquivo definir uma classe pública de nível superior.
Matthew Flaschen
6

"Perl, C, Java ou Python, e nessa ordem": atitude interessante :-)

"temos uma boa chance de saber se algo provavelmente é UTF-8": na verdade, a chance de que um arquivo contendo texto significativo codificado em algum outro conjunto de caracteres que usa bytes com conjuntos de bits altos seja decodificado com sucesso como UTF-8 é muito pequena.

Estratégias UTF-8 (no idioma de preferência):

# 100% Unicode-standard-compliant UTF-8
def utf8_strict(text):
    try:
        text.decode('utf8')
        return True
    except UnicodeDecodeError:
        return False

# looking for almost all UTF-8 with some junk
def utf8_replace(text):
    utext = text.decode('utf8', 'replace')
    dodgy_count = utext.count(u'\uFFFD') 
    return dodgy_count, utext
    # further action depends on how large dodgy_count / float(len(utext)) is

# checking for UTF-8 structure but non-compliant
# e.g. encoded surrogates, not minimal length, more than 4 bytes:
# Can be done with a regex, if you need it

Depois de decidir que não é ASCII nem UTF-8:

Os detectores de charset de origem Mozilla que eu conheço não suportam MacRoman e em qualquer caso não fazem um bom trabalho em charsets de 8 bits, especialmente com o inglês porque AFAICT eles dependem de verificar se a decodificação faz sentido no dado idioma, ignorando os caracteres de pontuação e com base em uma ampla seleção de documentos nesse idioma.

Como outros observaram, você realmente só tem os caracteres de pontuação de conjunto de bits altos disponíveis para distinguir entre cp1252 e macroman. Eu sugiro treinar um modelo do tipo Mozilla em seus próprios documentos, não Shakespeare ou Hansard ou a Bíblia KJV, e levando todos os 256 bytes em consideração. Presumo que seus arquivos não tenham nenhuma marcação (HTML, XML, etc) neles - isso distorceria as probabilidades algo chocante.

Você mencionou arquivos que são principalmente UTF-8, mas não conseguem decodificar. Você também deve suspeitar muito de:

(1) arquivos que são supostamente codificados em ISO-8859-1, mas contêm "caracteres de controle" na faixa de 0x80 a 0x9F, inclusive ... isso é tão predominante que o rascunho do padrão HTML5 diz para decodificar TODOS os streams HTML declarados como ISO-8859 -1 usando cp1252.

(2) arquivos que decodificam OK como UTF-8, mas o Unicode resultante contém "caracteres de controle" no intervalo U + 0080 a U + 009F inclusive ... isso pode resultar da transcodificação de cp1252 / cp850 (já vi acontecer!) / Etc arquivos de "ISO-8859-1" a UTF-8.

Contexto: Eu tenho um projeto de tarde de domingo úmido para criar um detector de conjunto de caracteres baseado em Python que é orientado a arquivo (em vez de orientado para a web) e funciona bem com conjuntos de caracteres de 8 bits, incluindo legacy ** nalguns como cp850 e cp437. Ainda não está nem perto do horário nobre. Estou interessado em arquivos de treinamento; seus arquivos ISO-8859-1 / cp1252 / MacRoman são tão "desimpedidos" quanto você espera que a solução de código de qualquer pessoa seja?

John Machin
fonte
1
a razão para a ordenação do idioma é o ambiente. A maioria de nossos aplicativos principais tende a ser em java e os utilitários secundários e alguns aplicativos são em perl. Temos um pouco de código aqui e ali que está em python. Eu sou principalmente um programador C e perl, pelo menos por primeira escolha, então eu estava procurando uma solução java para conectar em nossa biblioteca de aplicativos, ou uma biblioteca perl para o mesmo. Se fosse C, eu poderia construir uma camada de cola XS para conectá-la à interface perl, mas nunca fiz isso em python antes.
tchrist
3

Como você descobriu, não existe uma maneira perfeita de resolver esse problema, porque sem o conhecimento implícito sobre qual codificação um arquivo usa, todas as codificações de 8 bits são exatamente as mesmas: Uma coleção de bytes. Todos os bytes são válidos para todas as codificações de 8 bits.

O melhor que você pode esperar é algum tipo de algoritmo que analise os bytes e, com base nas probabilidades de um determinado byte sendo usado em um determinado idioma com uma determinada codificação, adivinhará qual codificação o arquivo usa. Mas isso tem que saber qual idioma o arquivo usa, e se torna completamente inútil quando você tem arquivos com codificações mistas.

Por outro lado, se você sabe que o texto em um arquivo está escrito em inglês, é improvável que você note qualquer diferença na codificação que decidir usar para esse arquivo, já que as diferenças entre todas as codificações mencionadas estão todas localizadas em as partes das codificações que especificam caracteres normalmente não usados ​​no idioma inglês. Você pode ter alguns problemas onde o texto usa formatação especial ou versões especiais de pontuação (CP1252 tem várias versões dos caracteres de aspas, por exemplo), mas para a essência do texto provavelmente não haverá problemas.

Epcylon
fonte
1

Se você puder detectar todas as codificações EXCETO para macroman, então seria lógico supor que aquelas que não podem ser decifradas estão em macroman. Em outras palavras, basta fazer uma lista dos arquivos que não puderam ser processados ​​e tratá-los como se fossem macroman.

Outra maneira de classificar esses arquivos seria fazer um programa baseado em servidor que permite aos usuários decidir qual codificação não está distorcida. Claro, seria dentro da empresa, mas com 100 funcionários fazendo alguns por dia, você terá milhares de arquivos feitos em nenhum momento.

Finalmente, não seria melhor apenas converter todos os arquivos existentes em um único formato e exigir que os novos arquivos estivessem nesse formato.

Eric Pauley
fonte
5
Engraçado! Quando li este comentário pela primeira vez depois de ser interrompido por 30 minutos, li "macroman" como "macro man" e não fiz a conexão com o MacRoman até que fiz uma pesquisa por aquela string para ver se o OP a havia mencionado
Adrian Pronk
+1 esta resposta é interessante. não tenho certeza se é uma boa ou má ideia. Alguém consegue pensar em uma codificação existente que também pode passar despercebida? é provável que haja um no futuro?
nome de usuário de
1

Alguém mais teve esse problema de um zilhão de arquivos de texto legados codificados aleatoriamente? Se sim, como você tentou resolvê-lo e qual foi seu grau de sucesso?

Atualmente, estou escrevendo um programa que traduz arquivos em XML. Ele precisa detectar automaticamente o tipo de cada arquivo, que é um superconjunto do problema de determinar a codificação de um arquivo de texto. Para determinar a codificação, estou usando uma abordagem bayesiana. Ou seja, meu código de classificação calcula uma probabilidade (probabilidade) de que um arquivo de texto tenha uma codificação específica para todas as codificações que entende. O programa então seleciona o decodificador mais provável. A abordagem bayesiana funciona assim para cada codificação.

  1. Defina a probabilidade inicial ( anterior ) de que o arquivo esteja na codificação, com base nas frequências de cada codificação.
  2. Examine cada byte por vez no arquivo. Pesquise o valor do byte para determinar a correlação entre o valor do byte presente e um arquivo que realmente está nessa codificação. Use essa correlação para calcular uma nova probabilidade ( posterior ) de que o arquivo está na codificação. Se você tiver mais bytes para examinar, use a probabilidade posterior desse byte como a probabilidade anterior ao examinar o próximo byte.
  3. Quando você chega ao final do arquivo (na verdade, eu olho apenas para os primeiros 1024 bytes), a probabilidade que você tem é a probabilidade de o arquivo estar na codificação.

Verifica-se que Bayes teorema torna-se muito fácil de fazer se ao invés de probabilidades de computação, você calcular conteúdo de informação , que é o logaritmo das probabilidades : info = log(p / (1.0 - p)).

Você terá que calcular a probabilidade initail priori, e as correlações, examinando um corpus de arquivos que você classificou manualmente.

Raedwald
fonte