Java: comment déterminer le bon encodage du jeu de caractères d'un flux

141

En référence au thread suivant: Application Java: impossible de lire correctement le fichier encodé iso-8859-1

Quelle est la meilleure façon de déterminer par programme le codage de jeu de caractères correct d'un flux d'entrée / fichier?

J'ai essayé d'utiliser ce qui suit:

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

Mais sur un fichier dont je sais qu'il est encodé avec ISO8859_1, le code ci-dessus renvoie ASCII, ce qui n'est pas correct, et ne me permet pas de restituer correctement le contenu du fichier sur la console.

Joël
la source
11
Eduard a raison, "Vous ne pouvez pas déterminer le codage d'un flux d'octets arbitraire". Toutes les autres propositions vous donnent les moyens (et les bibliothèques) de deviner au mieux. Mais au final, ce sont encore des suppositions.
Mihai Nita du
9
Reader.getEncodingrenvoie le codage que le lecteur a été configuré pour utiliser, qui dans votre cas est le codage par défaut.
Karol S du

Réponses:

70

J'ai utilisé cette bibliothèque, similaire à jchardet pour détecter l'encodage en Java: http://code.google.com/p/juniversalchardet/

Luciano Fiandesio
la source
6
J'ai trouvé que c'était plus précis: jchardet.sourceforge.net (je testais sur des documents de langue d'Europe occidentale encodés en ISO 8859-1, windows-1252, utf-8)
Joel
1
Ce juniversalchardet ne fonctionne pas. Il délivre UTF-8 la plupart du temps, même si le fichier est 100% encodé Windows-1212.
Brain du
1
juniversalchardet est maintenant sur GitHub .
démon
Il ne détecte pas les fenêtres d'Europe de l'Est-1250
Bernhard Döbler
J'ai essayé de suivre l'extrait de code pour la détection sur le fichier de " cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt " mais j'ai obtenu null comme jeu de caractères détecté. UniversalDetector ud = new UniversalDetector (null); byte [] bytes = FileUtils.readFileToByteArray (nouveau fichier (fichier)); ud.handleData (octets, 0, octets.longueur); ud.dataEnd (); détectéCharset = ud.getDetectedCharset ();
Rohit Verma le
105

Vous ne pouvez pas déterminer le codage d'un flux d'octets arbitraire. C'est la nature des encodages. Un codage signifie un mappage entre une valeur d'octet et sa représentation. Ainsi, chaque encodage «pourrait» être le bon.

La méthode getEncoding () retournera l'encodage qui a été mis en place (lisez le JavaDoc ) pour le flux. Il ne devinera pas l'encodage pour vous.

Certains flux vous indiquent quel encodage a été utilisé pour les créer: XML, HTML. Mais pas un flux d'octets arbitraire.

Quoi qu'il en soit, vous pouvez essayer de deviner un encodage par vous-même si nécessaire. Chaque langue a une fréquence commune pour chaque caractère. En anglais, le caractère apparaît très souvent mais ê apparaîtra très très rarement. Dans un flux ISO-8859-1, il n'y a généralement pas de caractères 0x00. Mais un flux UTF-16 en a beaucoup.

Ou: vous pouvez demander à l'utilisateur. J'ai déjà vu des applications qui vous présentent un extrait du fichier dans différents encodages et vous demandent de sélectionner le "correct".

Eduard Wirch
la source
18
Cela ne répond pas vraiment à la question. L'op devrait probablement utiliser docs.codehaus.org/display/GUESSENC/Home ou icu-project.org/apiref/icu4j/com/ibm/icu/text/… ou jchardet.sourceforge.net
Christoffer Hammarström
23
Alors, comment mon éditeur, notepad ++, sait-il ouvrir le fichier et me montrer les bons caractères?
mmm
12
@Hamidam c'est par chance qu'il vous montre les bons personnages. Lorsqu'il se trompe (et c'est souvent le cas), il existe une option (Menu >> Encodage) qui vous permet de changer l'encodage.
Pacerier
15
@Eduard: "Donc chaque encodage" pourrait "être le bon". pas tout à fait juste. De nombreux codages de texte ont plusieurs modèles qui ne sont pas valides, ce qui indique que le texte n'est probablement pas ce codage. En fait, étant donné les deux premiers octets d'un fichier, seulement 38% des combinaisons sont valides UTF8. Les chances que les 5 premiers points de code soient valides UTF8 par hasard sont inférieures à 0,77%. De même, UTF16BE et LE sont généralement facilement identifiés par le grand nombre d'octets zéro et leur emplacement.
Mooing Duck
38

