Java: So ermitteln Sie die korrekte Zeichensatzcodierung eines Streams

140

Mit Bezug auf den folgenden Thread: Java-App: ISO-8859-1-codierte Datei kann nicht korrekt gelesen werden

Wie kann die korrekte Zeichensatzcodierung eines Eingabestreams / einer Eingabedatei am besten programmgesteuert ermittelt werden?

Ich habe versucht, Folgendes zu verwenden:

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

Bei einer Datei, von der ich weiß, dass sie mit ISO8859_1 codiert ist, liefert der obige Code ASCII, was nicht korrekt ist und es mir nicht ermöglicht, den Inhalt der Datei korrekt auf der Konsole wiederzugeben.

Joel
quelle
11
Eduard hat Recht: "Sie können die Codierung eines beliebigen Bytestroms nicht bestimmen." Alle anderen Vorschläge bieten Ihnen Möglichkeiten (und Bibliotheken), um das Beste zu erraten. Aber am Ende sind sie immer noch Vermutungen.
Mihai Nita
9
Reader.getEncodingGibt die Codierung zurück, für die der Reader eingerichtet wurde. In Ihrem Fall ist dies die Standardcodierung.
Karol S

Antworten:

70

Ich habe diese Bibliothek ähnlich wie jchardet zum Erkennen der Codierung in Java verwendet: http://code.google.com/p/juniversalchardet/

Luciano Fiandesio
quelle
6
Ich fand, dass dies genauer war: jchardet.sourceforge.net (Ich habe westeuropäische Sprachdokumente getestet, die in ISO 8859-1, Windows-1252, utf-8 codiert sind)
Joel
1
Dieser juniversalchardet funktioniert nicht. Es liefert meistens UTF-8, selbst wenn die Datei zu 100% Windows-1212-codiert ist.
Gehirn
1
juniversalchardet ist jetzt auf GitHub .
Deamon
Es erkennt keine osteuropäischen Fenster-1250
Bernhard Döbler
Ich habe versucht, das folgende Code-Snippet zur Erkennung in der Datei von " cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt " zu verwenden, habe aber als erkannten Zeichensatz null erhalten. UniversalDetector ud = neuer UniversalDetector (null); byte [] bytes = FileUtils.readFileToByteArray (neue Datei (Datei)); ud.handleData (Bytes, 0, Bytes.Länge); ud.dataEnd (); DetectedCharset = ud.getDetectedCharset ();
Rohit Verma
105

Sie können die Codierung eines beliebigen Bytestreams nicht bestimmen. Dies ist die Natur der Kodierungen. Eine Codierung bedeutet eine Zuordnung zwischen einem Bytewert und seiner Darstellung. Jede Kodierung "könnte" also die richtige sein.

Die Methode getEncoding () gibt die Codierung zurück, die für den Stream eingerichtet wurde (lesen Sie JavaDoc ). Die Kodierung wird für Sie nicht erraten.

Einige Streams geben an, mit welcher Codierung sie erstellt wurden: XML, HTML. Aber kein beliebiger Bytestrom.

Auf jeden Fall könnten Sie versuchen, eine Kodierung selbst zu erraten, wenn Sie müssen. Jede Sprache hat eine gemeinsame Frequenz für jedes Zeichen. Im Englischen erscheint das Zeichen sehr oft, aber ê wird sehr, sehr selten erscheinen. In einem ISO-8859-1-Stream gibt es normalerweise keine 0x00-Zeichen. Aber ein UTF-16-Stream hat viele davon.

Oder: Sie könnten den Benutzer fragen. Ich habe bereits Anwendungen gesehen, die Ihnen einen Ausschnitt der Datei in verschiedenen Codierungen präsentieren und Sie bitten, die "richtige" auszuwählen.

