Java: cómo determinar la codificación correcta de charset de una secuencia

140

Con referencia al siguiente hilo: Aplicación Java: no se puede leer el archivo codificado iso-8859-1 correctamente

¿Cuál es la mejor manera de determinar mediante programación la codificación correcta del juego de caracteres de un flujo de entrada / archivo?

He intentado usar lo siguiente:

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

Pero en un archivo que sé que está codificado con ISO8859_1, el código anterior produce ASCII, que no es correcto, y no me permite reproducir correctamente el contenido del archivo en la consola.

Joel
fuente
11
Eduard tiene razón, "No se puede determinar la codificación de una secuencia de bytes arbitraria". Todas las demás propuestas le ofrecen formas (y bibliotecas) para adivinar mejor. Pero al final todavía son conjeturas.
Mihai Nita
9
Reader.getEncodingdevuelve la codificación que el lector fue configurado para usar, que en su caso es la codificación predeterminada.
Karol S

Respuestas:

70

He usado esta biblioteca, similar a jchardet para detectar la codificación en Java: http://code.google.com/p/juniversalchardet/

Luciano Fiandesio
fuente
66
Descubrí que esto era más preciso: jchardet.sourceforge.net (estaba probando en documentos de idiomas de Europa occidental codificados en ISO 8859-1, windows-1252, utf-8)
Joel
1
Este juniversalchardet no funciona. Ofrece UTF-8 la mayor parte del tiempo, incluso si el archivo está 100% codificado en Windows-1212.
Cerebro
1
juniversalchardet ahora está en GitHub .
Deamon
No detecta ventanas de Europa del Este-1250
Bernhard Döbler
Intenté seguir el fragmento de código para la detección en el archivo de " cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt " pero obtuve un valor nulo como conjunto de caracteres detectado. UniversalDetector ud = nuevo UniversalDetector (nulo); byte [] bytes = FileUtils.readFileToByteArray (nuevo archivo (archivo)); ud.handleData (bytes, 0, bytes.length); ud.dataEnd (); detectCharset = ud.getDetectedCharset ();
Rohit Verma
105

No puede determinar la codificación de una secuencia de bytes arbitraria. Esta es la naturaleza de las codificaciones. Una codificación significa un mapeo entre un valor de byte y su representación. Entonces cada codificación "podría" ser la correcta.

El método getEncoding () devolverá la codificación que se configuró (lea el JavaDoc ) para la secuencia. No adivinará la codificación por usted.

Algunas transmisiones le indican qué codificación se utilizó para crearlas: XML, HTML. Pero no una secuencia de bytes arbitraria.

De todos modos, podrías intentar adivinar una codificación por tu cuenta si es necesario. Cada idioma tiene una frecuencia común para cada char. En inglés, el carácter aparece muy a menudo, pero ê aparecerá muy muy raramente. En una secuencia ISO-8859-1 generalmente no hay caracteres 0x00. Pero una transmisión UTF-16 tiene muchos de ellos.

O: podrías preguntarle al usuario. Ya he visto aplicaciones que le presentan un fragmento del archivo en diferentes codificaciones y le piden que seleccione la "correcta".

Eduard Wirch
fuente
18
Esto realmente no responde la pregunta. El operador probablemente debería usar docs.codehaus.org/display/GUESSENC/Home o icu-project.org/apiref/icu4j/com/ibm/icu/text/… o jchardet.sourceforge.net
Christoffer Hammarström
23
Entonces, ¿cómo sabe mi editor, notepad ++ cómo abrir el archivo y mostrarme los caracteres correctos?
mmm
12
@Hamidam es por suerte que te muestra los personajes correctos. Cuando adivina incorrectamente (y a menudo lo hace), hay una opción (Menú >> Codificación) que le permite cambiar la codificación.
Pacerier
15
@Eduard: "Entonces cada codificación" podría "ser la correcta". no del todo bien. Muchas codificaciones de texto tienen varios patrones que no son válidos, lo que indica que el texto probablemente no sea esa codificación. De hecho, dados los primeros dos bytes de un archivo, solo el 38% de las combinaciones son UTF8 válidas. Las probabilidades de que los primeros 5 puntos de código sean UTF8 válidos por casualidad son inferiores al 0,77%. Del mismo modo, UTF16BE y LE generalmente se identifican fácilmente por la gran cantidad de cero bytes y dónde están.
Mooing Duck
38