Vérifiez ceci: http://site.icu-project.org/ (icu4j) ils ont des bibliothèques pour détecter le jeu de caractères d'IOStream pourrait être simple comme ceci:

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()
}
user345883
la source
2
J'ai essayé mais cela échoue énormément: j'ai créé 2 fichiers texte dans eclipse contenant tous deux "öäüß". Un réglé sur l'encodage iso et un sur utf8 - les deux sont détectés comme utf8! J'ai donc essayé un fichier sauvegardé quelque part sur mon hd (windows) - celui-ci a été détecté correctement ("windows-1252"). Ensuite, j'ai créé deux nouveaux fichiers sur hd, l'un édité avec l'éditeur, l'autre avec notepad ++. dans les deux cas "Big5" (chinois) a été détecté!
dermoritz le
2
EDIT: Ok, je devrais vérifier cm.getConfidence () - avec mon court "äöüß" la confiance est de 10. Je dois donc décider quelle confiance est assez bonne - mais c'est absolument ok pour cette entreprise (détection de charset)
dermoritz
1
Lien direct vers l' exemple de code: userguide.icu-project.org/conversion/detection
james.garriss
27

Voici mes favoris:

TikaEncodingDetector

Dépendance:

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

Échantillon:

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

GuessEncoding

Dépendance:

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

Échantillon:

  public static Charset guessCharset2(File file) throws IOException {
    return CharsetToolkit.guessEncoding(file, 4096, StandardCharsets.UTF_8);
  }
Benny Neugebauer
la source
2
Nota: TikaEncodingDetector 1.1 est en fait un fin wrapper autour de la classe ICU4J 3.4 CharsetDectector .
Stephan
Malheureusement, les deux bibliothèques ne fonctionnent pas. Dans un cas, il identifie un fichier UTF-8 avec Umlaute allemand comme ISO-8859-1 et US-ASCII.
Brain le
1
@Brain: Votre fichier testé est-il réellement au format UTF-8 et inclut-il une nomenclature ( en.wikipedia.org/wiki/Byte_order_mark )?
Benny Neugebauer
@BennyNeugebauer le fichier est un UTF-8 sans BOM. Je l'ai vérifié avec Notepad ++, également en modifiant l'encodage et en affirmant que les "Umlaute" sont toujours visibles.
Brain le
13

Vous pouvez certainement valider le fichier pour un jeu de caractères particulier en le décodant avec un CharsetDecoderet en faisant attention aux erreurs "malformed-input" ou "unmappable-character". Bien sûr, cela ne vous indique que si un jeu de caractères est incorrect; il ne vous dit pas si c'est correct. Pour cela, vous avez besoin d'une base de comparaison pour évaluer les résultats décodés, par exemple savez-vous à l'avance si les caractères sont limités à un sous-ensemble ou si le texte adhère à un format strict? L'essentiel est que la détection des jeux de caractères est une supposition sans aucune garantie.

Zach Scrivena
la source
12

Quelle bibliothèque utiliser?

Au moment d'écrire ces lignes, ce sont trois bibliothèques qui émergent:

Je n'inclus pas Apache Any23 car il utilise ICU4j 3.4 sous le capot.

Comment savoir lequel a détecté le bon jeu de caractères (ou le plus proche possible)?

Il est impossible de certifier le jeu de caractères détecté par chacune des bibliothèques ci-dessus. Cependant, il est possible de leur demander à tour de rôle et de noter la réponse renvoyée.

Comment noter la réponse retournée?

Chaque réponse peut se voir attribuer un point. Plus une réponse a de points, plus le jeu de caractères détecté est fiable. Il s'agit d'une méthode de notation simple. Vous pouvez en élaborer d'autres.

Existe-t-il un exemple de code?

Voici un extrait complet de la mise en œuvre de la stratégie décrite dans les lignes précédentes.

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

Améliorations: la guessEncodingméthode lit entièrement le flux d'entrée. Pour les grands flux d'entrée, cela peut être un problème. Toutes ces bibliothèques liraient tout le flux d'entrée. Cela impliquerait une grande consommation de temps pour détecter le jeu de caractères.

Il est possible de limiter le chargement initial des données à quelques octets et d'effectuer la détection du jeu de caractères sur ces quelques octets uniquement.

Stéphan
la source
8

Les bibliothèques ci-dessus sont de simples détecteurs de nomenclature qui, bien sûr, ne fonctionnent que s'il y a une nomenclature au début du fichier. Jetez un œil à http://jchardet.sourceforge.net/ qui scanne le texte

Lorrat
la source
18
juste à la pointe, mais il n'y a pas de "ci-dessus" sur ce site - pensez à indiquer les bibliothèques auxquelles vous faites référence.
McDowell
6

Autant que je sache, il n'existe pas de bibliothèque générale dans ce contexte pour convenir à tous les types de problèmes. Donc, pour chaque problème, vous devez tester les bibliothèques existantes et sélectionner la meilleure qui satisfait les contraintes de votre problème, mais souvent aucune d'elles n'est appropriée. Dans ces cas, vous pouvez écrire votre propre détecteur d'encodage! Comme je l'ai écrit ...

J'ai écrit un outil meta java pour détecter le codage de charset des pages Web HTML, en utilisant IBM ICU4j et Mozilla JCharDet comme composants intégrés. Ici vous pouvez trouver mon outil, veuillez lire la section README avant toute autre chose. En outre, vous pouvez trouver quelques concepts de base de ce problème dans mon article et dans ses références.

