Java: como determinar a codificação de conjunto de caracteres correta de um fluxo


140

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

Qual é a melhor maneira de determinar programaticamente a codificação correta do conjunto de caracteres de um fluxo de entrada / 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 que está codificado com ISO8859_1, o código acima gera ASCII, que não está correto, e não me permite renderizar corretamente o conteúdo do arquivo de volta ao console.


11
Eduard está certo: "Você não pode determinar a codificação de um fluxo de bytes arbitrário". Todas as outras propostas oferecem maneiras (e bibliotecas) de adivinhar melhor. Mas no final, eles ainda são palpites.
Mihai Nita

9
Reader.getEncodingretorna a codificação que o leitor foi configurado para usar, que no seu caso é a codificação padrão.
Karol S

Respostas:


70

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


6
Achei que isso era mais preciso: jchardet.sourceforge.net (eu estava testando em documentos do idioma da Europa Ocidental codificados na ISO 8859-1, windows-1252, utf-8)
Joel

1
Este juniversalchardet não funciona. Ele fornece UTF-8 na maioria das vezes, mesmo que o arquivo seja 100% codificado em Windows 1212.
Cérebro

1
juniversalchardet agora está no GitHub .
deamon

Ele não detecta windows-1250 da Europa Oriental
Bernhard Dobler

Tentei seguir o snippet de código para detecção no arquivo de " cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt ", mas fiquei nulo conforme o conjunto de caracteres detectado. UniversalDetector ud = novo UniversalDetector (nulo); byte [] bytes = FileUtils.readFileToByteArray (novo arquivo (arquivo)); ud.handleData (bytes, 0, bytes.length); ud.dataEnd (); detectadoCharset = ud.getDetectedCharset ();
Rohit Verma

105

Você não pode determinar a codificação de um fluxo 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. Portanto, toda codificação "poderia" estar certa.

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

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

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

Ou: você pode perguntar ao usuário. Já vi aplicativos que apresentam um trecho do arquivo em diferentes codificações e solicitam que você selecione o "correto".


18
Isso realmente não responde à pergunta. O op provavelmente deve estar usando docs.codehaus.org/display/GUESSENC/Home ou icu-project.org/apiref/icu4j/com/ibm/icu/text/… ou jchardet.sourceforge.net
Christoffer Hammarström

23
Então, como meu editor, o bloco de notas ++ sabe como abrir o arquivo e me mostrar os caracteres certos?
mmm

12
@ Hamidam, é por sorte que ele mostra os caracteres certos. Quando ele adivinha incorretamente (e geralmente o faz), existe uma opção (Menu >> Codificação) que permite alterar a codificação.
Pacerier 17/01/12

15
@ Eduard: "Portanto, toda codificação" pode "estar certa". não muito certo. Muitas codificações de texto têm vários padrões inválidos, que são um sinalizador de que o texto provavelmente não é essa codificação. De fato, dados os dois primeiros bytes de um arquivo, apenas 38% das combinações são UTF8 válidas. As chances de os 5 primeiros pontos de código serem válidos UTF8 por acaso são menores que 0,77%. Da mesma forma, UTF16BE e LE são geralmente facilmente identificados pelo grande número de bytes zero e onde estão.
Mooing Duck

38

verifique isto: http://site.icu-project.org/ (icu4j) eles têm bibliotecas para detectar charset a partir 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()
}

2
Eu tentei, mas falhou bastante: criei 2 arquivos de texto no eclipse, ambos contendo "öäüß". Um conjunto para codificação iso e outro para utf8 - ambos são detectados como utf8! Então, eu tentei um arquivo protegido em algum lugar no meu hd (windows) - este foi detectado corretamente ("windows-1252"). Então eu criei dois novos arquivos no hd, um editado com o editor e outro com o bloco de notas ++. em ambos os casos "Big5" (chinês) foi detectado!
Dermoritz 29/09/11

