Java: Jak określić prawidłowe kodowanie zestawu znaków w strumieniu

140

W odniesieniu do następującego wątku: Aplikacja Java: Nie można poprawnie odczytać pliku zakodowanego w standardzie iso-8859-1

Jaki jest najlepszy sposób programowego określenia prawidłowego kodowania zestawu znaków strumienia wejściowego / pliku?

Próbowałem użyć następujących:

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

Ale w przypadku pliku, o którym wiem, że jest zakodowany za pomocą ISO8859_1, powyższy kod daje ASCII, co nie jest poprawne i nie pozwala mi poprawnie wyrenderować zawartości pliku z powrotem na konsolę.

Joel
źródło
11
Eduard ma rację: „Nie można określić kodowania dowolnego strumienia bajtów”. Wszystkie inne propozycje podają sposoby (i biblioteki) najlepszego zgadywania. Ale ostatecznie to wciąż domysły.
Mihai Nita
9
Reader.getEncodingzwraca kodowanie, którego używał czytnik, które w twoim przypadku jest kodowaniem domyślnym.
Karol S

Odpowiedzi:

70

Użyłem tej biblioteki, podobnej do jchardet do wykrywania kodowania w Javie: http://code.google.com/p/juniversalchardet/

Luciano Fiandesio
źródło
6
Okazało się, że jest to dokładniejsze: jchardet.sourceforge.net (testowałem na dokumentach w językach zachodnioeuropejskich zakodowanych w ISO 8859-1, windows-1252, utf-8)
Joel
1
Ten juniversalchardet nie działa. Przez większość czasu dostarcza UTF-8, nawet jeśli plik jest w 100% zakodowany w systemie Windows-1212.
Brain
1
juniversalchardet jest teraz w serwisie GitHub .
demon
Nie wykrywa wschodnioeuropejskich windows-1250
Bernhard Döbler
Próbowałem wykonać następujący fragment kodu w celu wykrycia w pliku z „ cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt ”, ale otrzymałem wartość null jako wykryty zestaw znaków. UniversalDetector ud = nowy UniversalDetector (null); byte [] bytes = FileUtils.readFileToByteArray (nowy plik (plik)); ud.handleData (bytes, 0, bytes.length); ud.dataEnd (); detectionCharset = ud.getDetectedCharset ();
Rohit Verma
105

Nie można określić kodowania dowolnego strumienia bajtów. Taka jest natura kodowania. Kodowanie oznacza mapowanie między wartością bajtu a jej reprezentacją. Zatem każde kodowanie „mogłoby” być właściwe.

Metoda getEncoding () zwróci kodowanie, które zostało ustawione (przeczytaj JavaDoc ) dla strumienia. Nie będzie odgadnąć kodowania za Ciebie.

Niektóre strumienie informują, jakie kodowanie zostało użyte do ich utworzenia: XML, HTML. Ale nie jest to dowolny strumień bajtów.

W każdym razie możesz spróbować samodzielnie odgadnąć kodowanie, jeśli musisz. Każdy język ma wspólną częstotliwość dla każdego znaku. W języku angielskim char e pojawia się bardzo często, ale ê pojawia się bardzo rzadko. W strumieniu ISO-8859-1 zwykle nie ma znaków 0x00. Ale strumień UTF-16 ma ich dużo.

Lub: możesz zapytać użytkownika. Widziałem już aplikacje, które przedstawiają fragment pliku w różnych kodowaniach i proszą o wybranie „właściwego”.

