Java: як визначити правильне кодування коду потоку


140

З посиланням на наступний потік: Java-додаток: Не вдається правильно прочитати закодований файл iso-8859-1

Який найкращий спосіб програматично визначити правильне кодування діаграм для вхідного потоку / файлу?

Я спробував використовувати наступне:

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

Але у файлі, який я знаю, що він повинен бути закодований ISO8859_1, наведений вище код дає ASCII, що не є правильним, і не дозволяє мені правильно повернути вміст файлу до консолі.


11
Едуард правий: "Ви не можете визначити кодування довільного байтового потоку". Усі інші пропозиції дають вам шляхи (і бібліотеки) зробити найкращі здогадки. Але врешті-решт вони все ще здогадки.
Міхай Ніта

9
Reader.getEncodingповертає кодування, яку читач налаштував для використання, що у вашому випадку є кодуванням за замовчуванням.
Karol S

Відповіді:


70

Я використав цю бібліотеку, подібну до jchardet, для виявлення кодування на Java: http://code.google.com/p/juniversalchardet/


6
Я виявив, що це було більш точно: jchardet.sourceforge.net (я тестував документи західноєвропейської мови, закодовані в ISO 8859-1, windows-1252, utf-8)
Joel

1
Цей ювілейнийхардет не працює. Він доставляє UTF-8 більшу частину часу, навіть якщо файл на 100% закодований Windows-1212.
Мозок

1
juniversalchardet зараз на GitHub .
Діамон

Він не виявляє східноєвропейських windows-1250
Бернхард Дьоблер

Я спробував наступний фрагмент коду для виявлення у файлі з " cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt ", але я отримав null як встановлений набір символів. UniversalDetector ud = новий UniversalDetector (null); байт [] байт = FileUtils.readFileToByteArray (новий файл (файл)); ud.handleData (байти, 0, байти. довжина); ud.dataEnd (); deteCharset = ud.getDetectedCharset ();
Рохіт Верма

105

Ви не можете визначити кодування довільного байтового потоку. Така природа кодування. Кодування означає відображення між байтовим значенням та його поданням. Тож кожне кодування "могло" бути правильним.

Метод getEncoding () поверне кодування, яке було встановлено (читайте JavaDoc ) для потоку. Він не вгадає кодування для вас.

Деякі потоки повідомляють, яке кодування було використано для їх створення: XML, HTML. Але не довільний потік байтів.

У будь-якому випадку, ви можете спробувати відгадати кодування самостійно, якщо вам доведеться. Кожна мова має загальну частоту для кожної картки. Англійською мовою char є дуже часто, але ê з'являється дуже рідко. У потоці ISO-8859-1 зазвичай немає символів 0x00. Але у потоку UTF-16 їх дуже багато.

Або: ви можете запитати користувача. Я вже бачив програми, які представляють вам фрагмент файлу в різних кодуваннях і просять вибрати "правильний".


18
Це насправді не відповідає на питання. Оператор, ймовірно, повинен використовувати docs.codehaus.org/display/GUESSENC/Home або icu-project.org/apiref/icu4j/com/ibm/icu/text/… або jchardet.sourceforge.net
Christoffer Hammarström

23
Тож як мій редактор, блокнот ++ знає, як відкрити файл і показати мені потрібні символи?
ммм

12
@Hamidam випадково показує тобі правильних персонажів. Коли він здогадується неправильно (а це часто робиться), є варіант (Меню >> Кодування), який дозволяє змінити кодування.
Pacerier

15
@Eduard: "Отже, кожне кодування" може бути "правильним". не зовсім правильно. У багатьох текстових кодуваннях є кілька недійсних шаблонів, які є прапором, що текст, ймовірно, не такий. Насправді, враховуючи перші два байти файлу, лише 38% комбінацій є дійсними UTF8. Коефіцієнт перших 5 кодових точок, що є дійсним UTF8, випадково становить менше, ніж 77%. Так само UTF16BE та LE зазвичай легко ідентифікуються за великою кількістю нульових байтів та місцем їх знаходження.
Mooing Duck

38

перевірте це: http://site.icu-project.org/ (icu4j) у них бібліотеки для виявлення шаблону з IOStream можуть бути простими, як це:

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
я спробував, але сильно не вдається: я зробив 2 текстових файли в затемненні, обидва містять "öäüß". Один набір для кодування iso і один для utf8 - обидва виявляються як utf8! Тож я спробував файл, зафіксований десь на моєму hd (windows) - цей був виявлений правильно ("windows-1252"). Тоді я створив два нових файли на hd, один редагувався редактором, а інший із блокнотом ++. в обох випадках виявлено "Big5" (китайський)!
дерморіц