2
EDIT: Ok, eu deveria verificar cm.getConfidence () - com o meu pequeno "äöüß", a confiança é 10. Portanto, eu tenho que decidir qual confiança é boa o suficiente - mas isso é absolutamente bom para esse empreendimento (detecção de charset)
dermoritz

1
Link direto para código de exemplo: userguide.icu-project.org/conversion/detection
james.garriss

27

Aqui estão os meus favoritos:

TikaEncodingDetector

Dependência:

<dependency>
  <groupId>org.apache.any23</groupId>
  <artifactId>apache-any23-encoding</artifactId>
  <version>1.1</version>
</dependency>

Amostra:

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

GuessEncoding

Dependência:

<dependency>
  <groupId>org.codehaus.guessencoding</groupId>
  <artifactId>guessencoding</artifactId>
  <version>1.4</version>
  <type>jar</type>
</dependency>

Amostra:

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

2
Nota: O TikaEncodingDetector 1.1 é realmente um invólucro fino em torno da classe ICU4J 3.4 CharsetDectector .
Stephan

Infelizmente, ambas as bibliotecas não funcionam. Em um caso, ele identifica um arquivo UTF-8 com o Umlaute alemão como ISO-8859-1 e US-ASCII.
Cérebro

1
@Brain: Seu arquivo testado está realmente no formato UTF-8 e inclui uma lista técnica ( en.wikipedia.org/wiki/Byte_order_mark )?
Benny Neugebauer

@BennyNeugebauer, o arquivo é um UTF-8 sem BOM. Eu verifiquei com o Notepad ++, também alterando a codificação e afirmando que o "Umlaute" ainda está visível.
Cérebro

13

Certamente, você pode validar o arquivo para um conjunto de caracteres específico decodificando -o com um CharsetDecodere observando os erros de "entrada malformada" ou "caractere não mappável". Obviamente, isso apenas informa se um conjunto de caracteres está errado; não 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 segue algum formato estrito? A linha inferior é que a detecção de charset é uma adivinhação sem garantias.


12

Qual biblioteca usar?

No momento da redação deste artigo, são três as bibliotecas que emergem:

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

Como saber qual deles detectou o direito caracteres (ou o mais próximo possível)?

É impossível certificar o conjunto de caracteres detectado por cada uma das bibliotecas acima. No entanto, é possível perguntar por vez e pontuar a resposta retornada.

Como pontuar a resposta retornada?

Cada resposta pode ser atribuída a 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 exemplo?

Aqui está um trecho 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<String, int[]> encodingsScores = 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<String, int[]> maxEntry = null;
    for (Map.Entry<String, int[]> e : encodingsScores.entrySet()) {
        if (maxEntry == null || (e.getValue()[0] > maxEntry.getValue()[0])) {
            maxEntry = e;
        }
    }

    String winningEncoding = maxEntry.getKey();
    //dumpEncodingsScores(encodingsScores);
    return winningEncoding;
}

private static void updateEncodingsScores(Map<String, int[]> encodingsScores, String encoding) {
    String encodingName = encoding.toLowerCase();
    int[] encodingScore = encodingsScores.get(encodingName);

    if (encodingScore == null) {
        encodingsScores.put(encodingName, new int[] { 1 });
    } else {
        encodingScore[0]++;
    }
}    

private static void dumpEncodingsScores(Map<String, int[]> encodingsScores) {
    System.out.println(toString(encodingsScores));
}

private static String toString(Map<String, int[]> encodingsScores) {
    String GLUE = ", ";
    StringBuilder sb = new StringBuilder();

    for (Map.Entry<String, int[]> e : encodingsScores.entrySet()) {
        sb.append(e.getKey() + ":" + e.getValue()[0] + GLUE);
    }
    int len = sb.length();
    sb.delete(len - GLUE.length(), len);

    return "{ " + sb.toString() + " }";
}

Melhorias: OguessEncoding método lê o fluxo de entrada completamente. Para fluxos de entrada grandes, isso pode ser uma preocupação. Todas essas bibliotecas liam todo o fluxo de entrada. Isso implicaria um grande consumo de tempo para detectar o conjunto de caracteres.

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