Eduard Wirch
źródło
18
To naprawdę nie odpowiada na pytanie. Operacja powinna prawdopodobnie korzystać z docs.codehaus.org/display/GUESSENC/Home lub icu-project.org/apiref/icu4j/com/ibm/icu/text/… lub jchardet.sourceforge.net
Christoffer Hammarström
23
Skąd więc mój edytor, notepad ++, wie, jak otworzyć plik i pokazać mi właściwe znaki?
mmm
12
@Hamidam na szczęście pokazuje ci właściwe postacie. Gdy zgadnie nieprawidłowo (a często tak się dzieje), dostępna jest opcja (Menu >> Kodowanie), która umożliwia zmianę kodowania.
Pacerier,
15
@Eduard: „Więc każde kodowanie” może być „właściwe”. nie do końca. Wiele kodowań tekstu ma kilka nieprawidłowych wzorców, co oznacza, że ​​tekst prawdopodobnie nie jest tego kodowania. W rzeczywistości, biorąc pod uwagę pierwsze dwa bajty pliku, tylko 38% kombinacji to poprawne UTF8. Szansa, że ​​pierwsze 5 punktów kodowych będzie przez przypadek poprawnych w UTF8, jest mniejsza niż 0,77%. Podobnie, UTF16BE i LE są zwykle łatwe do zidentyfikowania dzięki dużej liczbie zerowych bajtów i ich lokalizacji.
Mooing Duck
38

sprawdź to: http://site.icu-project.org/ (icu4j) mają biblioteki do wykrywania zestawu znaków z IOStream może być takie proste:

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
źródło
2
Próbowałem, ale to bardzo się nie udaje: utworzyłem 2 pliki tekstowe w zaćmieniu, oba zawierające „öäüß”. Jeden ustawiony na kodowanie ISO i jeden na utf8 - oba są wykrywane jako utf8! Wypróbowałem więc plik zapisany gdzieś na moim dysku twardym (Windows) - ten został poprawnie wykryty („windows-1252”). Następnie utworzyłem dwa nowe pliki na hd, jeden edytowany za pomocą edytora, drugi z notatnikiem ++. w obu przypadkach wykryto „Big5” (chiński)!
dermoritz,
2
EDYCJA: Ok, powinienem sprawdzić cm.getConfidence () - z moim krótkim „äöüß” pewność wynosi 10. Więc muszę zdecydować, która pewność jest wystarczająco dobra - ale to absolutnie w porządku dla tego przedsięwzięcia (wykrywanie znaków)
dermoritz
1
Bezpośredni link do przykładowego kodu: userguide.icu-project.org/conversion/detection
james.garriss
27

Oto moje ulubione:

TikaEncodingDetector

Zależność:

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

Próba:

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

GuessEncoding

Zależność:

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

Próba:

  public static Charset guessCharset2(File file) throws IOException {
    return CharsetToolkit.guessEncoding(file, 4096, StandardCharsets.UTF_8);
  }
Benny Neugebauer
źródło
2
Uwaga: TikaEncodingDetector 1.1 jest w rzeczywistości cienką warstwą wokół klasy ICU4J 3.4 CharsetDectector .
Stephan
Niestety obie biblioteki nie działają. W jednym przypadku identyfikuje plik UTF-8 z niemieckim Umlaute jako ISO-8859-1 i US-ASCII.
Brain
1
@Brain: Czy Twój testowany plik faktycznie jest w formacie UTF-8 i czy zawiera BOM ( en.wikipedia.org/wiki/Byte_order_mark )?
Benny Neugebauer
@BennyNeugebauer plik jest w formacie UTF-8 bez BOM. Sprawdziłem to w Notepad ++, również zmieniając kodowanie i zapewniając, że „Umlaute” są nadal widoczne.
Brain,
13

Z pewnością można sprawdzić poprawność pliku pod kątem określonego zestawu znaków, dekodując go za pomocą a CharsetDecoderi zwracając uwagę na błędy „zniekształcone dane wejściowe” lub „niezamapowalne znaki”. Oczywiście to mówi ci tylko wtedy, gdy zestaw znaków jest zły; nie mówi ci, czy jest poprawna. Do tego potrzebna jest podstawa porównawcza do oceny dekodowanych wyników, np. Czy wiesz wcześniej, czy znaki są ograniczone do jakiegoś podzbioru, czy też tekst jest zgodny z jakimś ścisłym formatem? Najważniejsze jest to, że wykrywanie zestawu znaków to zgadywanie bez żadnych gwarancji.

Zach Scrivena
źródło
12

Z jakiej biblioteki skorzystać?

W chwili pisania tego artykułu powstały trzy biblioteki:

