Java:如何确定流的正确charset编码

时间:2009-01-31 15:34:10

标签: java file encoding stream character-encoding

参考以下主题: Java App : Unable to read iso-8859-1 encoded file correctly

以编程方式确定输入流/文件的正确字符集编码的最佳方法是什么?

我尝试过使用以下内容:

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

但是在我知道用ISO8859_1编码的文件中,上面的代码产生了ASCII,这是不正确的,并且不允许我正确地将文件内容呈现回控制台。

16 个答案:

答案 0 :(得分:97)

您无法确定任意字节流的编码。这是编码的本质。编码意味着字节值与其表示之间的映射。所以每个编码“都可能”是正确的。

getEncoding()方法将返回为流设置的编码(读取JavaDoc)。它不会猜测你的编码。

某些流告诉您使用哪种编码来创建它们:XML,HTML。但不是任意的字节流。

无论如何,如果必须,您可以尝试自己猜测编码。每种语言都有一个共同的频率。在英语中,char e经常出现,但ê似乎很少出现。在ISO-8859-1流中,通常没有0x00字符。但是UTF-16流有很多。

或者:你可以问用户。我已经看过应用程序以不同的编码为您呈现文件的片段,并要求您选择“正确的”。

答案 1 :(得分:65)

我使用过这个库,类似于jchardet来检测Java中的编码: http://code.google.com/p/juniversalchardet/

答案 2 :(得分:33)

检查一下: 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()
}

答案 3 :(得分:24)

以下是我的最爱:

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

答案 4 :(得分:13)

您可以decoding使用CharsetDecoder 验证特定字符集的文件,并注意“malformed-input”或“unmappable-character”错误。当然,这只会告诉你字符集是否错误;它没有告诉你它是否正确。为此,您需要一个比较基础来评估解码结果,例如:你是否知道如果字符被限制在一些子集中,或者文本是否遵循某种严格的格式?最重要的是,charset检测是猜测而没有任何保证。

答案 5 :(得分:10)

使用哪个库?

在撰写本文时,他们出现了三个图书馆:

我不包括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方法完全读取输入流。对于大型输入流,这可能是一个问题。所有这些库都将读取整个输入流。这意味着检测字符集的时间消耗很大。

可以将初始数据加载限制为几个字节,并仅对这几个字节执行字符集检测。

答案 6 :(得分:7)

上面的库是简单的BOM检测器,当然只有在文件开头有BOM时才能使用。看一下扫描文本的http://jchardet.sourceforge.net/

答案 7 :(得分:5)

我找到了一个可以检测实际编码的不错的第三方库: http://glaforge.free.fr/wiki/index.php?wiki=GuessEncoding

我没有广泛测试它,但似乎有效。

答案 8 :(得分: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();
                }
            }

记得把所有的try catch都需要它。

我希望这适合你。

答案 9 :(得分:4)

如果您不知道数据的编码,则确定起来并不容易,但您可以尝试使用library to guess it。此外,还有a similar question

答案 10 :(得分:4)

据我所知,在这种情况下,没有一般的图书馆适合所有类型的问题。因此,对于每个问题,您应该测试现有的库并选择满足您的问题约束的最佳库,但通常都不适合。在这些情况下,您可以编写自己的编码检测器!正如我写的那样......

我编写了一个meta java工具,用于检测HTML网页的字符集编码,使用IBM ICU4j和Mozilla JCharDet作为内置组件。 Here您可以找到我的工具,请先阅读README部分。此外,您可以在我的paper及其参考资料中找到此问题的一些基本概念。

贝娄我提供了一些我在工作中遇到的有用的评论:

  • Charset检测不是一个万无一失的过程,因为它基本上是基于统计数据,实际发生的是猜测而非检测
  • icu4j是IBM,imho
  • 在此背景下的主要工具
  • TikaEncodingDetector和Lucene-ICU4j都在使用icu4j,他们的准确度与我测试中的icu4j没有任何有意义的区别(至多%1,我记得)
  • icu4j比jchardet更通用,icu4j对IBM家族编码有点偏向,而jchardet强烈反对utf-8
  • 由于在HTML世界中广泛使用UTF-8;总体而言,jchardet是比icu4j更好的选择,但不是最好的选择!
  • icu4j非常适合东亚特定编码,如EUC-KR,EUC-JP,SHIFT_JIS,BIG5和GB家族编码
  • icu4j和jchardet在使用Windows-1251和Windows-1256编码处理HTML页面方面都很糟糕。 Windows-1251又名cp1251广泛用于基于西里尔语的语言,如俄语和Windows-1256又称cp1256广泛用于阿拉伯语
  • 几乎所有编码检测工具都使用统计方法,因此输出的准确性很大程度上取决于输入的大小和内容
  • 某些编码基本上只是部分差异相同,因此在某些情况下,猜测或检测到的编码可能是错误的,但同时也是如此!关于Windows-1252和ISO-8859-1。 (参见本文5.2部分的最后一段)

答案 11 :(得分:2)

对于ISO8859_1文件,没有一种简单的方法可以将它们与ASCII区分开来。但是对于Unicode文件,通常可以根据文件的前几个字节检测到这一点。

UTF-8和UTF-16文件在文件的最开头包含Byte Order Mark(BOM)。 BOM是零宽度不间断空间。

不幸的是,由于历史原因,Java不会自动检测到这种情况。记事本等程序将检查BOM并使用适当的编码。使用unix或Cygwin,您可以使用file命令检查BOM。例如:

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

对于Java,我建议您查看此代码,它将检测常见的文件格式并选择正确的编码:How to read a file and automatically specify the correct encoding

答案 12 :(得分:1)

TikaEncodingDetector的替代方法是使用Tika AutoDetectReader

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

答案 13 :(得分:0)

在纯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

答案 14 :(得分:0)

处理这个问题的一个好策略是使用一种自动检测输入字符集的方法。

我在 Java 11 中使用 org.xml.sax.InputSource 来解决它:

...    
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**

答案 15 :(得分:-11)

您可以在Constructor中选择适当的字符集:

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