Ci-dessous, j'ai fourni quelques commentaires utiles que j'ai expérimentés dans mon travail:

  • La détection des jeux de caractères n'est pas un processus infaillible, car elle est essentiellement basée sur des données statistiques et ce qui se passe réellement est de deviner et non de détecter
  • icu4j est l'outil principal dans ce contexte par IBM, à mon humble avis
  • TikaEncodingDetector et Lucene-ICU4j utilisent tous les deux icu4j et leur précision n'avait pas de différence significative par rapport à l'icu4j dans mes tests (au plus% 1, si je me souviens bien)
  • icu4j est beaucoup plus général que jchardet, icu4j est juste un peu biaisé pour les encodages de la famille IBM tandis que jchardet est fortement biaisé vers utf-8
  • En raison de l'utilisation répandue de UTF-8 dans le monde HTML; jchardet est un meilleur choix que icu4j dans l'ensemble, mais ce n'est pas le meilleur choix!
  • icu4j est idéal pour les encodages spécifiques d'Asie de l'Est tels que EUC-KR, EUC-JP, SHIFT_JIS, BIG5 et les encodages de la famille GB
  • Les deux icu4j et jchardet sont une débâcle dans le traitement des pages HTML avec les encodages Windows-1251 et Windows-1256. Windows-1251 aka cp1251 est largement utilisé pour les langues cyrilliques comme le russe et Windows-1256 aka cp1256 est largement utilisé pour l'arabe
  • Presque tous les outils de détection de codage utilisent des méthodes statistiques, de sorte que la précision de la sortie dépend fortement de la taille et du contenu de l'entrée
  • Certains encodages sont essentiellement les mêmes avec des différences partielles, donc dans certains cas, l'encodage deviné ou détecté peut être faux mais en même temps être vrai! En ce qui concerne Windows-1252 et ISO-8859-1. (reportez-vous au dernier paragraphe de la section 5.2 de mon article)
faghani
la source
5

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

Voici mon code:

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();
    }
}

N'oubliez pas de mettre tout le try-catch qu'il faut.

J'espère que cela fonctionne pour vous.

ssamuel68
la source
OMI, cette réponse est perfectible. Si vous souhaitez utiliser ICU4j, essayez plutôt celui-ci: stackoverflow.com/a/4013565/363573 .
Stephan
2

Pour les fichiers ISO8859_1, il n'existe pas de moyen facile de les distinguer de l'ASCII. Pour les fichiers Unicode, cependant, on peut généralement le détecter sur la base des premiers octets du fichier.

Les fichiers UTF-8 et UTF-16 incluent une marque d'ordre d'octet (BOM) au tout début du fichier. La nomenclature est un espace insécable de largeur nulle.

Malheureusement, pour des raisons historiques, Java ne le détecte pas automatiquement. Des programmes comme Notepad vérifieront la nomenclature et utiliseront le codage approprié. En utilisant unix ou Cygwin, vous pouvez vérifier la nomenclature avec la commande file. Par exemple:

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

Pour Java, je vous suggère de consulter ce code, qui détectera les formats de fichiers courants et sélectionnera le bon encodage: Comment lire un fichier et spécifier automatiquement le bon encodage

Brianegge
la source
15
Tous les fichiers UTF-8 ou UTF-16 n'ont pas de nomenclature, car cela n'est pas obligatoire et la nomenclature UTF-8 est déconseillée.
Christoffer Hammarström
1

Une alternative à TikaEncodingDetector est d'utiliser Tika AutoDetectReader .

Charset charset = new AutoDetectReader(new FileInputStream(file)).getCharset();
Nolf
la source
Tike AutoDetectReader utilise EncodingDetector chargé avec ServiceLoader. Quelles implémentations EncodingDetector utilisez-vous?
Stephan le
-1

En Java clair:

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

Cette approche essaiera les encodages un par un jusqu'à ce que l'un d'eux fonctionne ou que nous en manquions. (BTW ma liste d'encodages ne contient que ces éléments car ce sont les implémentations de jeux de caractères requises sur chaque plate-forme Java, https://docs.oracle.com/javase/9/docs/api/java/nio/charset/Charset.html )

Andrés
la source
Mais ISO-8859-1 (parmi beaucoup d'autres que vous n'avez pas listés) réussira toujours. Et, bien sûr, il ne s'agit que de devinettes, qui ne peuvent pas récupérer les métadonnées perdues qui sont essentielles à la communication de fichiers texte.
Tom Blodget
Salut @TomBlodget, suggérez-vous que l'ordre des encodages soit différent?
Andres
3
Je dis que beaucoup "travailleront" mais qu'un seul "aura raison". Et vous n'avez pas besoin de tester ISO-8859-1 car il "fonctionnera" toujours.
Tom Blodget
-12

Pouvez-vous choisir le jeu de caractères approprié dans le constructeur :

new InputStreamReader(new FileInputStream(in), "ISO8859_1");
Kevin
la source
8
Le but ici était de voir si le jeu de caractères pouvait être déterminé par programme.
Joel le
1
Non, il ne le devinera pas pour vous. Vous devez le fournir.
Kevin le
1
Il peut y avoir une méthode heuristique, comme suggéré par certaines des réponses ici stackoverflow.com/questions/457655/java-charset-and-windows/...
Joel