Nie dołączam Apache Any23, ponieważ pod maską używa ICU4j 3.4.

Jak sprawdzić, który z nich wykrył właściwy zestaw znaków (lub możliwie najbliższy)?

Nie można poświadczyć zestawu znaków wykrytego przez każdą z powyższych bibliotek. Można jednak zapytać ich po kolei i ocenić otrzymaną odpowiedź.

Jak ocenić zwróconą odpowiedź?

Każdej odpowiedzi można przypisać jeden punkt. Im więcej punktów ma odpowiedź, tym większe zaufanie ma wykryty zestaw znaków. To jest prosta metoda punktacji. Możesz rozwinąć innych.

Czy jest jakiś przykładowy kod?

Oto pełny fragment implementujący strategię opisaną w poprzednich wierszach.

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

Ulepszenia:guessEncoding metoda odczytuje InputStream całkowicie. W przypadku dużych strumieni wejściowych może to stanowić problem. Wszystkie te biblioteki czytałyby cały strumień wejściowy. Oznaczałoby to duże zużycie czasu na wykrycie zestawu znaków.

Możliwe jest ograniczenie początkowego ładowania danych do kilku bajtów i wykrywanie zestawu znaków tylko na tych kilku bajtach.

Stephan
źródło
8

Powyższe biblioteki to proste detektory BOM, które oczywiście działają tylko wtedy, gdy na początku pliku znajduje się BOM. Spójrz na http://jchardet.sourceforge.net/, który skanuje tekst

Lorrat
źródło
18
tylko wskazówka, ale nie ma „powyżej” na tej stronie - rozważ wskazanie bibliotek, do których się odnosisz.
McDowell,
6

O ile wiem, w tym kontekście nie ma ogólnej biblioteki, która byłaby odpowiednia dla wszystkich typów problemów. Dlatego dla każdego problemu należy przetestować istniejące biblioteki i wybrać najlepszą, która spełnia ograniczenia problemu, ale często żadna z nich nie jest odpowiednia. W takich przypadkach możesz napisać własny wykrywacz kodowania! Jak napisałem ...

Napisałem narzędzie meta java do wykrywania kodowania kodowania znaków na stronach HTML, używając IBM ICU4j i Mozilla JCharDet jako wbudowanych komponentów. Tutaj możesz znaleźć moje narzędzie, przeczytaj najpierw sekcję README. W moim artykule oraz w jego bibliografii można również znaleźć kilka podstawowych koncepcji tego problemu .

Poniżej zamieściłem kilka pomocnych komentarzy, których doświadczyłem w swojej pracy:

  • Wykrywanie zestawu znaków nie jest niezawodnym procesem, ponieważ zasadniczo opiera się na danych statystycznych, a to, co faktycznie się dzieje, to zgadywanie, a nie wykrywanie
  • icu4j jest w tym kontekście głównym narzędziem IBM, imho
  • Zarówno TikaEncodingDetector, jak i Lucene-ICU4j używają icu4j i ich dokładność nie miała znaczącej różnicy, od której icu4j w moich testach (co najwyżej% 1, jak pamiętam)
  • icu4j jest znacznie bardziej ogólny niż jchardet, icu4j jest tylko trochę uprzedzony do kodowania rodziny IBM, podczas gdy jchardet jest silnie nastawiony na utf-8
  • Ze względu na powszechne użycie UTF-8 w świecie HTML; jchardet jest ogólnie lepszym wyborem niż icu4j, ale nie jest najlepszym wyborem!
  • icu4j doskonale nadaje się do kodowania specyficznego dla Azji Wschodniej, takich jak EUC-KR, EUC-JP, SHIFT_JIS, BIG5 i kodowanie rodziny GB
  • Zarówno icu4j, jak i jchardet nie radzą sobie ze stronami HTML z kodowaniem Windows-1251 i Windows-1256. Windows-1251 aka cp1251 jest szeroko stosowany w językach opartych na cyrylicy, takich jak rosyjski i Windows-1256, czyli cp1256, jest szeroko stosowany w języku arabskim
  • Prawie wszystkie narzędzia do wykrywania kodowania używają metod statystycznych, więc dokładność danych wyjściowych silnie zależy od rozmiaru i zawartości danych wejściowych
  • Niektóre kodowania są zasadniczo takie same, tylko z częściowymi różnicami, więc w niektórych przypadkach zgadane lub wykryte kodowanie może być fałszywe, ale jednocześnie prawdziwe! Jeśli chodzi o Windows-1252 i ISO-8859-1. (patrz ostatni akapit w sekcji 5.2 mojego artykułu)
