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

Со ссылкой на следующую ветку: Приложение 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, что неверно и не позволяет мне корректно отображать содержимое файла обратно на консоль.

16 ответов

Решение

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

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

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

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

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

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

Проверьте это: 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()
}

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

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

Какую библиотеку использовать?

На момент написания статьи появилось три библиотеки:

Я не включаю 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 Метод полностью читает входной поток. Для больших входных потоков это может быть проблемой. Все эти библиотеки будут читать весь входной поток. Это предполагает большой расход времени на обнаружение кодировки.

Можно ограничить начальную загрузку данных несколькими байтами и выполнить обнаружение кодировки только для этих нескольких байтов.

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

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

Я написал инструмент мета-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 моей статьи)

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

Я нашел хорошую стороннюю библиотеку, которая может определять фактическую кодировку: http://glaforge.free.fr/wiki/index.php?wiki=GuessEncoding

Я не проверял это всесторонне, но это, кажется, работает.

Если вы используете 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();
                }
            }

Не забудьте поставить все попытки поймать это нужно.

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

Если вы не знаете кодировку ваших данных, это не так легко определить, но вы можете попытаться использовать библиотеку, чтобы угадать это. Также есть похожий вопрос.

Для файлов 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, я предлагаю вам проверить этот код, который будет определять общие форматы файлов и выбирать правильную кодировку: Как прочитать файл и автоматически указать правильную кодировку

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

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

Хорошая стратегия для решения этой проблемы - способ автоматического определения входной кодировки.

Я использую org.xml.sax.InputSource в Java 11, чтобы решить эту проблему:

      ...    
import org.xml.sax.InputSource;
...

InputSource inputSource = new InputSource(inputStream);
inputStreamReader = new InputStreamReader(
    inputSource.getByteStream(), inputSource.getEncoding()
  );

Входной образец:

      <?xml version="1.0" encoding="utf-16"?>
<rss xmlns:dc="https://purl.org/dc/elements/1.1/" version="2.0">
<channel>
...**strong text**

В простой 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).

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

new InputStreamReader(new FileInputStream(in), "ISO8859_1");
Другие вопросы по тегам