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

143

Со ссылкой на следующий поток: Приложение 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

Ответы:

72

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

Лучано Фиандезио
источник
6
Я обнаружил, что это более точно: jchardet.sourceforge.net (я тестировал документы на западноевропейском языке, закодированные в ISO 8859-1, windows-1252, utf-8)
Joel
1
Этот juniversalchardet не работает. Он предоставляет UTF-8 большую часть времени, даже если файл на 100% закодирован в Windows-1212.
Brain
1
juniversalchardet теперь на GitHub .
deamon
Он не обнаруживает восточноевропейские окна-1250
Бернхард Дёблер 01
Я попробовал следующий фрагмент кода для обнаружения в файле из " cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt ", но получил null как обнаруженный набор символов. UniversalDetector ud = новый UniversalDetector (null); byte [] bytes = FileUtils.readFileToByteArray (новый файл (файл)); ud.handleData (байты, 0, длина байтов); ud.dataEnd (); DetectedCharset = ud.getDetectedCharset ();
Рохит Верма,
104

Вы не можете определить кодировку произвольного байтового потока. Такова природа кодировок. Кодирование означает отображение между байтовым значением и его представлением. Так что каждая кодировка «могла быть» правильной.

Метод getEncoding () вернет кодировку, которая была настроена (прочтите JavaDoc ) для потока. Он не угадает за вас кодировку.

Некоторые потоки сообщают вам, какая кодировка использовалась для их создания: XML, HTML. Но не произвольный поток байтов.

В любом случае, вы можете попытаться угадать кодировку самостоятельно, если вам нужно. Каждый язык имеет общую частоту для каждого символа. В английском языке символ e встречается очень часто, а ê - очень редко. В потоке ISO-8859-1 обычно нет символов 0x00. Но в потоке UTF-16 их много.

Или: вы можете спросить пользователя. Я уже видел приложения, которые представляют вам фрагмент файла в разных кодировках и просят выбрать «правильную».

Эдуард Вирч
источник
19
Это не совсем ответ на вопрос. Операция, вероятно, должна использовать docs.codehaus.org/display/GUESSENC/Home или icu-project.org/apiref/icu4j/com/ibm/icu/text/… или jchardet.sourceforge.net
Кристофер Хаммарстрём
24
Итак, как мой редактор, блокнот ++, знает, как открыть файл и показать мне правильные символы?
ммм
12
@Hamidam удачно показывает вам правильных персонажей. Когда он угадывает ошибочно (а это часто бывает), есть опция (Меню >> Кодировка), которая позволяет вам изменить кодировку.
Pacerier
15
@Eduard: «Значит, каждая кодировка может быть правильной». не совсем так. Многие кодировки текста имеют несколько недействительных шаблонов, которые являются признаком того, что текст, вероятно, не в этой кодировке. Фактически, учитывая первые два байта файла, только 38% комбинаций являются допустимыми UTF8. Вероятность того, что первые 5 кодовых точек окажутся действительными UTF8 случайно, составляет менее 0,77%. Точно так же UTF16BE и LE обычно легко идентифицируются по большому количеству нулевых байтов и их местонахождению.
Mooing Duck
Было бы неплохо получить хотя бы такой же точный метод, как Notepad ++ или просто Notepad. Никто не может сказать нам, что это такое?
Питер Мур,
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()
}
user345883
источник
2
Я попытался, но это очень не удалось: я сделал 2 текстовых файла в eclipse, оба содержащие "öäüß". Один установлен на кодировку iso, а другой - на utf8 - оба распознаются как utf8! Итак, я попробовал файл, сохраненный где-то на моем жестком диске (Windows) - он был обнаружен правильно ("windows-1252"). Затем я создал два новых файла на жестком диске, один из которых отредактировал редактором, а другой - с помощью блокнота ++. в обоих случаях был обнаружен "Big5" (китайский)!
dermoritz
2
РЕДАКТИРОВАТЬ: Хорошо, я должен проверить cm.getConfidence () - с моим коротким "äöüß" уверенность равна 10. Итак, я должен решить, какая уверенность достаточно хороша - но это абсолютно нормально для этого усилия (определение кодировки)
dermoritz
1
Прямая ссылка на пример кода: userguide.icu-project.org/conversion/detection
james.garriss
28

Вот мои любимые:

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 на самом деле представляет собой тонкую оболочку для CharsetDectector класса ICU4J 3.4 .
Стефан
К сожалению, обе библиотеки не работают. В одном случае он идентифицирует файл UTF-8 с немецким Umlaute как ISO-8859-1 и US-ASCII.
Brain
1
@Brain: действительно ли ваш протестированный файл имеет формат UTF-8 и включает ли он спецификацию ( en.wikipedia.org/wiki/Byte_order_mark )?
Бенни Нойгебауэр,
@BennyNeugebauer - это файл в формате UTF-8 без спецификации. Я проверил это с помощью Notepad ++, также изменив кодировку и заявив, что "Umlaute" все еще видны.
Brain
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

