Java: Como determinar a codificação correta do conjunto de caracteres de um stream

Com referência ao seguinte encadeamento: Aplicativo Java: Não é possível ler corretamente o arquivo codificado iso-8859-1

Qual é a melhor maneira de determinar programaticamente a codificação correta do conjunto de caracteres de um stream de input / arquivo?

Eu tentei usar o seguinte:

File in = new File(args[0]); InputStreamReader r = new InputStreamReader(new FileInputStream(in)); System.out.println(r.getEncoding()); 

Mas em um arquivo que eu sei estar codificado com ISO8859_1, o código acima produz ASCII, o que não é correto, e não me permite renderizar corretamente o conteúdo do arquivo de volta ao console.

Eu usei essa biblioteca, semelhante ao jchardet para detectar a codificação em Java: http://code.google.com/p/juniversalchardet/

Você não pode determinar a codificação de um stream de bytes arbitrário. Essa é a natureza das codificações. Uma codificação significa um mapeamento entre um valor de byte e sua representação. Então, toda codificação “poderia” ser o certo.

O método getEncoding () retornará a codificação que foi configurada (leia o JavaDoc ) para o stream. Não vai adivinhar a codificação para você.

Alguns streams informam qual codificação foi usada para criá-los: XML, HTML. Mas não um stream de bytes arbitrário.

De qualquer forma, você pode tentar adivinhar uma codificação por conta própria, se for necessário. Cada idioma tem uma frequência comum para cada caractere. Em inglês, o caracter aparece com muita frequência, mas aparecerá muito raramente. Em um stream ISO-8859-1 geralmente não há caracteres 0x00. Mas um stream UTF-16 tem muitos deles.

Ou: você poderia perguntar ao usuário. Já vi aplicativos que apresentam um fragment do arquivo em diferentes codificações e peço para você selecionar o arquivo “correto”.

veja isto: http://site.icu-project.org/ (icu4j) eles têm bibliotecas para detectar o charset do IOStream poderia ser simples assim:

 BufferedInputStream bis = new BufferedInputStream(input); CharsetDetector cd = new CharsetDetector(); cd.setText(bis); CharsetMatch cm = cd.detect(); if (cm != null) { reader = cm.getReader(); charset = cm.getName(); }else { throw new UnsupportedCharsetException() } 

Aqui estão meus favoritos:

TikaEncodingDetector

Dependência:

  org.apache.any23 apache-any23-encoding 1.1  

Amostra:

 public static Charset guessCharset(InputStream is) throws IOException { return Charset.forName(new TikaEncodingDetector().guessEncoding(is)); } 

GuessEncoding

Dependência:

  org.codehaus.guessencoding guessencoding 1.4 jar  

Amostra:

  public static Charset guessCharset2(File file) throws IOException { return CharsetToolkit.guessEncoding(file, 4096, StandardCharsets.UTF_8); } 

Você pode certamente validar o arquivo para um conjunto de caracteres específico, decodificando -o com um CharsetDecoder e observando os erros de “input mal formada” ou “caractere não mapeável”. Claro, isso só informa se um charset está errado; Não lhe diz se está correto. Para isso, você precisa de uma base de comparação para avaliar os resultados decodificados, por exemplo, você sabe de antemão se os caracteres estão restritos a algum subconjunto ou se o texto adere a algum formato estrito? A linha inferior é que a detecção de charset é adivinhação sem qualquer garantia.

As bibliotecas acima são simples detectores de lista de materiais que, claro, só funcionam se houver uma lista de materiais no início do arquivo. Dê uma olhada em http://jchardet.sourceforge.net/, que analisa o texto

Eu encontrei uma boa biblioteca de terceiros que pode detectar a codificação real: http://glaforge.free.fr/wiki/index.php?wiki=GuessEncoding

Eu não testei extensivamente, mas parece funcionar.