faghani
źródło
5

Jeśli używasz ICU4J ( http://icu-project.org/apiref/icu4j/ )

Oto mój kod:

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

Pamiętaj, aby umieścić wszystkie try-catch potrzebne.

Mam nadzieję, że to działa dla Ciebie.

ssamuel68
źródło
IMO, ta odpowiedź jest możliwa do udoskonalenia. Jeśli chcesz korzystać z ICU4j, spróbuj zamiast tego: stackoverflow.com/a/4013565/363573 .
Stephan
2

W przypadku plików ISO8859_1 nie ma łatwego sposobu na odróżnienie ich od ASCII. Jednak w przypadku plików Unicode można to zwykle wykryć na podstawie kilku pierwszych bajtów pliku.

Pliki UTF-8 i UTF-16 zawierają znacznik kolejności bajtów (BOM) na samym początku pliku. Zestawienie komponentów to nierozdzielająca przestrzeń o zerowej szerokości.

Niestety z powodów historycznych Java nie wykrywa tego automatycznie. Programy takie jak Notatnik sprawdzą BOM i zastosują odpowiednie kodowanie. Używając unix lub Cygwin, możesz sprawdzić BOM za pomocą polecenia plik. Na przykład:

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

W przypadku języka Java sugeruję sprawdzenie tego kodu, który wykryje popularne formaty plików i wybierze prawidłowe kodowanie: Jak odczytać plik i automatycznie określić prawidłowe kodowanie

brianegge
źródło
15
Nie wszystkie pliki UTF-8 lub UTF-16 mają BOM, ponieważ nie jest to wymagane, a BOM UTF-8 jest odradzany.
Christoffer Hammarström
1

Alternatywą dla TikaEncodingDetector jest użycie Tika AutoDetectReader .

Charset charset = new AutoDetectReader(new FileInputStream(file)).getCharset();
Nolf
źródło
Tike AutoDetectReader używa EncodingDetector załadowanego z ServiceLoader. Których implementacji EncodingDetector używasz?
Stephan,
-1

W zwykłej Javie:

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

To podejście będzie próbować kodowania jeden po drugim, aż jedno zadziała lub zabraknie ich. (Przy okazji moja lista kodowań zawiera tylko te elementy, ponieważ są to implementacje zestawów znaków wymagane na każdej platformie Java, https://docs.oracle.com/javase/9/docs/api/java/nio/charset/Charset.html )

Andres
źródło
Ale ISO-8859-1 (pośród wielu innych, których nie wymieniłeś) zawsze się powiedzie. I oczywiście jest to tylko zgadywanie, które nie może odzyskać utraconych metadanych, które są niezbędne do komunikacji w plikach tekstowych.
Tom Blodget
Cześć @TomBlodget, czy sugerujesz, że kolejność kodowania powinna być inna?
Andres
3
Mówię, że wielu będzie „pracowało”, ale tylko jeden jest „właściwy”. I nie musisz testować ISO-8859-1, ponieważ zawsze będzie działać.
Tom Blodget
-12

Czy potrafisz wybrać odpowiedni zestaw znaków w konstruktorze :

new InputStreamReader(new FileInputStream(in), "ISO8859_1");
Kevin
źródło
8
Chodziło o sprawdzenie, czy zestaw znaków można określić programowo.
Joel
1
Nie, nie zgadnie tego za Ciebie. Musisz to dostarczyć.
Kevin
1
Może istnieć metoda heurystyczna, jak sugerują niektóre odpowiedzi tutaj stackoverflow.com/questions/457655/java-charset-and-windows/ ...
Joel