Eduard Wirch
quelle
18
Dies beantwortet die Frage nicht wirklich. Die Operation sollte wahrscheinlich docs.codehaus.org/display/GUESSENC/Home oder icu-project.org/apiref/icu4j/com/ibm/icu/text/… oder jchardet.sourceforge.net
Christoffer Hammarström
23
Woher weiß mein Editor, Notepad ++, wie man die Datei öffnet und mir die richtigen Zeichen zeigt?
mmm
12
@ Hamidam es ist ein Glück, dass es Ihnen die richtigen Charaktere zeigt. Wenn falsch geraten wird (und dies häufig der Fall ist), gibt es eine Option (Menü >> Codierung), mit der Sie die Codierung ändern können.
Pacerier
15
@Eduard: "Also jede Kodierung" könnte "die richtige sein". nicht ganz richtig. Viele Textcodierungen haben mehrere ungültige Muster. Dies ist ein Flag, dass der Text wahrscheinlich nicht so codiert ist. In Anbetracht der ersten zwei Bytes einer Datei sind nur 38% der Kombinationen gültiges UTF8. Die Wahrscheinlichkeit, dass die ersten 5 Codepunkte zufällig für UTF8 gültig sind, liegt unter 0,77%. Ebenso sind UTF16BE und LE normalerweise leicht an der großen Anzahl von Null-Bytes zu erkennen und wo sie sich befinden.
Mooing Duck
38

Überprüfen Sie dies: http://site.icu-project.org/ (icu4j) Sie haben Bibliotheken zum Erkennen von Zeichensätzen von IOStream könnte so einfach sein:

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
quelle
2
Ich habe es versucht, aber es schlägt sehr fehl: Ich habe 2 Textdateien in Eclipse erstellt, die beide "öäüß" enthalten. Ein Satz auf ISO-Codierung und einer auf utf8 - beide werden als utf8 erkannt! Also habe ich versucht, eine Datei irgendwo auf meiner Festplatte (Windows) zu sichern - diese wurde korrekt erkannt ("Windows-1252"). Dann habe ich zwei neue Dateien auf HD erstellt, eine mit Editor bearbeitet, die andere mit Notepad ++. in beiden Fällen wurde "Big5" (Chinesisch) erkannt!
Dermoritz
2
EDIT: Ok, ich sollte cm.getConfidence () überprüfen - mit meinem kurzen "äöüß" ist das Vertrauen 10. Also muss ich entscheiden, welches Vertrauen gut genug ist - aber das ist absolut in Ordnung für dieses Unterfangen (Zeichensatzerkennung)
dermoritz
1
Direkter Link zum Beispielcode: userguide.icu-project.org/conversion/detection
james.garriss
27

Hier sind meine Favoriten:

TikaEncodingDetector

Abhängigkeit:

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

Stichprobe:

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

GuessEncoding

Abhängigkeit:

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

Stichprobe:

  public static Charset guessCharset2(File file) throws IOException {
    return CharsetToolkit.guessEncoding(file, 4096, StandardCharsets.UTF_8);
  }
Benny Neugebauer
quelle
2
Hinweis : TikaEncodingDetector 1.1 ist eigentlich ein Thin Wrapper um die ICU4J 3.4- CharsetDectector Klasse.
Stephan
Leider funktionieren beide Bibliotheken nicht. In einem Fall wird eine UTF-8-Datei mit deutschem Umlaute als ISO-8859-1 und US-ASCII identifiziert.
Gehirn
1
@Brain: Hat Ihre getestete Datei tatsächlich ein UTF-8-Format und enthält sie eine Stückliste ( en.wikipedia.org/wiki/Byte_order_mark )?
Benny Neugebauer
@BennyNeugebauer Die Datei ist eine UTF-8 ohne Stückliste. Ich habe es mit Notepad ++ überprüft, auch indem ich die Codierung geändert und behauptet habe, dass die "Umlaute" noch sichtbar sind.
Gehirn
13

Sie können die Datei auf jeden Fall für einen bestimmten Zeichensatz validieren, indem Sie sie mit a dekodierenCharsetDecoder und auf Fehler bei "fehlerhafter Eingabe" oder "nicht zuordnbaren Zeichen" achten. Dies sagt Ihnen natürlich nur, wenn ein Zeichensatz falsch ist. es sagt dir nicht, ob es richtig ist. Dazu benötigen Sie eine Vergleichsbasis, um die dekodierten Ergebnisse auszuwerten. Wissen Sie beispielsweise im Voraus, ob die Zeichen auf eine Teilmenge beschränkt sind oder ob der Text einem strengen Format entspricht? Das Fazit ist, dass die Zeichensatzerkennung ohne Garantie ein Rätselraten ist.

Zach Scrivena
quelle
12

Welche Bibliothek soll ich benutzen?

Zum jetzigen Zeitpunkt entstehen drei Bibliotheken:

Ich schließe Apache Any23 nicht ein da es ICU4j 3.4 unter der Haube verwendet.