Se você usa ICU4J ( http://icu-project.org/apiref/icu4j/ )

Aqui está o meu código:

  String charset = "ISO-8859-1"; //Default chartset, put whatever you want byte[] fileContent = null; FileInputStream fin = null; //create FileInputStream object fin = new FileInputStream(file.getPath()); /* * Create byte array large enough to hold the content of the file. * Use File.length to determine size of the file in bytes. */ fileContent = new byte[(int) file.length()]; /* * To read content of the file in byte array, use * int read(byte[] byteArray) method of java FileInputStream class. * */ fin.read(fileContent); byte[] data = fileContent; CharsetDetector detector = new CharsetDetector(); detector.setText(data); CharsetMatch cm = detector.detect(); if (cm != null) { int confidence = cm.getConfidence(); System.out.println("Encoding: " + cm.getName() + " - Confidence: " + confidence + "%"); //Here you have the encode name and the confidence //In my case if the confidence is > 50 I return the encode, else I return the default value if (confidence > 50) { charset = cm.getName(); } } 

Lembre-se de colocar todo o try catch necessário.

Espero que isso funcione pra você.

Qual biblioteca usar?

No momento em que escrevo, eles são três bibliotecas que emergem:

  • GuessEncoding
  • ICU4j
  • juniversalchardet

Eu não incluo o Apache Any23 porque ele usa o ICU4j 3.4 sob o capô.

Como saber qual detectou o charset correto (ou o mais próximo possível)?

É impossível certificar o charset detectado pelas bibliotecas acima. No entanto, é possível perguntar por sua vez e marcar a resposta retornada.

Como marcar a resposta retornada?

Cada resposta pode receber um ponto. Quanto mais pontos uma resposta tiver, mais confiança terá o conjunto de caracteres detectado. Este é um método simples de pontuação. Você pode elaborar outros.

Existe algum código de amostra?

Aqui está um snippet completo implementando a estratégia descrita nas linhas anteriores.

 public static String guessEncoding(InputStream input) throws IOException { // Load input data long count = 0; int n = 0, EOF = -1; byte[] buffer = new byte[4096]; ByteArrayOutputStream output = new ByteArrayOutputStream(); while ((EOF != (n = input.read(buffer))) && (count <= Integer.MAX_VALUE)) { output.write(buffer, 0, n); count += n; } if (count > Integer.MAX_VALUE) { throw new RuntimeException("Inputstream too large."); } byte[] data = output.toByteArray(); // Detect encoding Map encodingsScolors = new HashMap<>(); // * GuessEncoding updateEncodingsScores(encodingsScores, new CharsetToolkit(data).guessEncoding().displayName()); // * ICU4j CharsetDetector charsetDetector = new CharsetDetector(); charsetDetector.setText(data); charsetDetector.enableInputFilter(true); CharsetMatch cm = charsetDetector.detect(); if (cm != null) { updateEncodingsScores(encodingsScores, cm.getName()); } // * juniversalchardset UniversalDetector universalDetector = new UniversalDetector(null); universalDetector.handleData(data, 0, data.length); universalDetector.dataEnd(); String encodingName = universalDetector.getDetectedCharset(); if (encodingName != null) { updateEncodingsScores(encodingsScores, encodingName); } // Find winning encoding Map.Entry maxEntry = null; for (Map.Entry e : encodingsScolors.entrySet()) { if (maxEntry == null || (e.getValue()[0] > maxEntry.getValue()[0])) { maxEntry = e; } } String winningEncoding = maxEntry.getKey(); //dumpEncodingsScolors(encodingsScolors); return winningEncoding; } private static void updateEncodingsScolors(Map encodingsScolors, String encoding) { String encodingName = encoding.toLowerCase(); int[] encodingScore = encodingsScolors.get(encodingName); if (encodingScore == null) { encodingsScolors.put(encodingName, new int[] { 1 }); } else { encodingScore[0]++; } } private static void dumpEncodingsScolors(Map encodingsScolors) { System.out.println(toString(encodingsScolors)); } private static String toString(Map encodingsScolors) { String GLUE = ", "; StringBuilder sb = new StringBuilder(); for (Map.Entry e : encodingsScolors.entrySet()) { sb.append(e.getKey() + ":" + e.getValue()[0] + GLUE); } int len = sb.length(); sb.delete(len - GLUE.length(), len); return "{ " + sb.toString() + " }"; } 

Melhorias: O método guessEncoding lê o stream de input inteiramente. Para grandes streams de input, isso pode ser uma preocupação. Todas essas bibliotecas leriam todo o stream de input. Isso implicaria um grande consumo de tempo para detectar o charset.

É possível limitar o carregamento inicial de dados a alguns bytes e realizar a detecção do conjunto de caracteres apenas nesses poucos bytes.

Se você não sabe a codificação dos seus dados, não é tão fácil de determinar, mas você pode tentar usar uma biblioteca para adivinhá-la . Além disso, há uma questão semelhante .

Tanto quanto sei, não há biblioteca geral neste contexto para ser adequado para todos os tipos de problemas. Portanto, para cada problema, você deve testar as bibliotecas existentes e selecionar a melhor que satisfaça as restrições do seu problema, mas, freqüentemente, nenhuma delas é apropriada. Nestes casos, você pode escrever seu próprio detector de codificação! Como eu escrevi …

Eu escrevi uma ferramenta meta-java para detectar a codificação de charset de páginas da Web em HTML, usando o IBM ICU4j e o Mozilla JCharDet como componentes internos. Aqui você pode encontrar minha ferramenta, por favor leia a seção README antes de mais nada. Além disso, você pode encontrar alguns conceitos básicos deste problema no meu artigo e em suas referências.

Abaixo, forneci alguns comentários úteis que experimentei no meu trabalho:

  • A detecção de charset não é um processo infalível, porque é essencialmente baseado em dados estatísticos e o que realmente acontece é adivinhar não detectar
  • O icu4j é a principal ferramenta neste contexto da IBM,
  • Tanto o TikaEncodingDetector quanto o Lucene-ICU4j estão usando o icu4j e sua precisão não teve uma diferença significativa da qual o icu4j em meus testes (no máximo% 1, como eu me lembro)
  • O icu4j é muito mais geral que o jchardet, o icu4j é um pouco tendencioso para as codificações da família IBM, enquanto o jchardet é fortemente influenciado pelo utf-8
  • Devido ao uso generalizado de UTF-8 no mundo HTML; O jchardet é uma escolha melhor do que o icu4j em geral, mas não é a melhor escolha!
  • O icu4j é excelente para codificações específicas do leste asiático, como EUC-KR, EUC-JP, SHIFT_JIS, BIG5 e as codificações da família GB
  • Tanto o icu4j quanto o jchardet são um fracasso ao lidar com páginas HTML com as codificações Windows-1251 e Windows-1256. O Windows-1251, também conhecido como cp1251, é amplamente utilizado para idiomas baseados em cirílico, como o russo e o Windows-1256, também conhecido como cp1256, e é amplamente usado para o árabe
  • Quase todas as ferramentas de detecção de codificação estão usando methods estatísticos, portanto a precisão da saída depende fortemente do tamanho e do conteúdo da input
  • Algumas codificações são essencialmente as mesmas apenas com diferenças parciais, portanto, em alguns casos, a codificação detectada ou detectada pode ser falsa, mas ao mesmo tempo ser verdadeira! Quanto a Windows-1252 e ISO-8859-1. (consulte o último parágrafo na seção 5.2 do meu artigo)

Para arquivos ISO8859_1, não há uma maneira fácil de diferenciá-los do ASCII. No entanto, para arquivos Unicode, geralmente é possível detectar isso com base nos primeiros bytes do arquivo.

Arquivos UTF-8 e UTF-16 incluem uma Byte Order Mark (BOM) no início do arquivo. A BOM é um espaço sem quebra de largura zero.

Infelizmente, por razões históricas, o Java não detecta isso automaticamente. Programas como o Notepad irão verificar o BOM e usar a codificação apropriada. Usando o unix ou o Cygwin, você pode verificar a BOM com o comando file. Por exemplo:

 $ file sample2.sql sample2.sql: Unicode text, UTF-16, big-endian 

Para Java, sugiro que você verifique este código, que detectará os formatos de arquivo comuns e selecionará a codificação correta: Como ler um arquivo e especificar automaticamente a codificação correta

Uma alternativa ao TikaEncodingDetector é usar o Tika AutoDetectReader .

 Charset charset = new AutoDetectReader(new FileInputStream(file)).getCharset(); 

Na planície Java:

 final String[] encodings = { "US-ASCII", "ISO-8859-1", "UTF-8", "UTF-16BE", "UTF-16LE", "UTF-16" }; List lines; for (String encoding : encodings) { try { lines = Files.readAllLines(path, Charset.forName(encoding)); for (String line : lines) { // do something... } break; } catch (IOException ioe) { System.out.println(encoding + " failed, trying next."); } } 

Essa abordagem tentará as codificações uma por uma até que uma funcione ou ficemos sem elas. (BTW minha lista de codificações tem apenas esses itens porque eles são as implementações de conjuntos de caracteres necessários em cada plataforma Java, https://docs.oracle.com/javase/9/docs/api/java/nio/charset/Charset.html )

Você pode escolher o conjunto de caracteres apropriado no Construtor :

 new InputStreamReader(new FileInputStream(in), "ISO8859_1");