8

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


18
apenas na ponta, mas não há "acima" neste site - considere indicar as bibliotecas às quais você está se referindo.
McDowell

6

Até onde eu sei, não existe uma biblioteca geral nesse contexto que seja adequada 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 geralmente nenhuma delas é apropriada. Nestes casos, você pode escrever seu próprio detector de codificação! Como eu escrevi ...

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

Abaixo, fiz alguns comentários úteis que experimentei em 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 é supor não detectar
  • icu4j é a principal ferramenta nesse contexto da IBM, imho
  • O TikaEncodingDetector e o Lucene-ICU4j estão usando o icu4j e sua precisão não teve uma diferença significativa da qual o icu4j nos meus testes (no máximo% 1, pelo que me lembro)
  • O icu4j é muito mais geral do que o jchardet, o icu4j é um pouco influenciado pelas codificações da família IBM, enquanto o jchardet é fortemente influenciado pelo utf-8
  • Devido ao amplo uso do UTF-8 no mundo HTML; jchardet é uma escolha melhor do que icu4j no geral, mas não é a melhor escolha!
  • O icu4j é ótimo 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 desastre ao lidar com páginas HTML com as codificações Windows-1251 e Windows-1256. O Windows-1251, também conhecido como cp1251, é amplamente usado em idiomas baseados em cirílico, como russo, e o Windows-1256, também conhecido como cp1256, é amplamente usado em árabe.
  • Quase todas as ferramentas de detecção de codificação estão usando métodos estatísticos, portanto, a precisão da saída depende fortemente do tamanho e do conteúdo da entrada
  • Algumas codificações são essencialmente as mesmas apenas com diferenças parciais; portanto, em alguns casos, a codificação adivinhada ou detectada pode ser falsa, mas ao mesmo tempo verdadeira! Como no Windows-1252 e ISO-8859-1. (consulte o último parágrafo na seção 5.2 do meu artigo)


5

Se você usa o 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ê.


IMO, esta resposta é perfeita. Se você deseja usar o ICU4j, tente este: stackoverflow.com/a/4013565/363573 .
Stephan


2

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

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

Infelizmente, por razões históricas, o Java não detecta isso automaticamente. Programas como o Bloco de notas verificarão a lista técnica e usarão a codificação apropriada. Usando unix ou Cygwin, você pode verificar a lista técnica 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 irá detectar os formatos de arquivo comuns e selecionar a codificação correta: Como ler um arquivo e especificar automaticamente a codificação correta


15
Nem todos os arquivos UTF-8 ou UTF-16 têm uma BOM, pois não é necessária, e a UTF-8 BOM é desencorajada.
Christoffer Hammarström

1

Uma alternativa ao TikaEncodingDetector é usar o Tika AutoDetectReader .

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

O Tike AutoDetectReader usa o EncodingDetector carregado com o ServiceLoader. Quais implementações de EncodingDetector você usa?
Stephan

-1

Em Java simples:

final String[] encodings = { "US-ASCII", "ISO-8859-1", "UTF-8", "UTF-16BE", "UTF-16LE", "UTF-16" };

List<String> 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 a uma até que uma funcione ou que as esgotemos. (BTW, minha lista de codificações possui apenas esses itens porque são as implementações de conjuntos de caracteres necessárias em todas as plataformas Java, https://docs.oracle.com/javase/9/docs/api/java/nio/charset/Charset.html )


Mas a ISO-8859-1 (entre muitas outras que você não listou) sempre terá êxito. E, claro, isso é apenas um palpite, que não pode recuperar os metadados perdidos, essenciais para a comunicação de arquivos de texto.
Tom Blodget

Olá @TomBlodget, você está sugerindo que a ordem das codificações seja diferente?
Andres

3
Estou dizendo que muitos "trabalharão", mas apenas um está "certo". E você não precisa testar a ISO-8859-1 porque ela sempre "funcionará".
Tom Blodget

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.