2
EDIT: Гаразд, я повинен перевірити cm.getConfidence () - з моїм коротким "äöüß" впевненість становить 10. Тож я повинен вирішити, яка впевненість хороша - але це абсолютно нормально для цього починання (виявлення
діаграм

1
Пряме посилання на зразок коду: userguide.icu-project.org/conversion/detection
james.garriss

27

Ось мої улюблені:

TikaEncodingDetector

Залежність:

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

Зразок:

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

GuessEncoding

Залежність:

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

Зразок:

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

2
Примітка : TikaEncodingDetector 1.1 - це фактично тонка обгортка навколо класу ICU4J 3.4 CharsetDectector .
Стефан

На жаль, обидві мочки не працюють. В одному випадку він ідентифікує файл UTF-8 з німецьким Umlaute як ISO-8859-1 та US-ASCII.
Мозок

1
@Brain: Ваш тестований файл насправді у форматі UTF-8 і чи містить він BOM ( en.wikipedia.org/wiki/Byte_order_mark )?
Бенні Нойгебауер

@BennyNeugebauer файл UTF-8 без BOM. Я перевірив це за допомогою Notepad ++, також змінивши кодування та запевнивши, що "Umlaute" все ще видно.
Мозок

13

Ви, звичайно, можете перевірити файл для певного шаблону, розшифрувавши його за допомогою "A" CharsetDecoderта спостерігаючи за помилками "неправильного введення" чи "неможливим символом". Звичайно, це говорить вам лише про те, що шафа неправильна; це не говорить вам, чи це правильно. Для цього вам потрібна база порівняння для оцінки декодованих результатів, наприклад, чи заздалегідь ви знаєте, чи символи обмежені якоюсь підмножиною, чи текст дотримується якогось строгого формату? Суть полягає в тому, що виявлення шаблонів - здогади без будь-яких гарантій.


12

Яку бібліотеку використовувати?

Станом на це написання, це три бібліотеки:

Я не включаю Apache Any23 оскільки він використовує ICU4j 3.4 під кришкою.

Як сказати, хто з них виявив право шаблону (або якомога ближче)?

Неможливо засвідчити шаблони, виявлені кожною з вищезазначених бібліотек. Однак можна попросити їх по черзі і зарахувати повернуту відповідь.

Як оцінити повернуту відповідь?

Кожній відповіді може бути призначений один бал. Чим більше балів має відповідь, тим більше впевненості має виявлена ​​карта. Це простий метод скорингу. Ви можете розробити інші.

Чи є зразок коду?

Ось повний фрагмент реалізації стратегії, описаної в попередніх рядках.

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

Покращення: ВguessEncoding метод зчитує InputStream повністю. Для великих вхідних потоків це може викликати занепокоєння. Усі ці бібліотеки читали б весь вхідний потік. Це означало б велику витрату часу на виявлення гардероба.

Можна обмежити початкове завантаження даних кількома байтами і виконати виявлення шаблонів лише на цих кількох байтах.


8

Наведені вище - прості детектори BOM, які, звичайно, працюють лише у тому випадку, якщо на початку файлу є BOM. Погляньте на http://jchardet.sourceforge.net/, який сканує текст


18
лише на підказці, але на цьому веб-сайті немає «нагорі» - подумайте про те, які бібліотеки ви посилаєтесь.
Макдауелл

6

Наскільки мені відомо, в цьому контексті не існує загальної бібліотеки, яка була б придатною для всіх типів проблем. Отже, для кожної проблеми слід протестувати наявні бібліотеки та вибрати найкращу, яка задовольняє обмеження вашої проблеми, але часто жодна з них не підходить. У цих випадках ви можете написати власний детектор кодування! Як я вже писав ...

Я написав інструмент мета-java для виявлення кодування шаблону веб-сторінок HTML, використовуючи IBM ICU4j та Mozilla JCharDet як вбудовані компоненти. Тут ви можете знайти мій інструмент, будь-ласка, прочитайте розділ README перш ніж будь-що інше. Крім того, ви можете знайти основні поняття цієї проблеми в моїй роботі та в її посиланнях.

Нижче я надав кілька корисних коментарів, які зазнав у своїй роботі:

  • Виявлення шаблонів не є надійним процесом, оскільки воно, по суті, базується на статистичних даних, і те, що насправді відбувається, - це здогадки, що не виявляють
  • icu4j - головний інструмент у цьому контексті від IBM, imho
  • І TikaEncodingDetector, і Lucene-ICU4j використовують icu4j, і їх точність не мала суттєвої різниці, від якої icu4j у моїх тестах (максимум% 1, наскільки я пам’ятаю)
  • icu4j набагато більш загальний, ніж jchardet, icu4j є трохи упередженим кодуванням сімейства IBM, тоді як jchardet сильно упереджений до utf-8
  • Через широке використання UTF-8 у світі HTML; jchardet - кращий вибір, ніж icu4j в цілому, але це не найкращий вибір!
  • icu4j чудово підходить для кодексів для східної Азії, таких як EUC-KR, EUC-JP, SHIFT_JIS, BIG5 та кодування сімейства GB
  • І icu4j, і jchardet є дебале в роботі з HTML-сторінками з кодуванням Windows-1251 та Windows-1256. Windows-1251 aka cp1251 широко використовується для кирилицьких мов, таких як російська, а Windows-1256 aka cp1256 широко використовується для арабської
  • Практично всі засоби виявлення кодування використовують статистичні методи, тому точність виводу сильно залежить від розміру та вмісту введення
  • Деякі кодування по суті є однаковими лише з частковими відмінностями, тому в деяких випадках здогадане або виявлене кодування може бути помилковим, але в той же час бути істинним! Щодо Windows-1252 та ISO-8859-1. (див. останній абзац у розділі 5.2 мого документу)


5

Якщо ви використовуєте ICU4J ( http://icu-project.org/apiref/icu4j/ )

Ось мій код:

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

Не забудьте поставити всі необхідні для цього спроби улову.

Я сподіваюся, що це працює для вас.


ІМО, ця відповідь досконала. Якщо ви хочете використовувати ICU4j, спробуйте скористатися цим: stackoverflow.com/a/4013565/363573 .
Стефан


2

Для файлів ISO8859_1 непростий спосіб відрізнити їх від ASCII. Для файлів Unicode, однак, як правило, це можна виявити на основі кількох перших байтів файлу.

Файли UTF-8 і UTF-16 містять позначку порядку в байтах (BOM) на самому початку файлу. BOM - це нерозривний простір нульової ширини.

На жаль, з історичних причин Java не визначає це автоматично. Такі програми, як «Блокнот», перевірять BOM та використовують відповідне кодування. Використовуючи unix або Cygwin, ви можете перевірити BOM за допомогою команди файлу. Наприклад:

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

Для Java, я пропоную вам перевірити цей код, який виявить поширені формати файлів і вибрати правильне кодування: Як прочитати файл і автоматично вказати правильне кодування


15
Не всі файли UTF-8 або UTF-16 мають BOM, як це не потрібно, а BT UTF-8 не рекомендується.
Крістофер Хаммарстрем

1

Альтернативою TikaEncodingDetector є використання Tika AutoDetectReader .

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

Tike AutoDetectReader використовує EncodingDetector, завантажений ServiceLoader. Які реалізації EncodingDetector ви використовуєте?
Стефане

-1

На простому Яві:

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

Цей підхід спробує кодування по черзі, поки одна не працює, або ми не закінчимо їх. (У моєму списку кодувань BTW є лише ті елементи, оскільки вони є реалізаціями шаблонів, необхідними для кожної платформи Java, https://docs.oracle.com/javase/9/docs/api/java/nio/charset/Charset.html )


Але ISO-8859-1 (серед багатьох інших, яких ви не вказали) завжди матиме успіх. І, звичайно, це лише здогадки, які не можуть відновити втрачені метадані, які є важливими для зв'язку з текстовими файлами.
Том Блоджет

Привіт, @TomBlodget, ви припускаєте, що порядок кодування має бути різним?
Андрес

3
Я кажу, що багато хто буде "працювати", але лише один - "правильно". І вам не потрібно тестувати ISO-8859-1, оскільки він завжди буде "працювати".
Том Блоджет

-12

Чи можете ви вибрати відповідний набір символів у Конструкторі :

new InputStreamReader(new FileInputStream(in), "ISO8859_1");

8
Сенс у цьому полягав у тому, щоб визначити, чи можна діаграму визначати програмно.
Джоель

1
Ні, для вас це не здогадається. Ви повинні його поставити.
Кевін

1
Можливо, є евристичний метод, як це запропоновано в деяких відповідях тут stackoverflow.com/questions/457655/java-charset-and-windows/…
Джоель
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.