Wie man erkennt, wer das Recht erkannt hat Zeichensatz (oder so nah wie möglich)?

Es ist unmöglich, den von den oben genannten Bibliotheken erkannten Zeichensatz zu zertifizieren. Es ist jedoch möglich, sie nacheinander zu fragen und die zurückgegebene Antwort zu bewerten.

Wie kann ich die zurückgegebene Antwort bewerten?

Jeder Antwort kann ein Punkt zugewiesen werden. Je mehr Punkte eine Antwort hat, desto mehr Vertrauen hat der erkannte Zeichensatz. Dies ist eine einfache Bewertungsmethode. Sie können andere ausarbeiten.

Gibt es einen Beispielcode?

Hier ist ein vollständiger Ausschnitt, der die in den vorherigen Zeilen beschriebene Strategie implementiert.

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

Verbesserungen: DieguessEncoding Methode liest den Eingabestream vollständig. Bei großen Eingangsströmen kann dies ein Problem sein. Alle diese Bibliotheken würden den gesamten Eingabestream lesen. Dies würde einen großen Zeitaufwand für die Erkennung des Zeichensatzes bedeuten.

Es ist möglich, das anfängliche Laden von Daten auf einige Bytes zu beschränken und die Zeichensatzerkennung nur für diese wenigen Bytes durchzuführen.

Stephan
quelle
8

Die obigen Bibliotheken sind einfache Stücklistendetektoren, die natürlich nur funktionieren, wenn sich am Anfang der Datei eine Stückliste befindet. Schauen Sie sich http://jchardet.sourceforge.net/ an, das den Text scannt

Lorrat
quelle
18
Nur zur Spitze, aber es gibt kein "oben" auf dieser Site - geben Sie die Bibliotheken an, auf die Sie sich beziehen.
McDowell
6

Soweit ich weiß, gibt es in diesem Zusammenhang keine allgemeine Bibliothek, die für alle Arten von Problemen geeignet wäre. Daher sollten Sie für jedes Problem die vorhandenen Bibliotheken testen und die beste auswählen, die den Einschränkungen Ihres Problems entspricht. Oft ist jedoch keine davon geeignet. In diesen Fällen können Sie Ihren eigenen Codierungsdetektor schreiben! Wie ich geschrieben habe ...

Ich habe ein Meta-Java-Tool zum Erkennen der Zeichensatzcodierung von HTML-Webseiten mit IBM ICU4j und Mozilla JCharDet als integrierten Komponenten geschrieben. Hier finden Sie mein Tool. Bitte lesen Sie vor allem den Abschnitt README. In meinem Artikel finden Sie auch einige grundlegende Konzepte für dieses Problem und in seinen Referenzen.

Unten habe ich einige hilfreiche Kommentare abgegeben, die ich in meiner Arbeit erlebt habe:

  • Die Zeichensatzerkennung ist kein narrensicherer Prozess, da sie im Wesentlichen auf statistischen Daten basiert und tatsächlich davon ausgeht, dass keine Erkennung erfolgt
  • icu4j ist in diesem Zusammenhang das Hauptwerkzeug von IBM, imho
  • Sowohl TikaEncodingDetector als auch Lucene-ICU4j verwenden icu4j und ihre Genauigkeit hatte keinen signifikanten Unterschied zu dem icu4j in meinen Tests (höchstens% 1, wie ich mich erinnere).
  • icu4j ist viel allgemeiner als jchardet, icu4j ist nur ein bisschen voreingenommen gegenüber Codierungen der IBM-Familie, während jchardet stark auf utf-8 voreingenommen ist
  • Aufgrund der weit verbreiteten Verwendung von UTF-8 in der HTML-Welt; jchardet ist insgesamt eine bessere Wahl als icu4j, aber nicht die beste Wahl!
  • icu4j eignet sich hervorragend für ostasiatische spezifische Codierungen wie EUC-KR, EUC-JP, SHIFT_JIS, BIG5 und die Codierungen der GB-Familie
  • Sowohl icu4j als auch jchardet sind ein Debakel im Umgang mit HTML-Seiten mit Windows-1251- und Windows-1256-Codierungen. Windows-1251 alias cp1251 wird häufig für kyrillische Sprachen wie Russisch verwendet, und Windows-1256 alias cp1256 wird häufig für Arabisch verwendet
  • Fast alle Codierungserkennungswerkzeuge verwenden statistische Methoden, sodass die Genauigkeit der Ausgabe stark von der Größe und dem Inhalt der Eingabe abhängt
  • Einige Codierungen sind im Wesentlichen gleich, nur mit teilweisen Unterschieden. In einigen Fällen kann die erratene oder erkannte Codierung falsch sein, aber gleichzeitig wahr! Wie bei Windows-1252 und ISO-8859-1. (Siehe den letzten Absatz unter 5.2 in meinem Artikel.)