mira esto: http://site.icu-project.org/ (icu4j) tienen bibliotecas para detectar charset de IOStream, podría ser simple como este:

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()
}
usuario345883
fuente
2
Lo intenté pero falla en gran medida: hice 2 archivos de texto en eclipse, ambos con "öäüß". Un conjunto para codificación iso y otro para utf8: ¡ambos se detectan como utf8! Así que probé un archivo guardado en algún lugar de mi hd (windows); este se detectó correctamente ("windows-1252"). Luego creé dos nuevos archivos en hd, uno editado con editor y el otro con notepad ++. en ambos casos se detectó "Big5" (chino)!
dermoritz
2
EDITAR: Ok, debería verificar cm.getConfidence () - con mi breve "äöüß" la confianza es 10. Así que tengo que decidir qué confianza es lo suficientemente buena, pero eso es absolutamente correcto para este esfuerzo (detección de caracteres)
dermoritz
1
Enlace directo al código de muestra: userguide.icu-project.org/conversion/detection
james.garriss
27

Aquí están mis favoritos:

TikaEncodingDetector

Dependencia:

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

Muestra:

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

GuessEncoding

Dependencia:

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

Muestra:

  public static Charset guessCharset2(File file) throws IOException {
    return CharsetToolkit.guessEncoding(file, 4096, StandardCharsets.UTF_8);
  }
Benny Neugebauer
fuente
2
Nota: TikaEncodingDetector 1.1 es en realidad una envoltura delgada alrededor de la clase ICU4J 3.4 CharsetDectector .
Stephan
Lamentablemente, ambas bibliotecas no funcionan. En un caso, identifica un archivo UTF-8 con Umlaute alemán como ISO-8859-1 y US-ASCII.
Cerebro
1
@Brain: ¿Su archivo probado está realmente en formato UTF-8 e incluye una lista de materiales ( en.wikipedia.org/wiki/Byte_order_mark )?
Benny Neugebauer
@BennyNeugebauer el archivo es un UTF-8 sin BOM. Lo comprobé con Notepad ++, también cambiando la codificación y afirmando que el "Umlaute" todavía está visible.
Cerebro
13

Ciertamente, puede validar el archivo para un conjunto de caracteres en particular decodificándolo con un CharsetDecodery vigilando los errores de "entrada mal formada" o "caracteres no asignables". Por supuesto, esto solo te dice si un juego de caracteres está mal; no te dice si es correcto. Para eso, necesita una base de comparación para evaluar los resultados decodificados, por ejemplo, ¿sabe de antemano si los caracteres están restringidos a algún subconjunto o si el texto se adhiere a algún formato estricto? La conclusión es que la detección de juegos de caracteres es una conjetura sin ninguna garantía.

Zach Scrivena
fuente
12

¿Qué biblioteca usar?

Al escribir estas líneas, son tres bibliotecas que emergen:

No incluyo Apache Any23 porque usa ICU4j 3.4 debajo del capó.

¿Cómo saber cuál ha detectado el juego de caracteres correcto (o lo más cerca posible)?

Es imposible certificar el conjunto de caracteres detectado por cada biblioteca anterior. Sin embargo, es posible preguntarles por turno y calificar la respuesta devuelta.

¿Cómo calificar la respuesta devuelta?

A cada respuesta se le puede asignar un punto. Cuantos más puntos tenga una respuesta, más confianza tendrá el juego de caracteres detectado. Este es un método de puntuación simple. Puedes elaborar otros.

¿Hay algún código de muestra?

Aquí hay un fragmento completo que implementa la estrategia descrita en las líneas 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() + " }";
}

Mejoras: el guessEncodingmétodo lee el flujo de entrada por completo. Para grandes flujos de entrada esto puede ser una preocupación. Todas estas bibliotecas leerían todo el flujo de entrada. Esto implicaría un gran consumo de tiempo para detectar el juego de caracteres.

Es posible limitar la carga de datos inicial a unos pocos bytes y realizar la detección de juego de caracteres solo en esos pocos bytes.

Stephan
fuente
8

Las bibliotecas anteriores son simples detectores de BOM que, por supuesto, solo funcionan si hay una BOM al comienzo del archivo. Echa un vistazo a http://jchardet.sourceforge.net/ que escanea el texto

Lorrat
fuente
18
solo en la punta, pero no hay "arriba" en este sitio; considere indicar las bibliotecas a las que se refiere.
McDowell
6

Hasta donde sé, no hay una biblioteca general en este contexto que sea adecuada para todo tipo de problemas. Por lo tanto, para cada problema, debe probar las bibliotecas existentes y seleccionar la mejor que satisfaga las restricciones de su problema, pero a menudo ninguna de ellas es apropiada. En estos casos, puede escribir su propio detector de codificación. Como he escrito ...

He escrito una herramienta meta java para detectar la codificación de juegos de caracteres de páginas web HTML, utilizando IBM ICU4j y Mozilla JCharDet como componentes integrados. Aquí puede encontrar mi herramienta, lea la sección README antes que nada. Además, puede encontrar algunos conceptos básicos de este problema en mi artículo y en sus referencias.