Приведенные выше библиотеки представляют собой простые детекторы спецификации, которые, конечно, работают только в том случае, если в начале файла есть спецификация. Взгляните на http://jchardet.sourceforge.net/, который сканирует текст

Лоррат
источник
18
только на кончике, но на этом сайте нет «выше» - подумайте о том, чтобы указать библиотеки, о которых вы говорите.
McDowell
7

Насколько мне известно, в этом контексте нет общей библиотеки, подходящей для всех типов задач. Итак, для каждой проблемы вы должны протестировать существующие библиотеки и выбрать лучшую, которая удовлетворяет ограничениям вашей проблемы, но часто ни одна из них не подходит. В этих случаях вы можете написать свой собственный детектор кодирования! Как я уже писал ...

Я написал мета-инструмент 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, также известная как cp1251, широко используется для языков на основе кириллицы, таких как русский, а Windows-1256 aka cp1256 широко используется для арабского языка.
  • Почти все инструменты обнаружения кодирования используют статистические методы, поэтому точность вывода сильно зависит от размера и содержимого ввода.
  • Некоторые кодировки по сути одинаковы, только с частичными различиями, поэтому в некоторых случаях предполагаемая или обнаруженная кодировка может быть ложной, но в то же время быть верной! Как насчет Windows-1252 и ISO-8859-1. (см. последний абзац в разделе 5.2 моей статьи)
Faghani
источник
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();
    }
}

Не забудьте поставить все, что нужно попробовать-поймать.

Надеюсь, это сработает для вас.

ssamuel68
источник
ИМО, этот ответ совершенен. Если вы хотите использовать ICU4j, попробуйте вместо этого: stackoverflow.com/a/4013565/363573 .
Стефан
2

Для файлов ISO8859_1 не существует простого способа отличить их от ASCII. Однако для файлов Unicode это обычно можно обнаружить на основе первых нескольких байтов файла.

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

К сожалению, по историческим причинам Java не обнаруживает это автоматически. Такие программы, как Блокнот, проверяют спецификацию и используют соответствующую кодировку. Используя unix или Cygwin, вы можете проверить спецификацию с помощью команды file. Например:

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

Для Java я предлагаю вам проверить этот код, который определит распространенные форматы файлов и выберет правильную кодировку: Как читать файл и автоматически указывать правильную кодировку

Brianegge
источник
15
Не все файлы UTF-8 или UTF-16 имеют спецификацию, поскольку она не требуется, а спецификация UTF-8 не рекомендуется.
Christoffer Hammarström
1

Альтернативой TikaEncodingDetector является использование Tika AutoDetectReader .

Charset charset = new AutoDetectReader(new FileInputStream(file)).getCharset();
Нольф
источник
Tike AutoDetectReader использует EncodingDetector, загруженный с ServiceLoader. Какие реализации EncodingDetector вы используете?
Стефан
-1

На простой 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.");
    }
}

Этот подход будет пробовать кодировки одну за другой, пока одна из них не сработает или они не закончатся. (Кстати, в моем списке кодировок есть только эти элементы, потому что они являются реализациями кодировок, необходимыми на каждой платформе Java, https://docs.oracle.com/javase/9/docs/api/java/nio/charset/Charset.html )

Андрес
источник
Но ISO-8859-1 (среди многих других, которые вы не перечислили) всегда будет успешным. И, конечно же, это всего лишь предположение, которое не может восстановить потерянные метаданные, необходимые для обмена текстовыми файлами.
Tom Blodget
Привет, @TomBlodget, вы предлагаете, чтобы порядок кодировок был другим?
Андрес
3
Я говорю, что многие будут «работать», но только один «правильный». И вам не нужно тестировать ISO-8859-1, потому что он всегда «работает».
Tom Blodget
-12

Можете ли вы выбрать соответствующий набор символов в конструкторе :

new InputStreamReader(new FileInputStream(in), "ISO8859_1");
Кевин
источник
8
Дело здесь в том, чтобы посмотреть, можно ли определить кодировку программно.
Joel
1
Нет, он не угадает за вас. Вы должны это предоставить.
Кевин
1
Возможно, существует эвристический метод, о чем свидетельствуют некоторые ответы здесь stackoverflow.com/questions/457655/java-charset-and-windows/…
Джоэл