Faghani
quelle
5

Wenn Sie ICU4J verwenden ( http://icu-project.org/apiref/icu4j/ )

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

Denken Sie daran, alles zu versuchen, was Sie brauchen.

Ich hoffe das funktioniert bei dir.

ssamuel68
quelle
IMO, diese Antwort ist perfekt. Wenn Sie ICU4j verwenden möchten, versuchen Sie stattdessen Folgendes : stackoverflow.com/a/4013565/363573 .
Stephan
2

Für ISO8859_1-Dateien gibt es keine einfache Möglichkeit, sie von ASCII zu unterscheiden. Bei Unicode-Dateien kann man dies jedoch im Allgemeinen anhand der ersten paar Bytes der Datei erkennen.

UTF-8- und UTF-16-Dateien enthalten ganz am Anfang der Datei ein Byte Order Mark (BOM). Die Stückliste ist ein nicht unterbrechender Raum mit einer Breite von Null.

Leider erkennt Java dies aus historischen Gründen nicht automatisch. Programme wie Notepad überprüfen die Stückliste und verwenden die entsprechende Codierung. Mit Unix oder Cygwin können Sie die Stückliste mit dem Befehl file überprüfen. Beispielsweise:

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

Für Java empfehle ich Ihnen, diesen Code zu überprüfen, der die gängigen Dateiformate erkennt und die richtige Codierung auswählt: So lesen Sie eine Datei und geben automatisch die richtige Codierung an

Brianegge
quelle
15
Nicht alle UTF-8- oder UTF-16-Dateien haben eine Stückliste, da dies nicht erforderlich ist, und von UTF-8-Stücklisten wird abgeraten.
Christoffer Hammarström
1

Eine Alternative zu TikaEncodingDetector ist die Verwendung von Tika AutoDetectReader .

Charset charset = new AutoDetectReader(new FileInputStream(file)).getCharset();
Nolf
quelle
Tike AutoDetectReader verwendet EncodingDetector, der mit ServiceLoader geladen ist. Welche EncodingDetector-Implementierungen verwenden Sie?
Stephan
-1

In einfachem Java:

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

Bei diesem Ansatz werden die Codierungen einzeln ausprobiert, bis eine funktioniert oder wir keine mehr haben. (Übrigens enthält meine Codierungsliste nur diese Elemente, da es sich um die auf jeder Java-Plattform erforderlichen Zeichensatzimplementierungen handelt: https://docs.oracle.com/javase/9/docs/api/java/nio/charset/Charset.html )

Andres
quelle
Aber ISO-8859-1 (unter vielen anderen, die Sie nicht aufgelistet haben) wird immer erfolgreich sein. Und dies ist natürlich nur eine Vermutung, die die verlorenen Metadaten, die für die Kommunikation mit Textdateien wesentlich sind, nicht wiederherstellen kann.
Tom Blodget
Hallo @TomBlodget, schlagen Sie vor, dass die Codierungsreihenfolge unterschiedlich sein sollte?
Andres
3
Ich sage, dass viele "arbeiten" werden, aber nur einer "richtig" ist. Und Sie müssen nicht auf ISO-8859-1 testen, da dies immer "funktioniert".
Tom Blodget
-12

Können Sie den entsprechenden Zeichensatz im Konstruktor auswählen :

new InputStreamReader(new FileInputStream(in), "ISO8859_1");
Kevin
quelle
8
Hier ging es darum zu sehen, ob der Zeichensatz programmgesteuert bestimmt werden kann.
Joel
1
Nein, es wird es nicht für dich erraten. Sie müssen es liefern.
Kevin
1
Möglicherweise gibt es eine heuristische Methode, wie in einigen der Antworten hier vorgeschlagen. Stackoverflow.com/questions/457655/java-charset-and-windows/…
Joel