A continuación proporcioné algunos comentarios útiles que he experimentado en mi trabajo:

  • La detección de juegos de caracteres no es un proceso infalible, porque se basa esencialmente en datos estadísticos y lo que realmente sucede es adivinar no detectar
  • icu4j es la herramienta principal en este contexto por IBM, en mi opinión
  • Tanto TikaEncodingDetector como Lucene-ICU4j están usando icu4j y su precisión no tuvo una diferencia significativa con respecto a icu4j en mis pruebas (como máximo% 1, según recuerdo)
  • icu4j es mucho más general que jchardet, icu4j está un poco sesgado a las codificaciones de la familia IBM, mientras que jchardet está fuertemente sesgado a utf-8
  • Debido al uso generalizado de UTF-8 en HTML-world; jchardet es una mejor opción que icu4j en general, ¡pero no es la mejor opción!
  • icu4j es ideal para codificaciones específicas de Asia oriental como EUC-KR, EUC-JP, SHIFT_JIS, BIG5 y las codificaciones de la familia GB
  • Tanto icu4j como jchardet se debaten al tratar con páginas HTML con codificaciones Windows-1251 y Windows-1256. Windows-1251, también conocido como cp1251, se usa ampliamente para los idiomas cirílicos como el ruso y Windows-1256, también conocido como cp1256, se usa ampliamente para el árabe
  • Casi todas las herramientas de detección de codificación utilizan métodos estadísticos, por lo que la precisión de la salida depende en gran medida del tamaño y el contenido de la entrada.
  • Algunas codificaciones son esencialmente iguales solo con diferencias parciales, por lo que en algunos casos la codificación adivinada o detectada puede ser falsa pero al mismo tiempo ser verdadera. Como sobre Windows-1252 e ISO-8859-1. (Consulte el último párrafo en la sección 5.2 de mi artículo)
faghani
fuente
5

Si usa ICU4J ( http://icu-project.org/apiref/icu4j/ )

Aquí está mi 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();
    }
}

Recuerda poner todo el try-catch que lo necesites.

Espero que esto funcione para ti.

ssamuel68
fuente
OMI, esta respuesta es perfecta. Si desea utilizar ICU4j, intente con este: stackoverflow.com/a/4013565/363573 .
Stephan
2

Para los archivos ISO8859_1, no existe una manera fácil de distinguirlos de ASCII. Sin embargo, para archivos Unicode, generalmente se puede detectar esto en función de los primeros bytes del archivo.

Los archivos UTF-8 y UTF-16 incluyen una marca de orden de bytes (BOM) al comienzo del archivo. La lista de materiales es un espacio sin ruptura de ancho cero.

Desafortunadamente, por razones históricas, Java no detecta esto automáticamente. Programas como el Bloc de notas verificará la lista de materiales y utilizará la codificación adecuada. Usando unix o Cygwin, puede verificar la lista de materiales con el comando de archivo. Por ejemplo:

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

Para Java, le sugiero que consulte este código, que detectará los formatos de archivo comunes y seleccionará la codificación correcta: cómo leer un archivo y especificar automáticamente la codificación correcta

brianegge
fuente
15
No todos los archivos UTF-8 o UTF-16 tienen una lista de materiales, ya que no es necesaria, y se desaconseja la lista de materiales UTF-8.
Christoffer Hammarström
1

Una alternativa a TikaEncodingDetector es usar Tika AutoDetectReader .

Charset charset = new AutoDetectReader(new FileInputStream(file)).getCharset();
Nolf
fuente
Tike AutoDetectReader utiliza EncodingDetector cargado con ServiceLoader. ¿Qué implementaciones de EncodingDetector utiliza?
Stephan
-1

En Java simple:

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.");
    }
}

Este enfoque probará las codificaciones una por una hasta que uno funcione o nos quedemos sin ellas. (Por cierto, mi lista de codificaciones tiene solo esos elementos porque son las implementaciones de conjuntos de caracteres requeridas en cada plataforma Java, https://docs.oracle.com/javase/9/docs/api/java/nio/charset/Charset.html )

Andres
fuente
Pero ISO-8859-1 (entre muchos otros que no ha enumerado) siempre tendrá éxito. Y, por supuesto, esto es solo adivinar, lo que no puede recuperar los metadatos perdidos que son esenciales para la comunicación de archivos de texto.
Tom Blodget
Hola @TomBlodget, ¿estás sugiriendo que el orden de las codificaciones debería ser diferente?
Andres
3
Digo que muchos "funcionarán" pero solo uno es "correcto". Y no necesita hacer una prueba de ISO-8859-1 porque siempre "funcionará".
Tom Blodget
-12

¿Puedes elegir el conjunto de caracteres apropiado en el Constructor ?

new InputStreamReader(new FileInputStream(in), "ISO8859_1");
Kevin
fuente
8
El punto aquí era ver si el juego de caracteres podía determinarse programáticamente.
Joel
1
No, no lo adivinará por ti. Tienes que suministrarlo.
Kevin
1
Puede haber un método heurístico, como lo sugieren algunas de las respuestas aquí stackoverflow.com/questions/457655/java-charset-and-windows/…
Joel