Java:如何确定流的正确字符集编码


140

参考以下线程: Java App:无法正确读取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
爱德华说得对,“您无法确定任意字节流的编码”。所有其他建议都为您提供了最佳猜测的方法(和库)。但是最后他们仍然是猜测。
米海妮塔(Mihai Nita)

9
Reader.getEncoding返回设置阅读器使用的编码,在您的情况下为默认编码。
Karol S

Answers:


70

我已经使用了类似于jchardet的该库来检测Java中的编码:http : //code.google.com/p/juniversalchardet/


6
我发现这更准确:jchardet.sourceforge.net(我正在测试以ISO 8859-1,windows-1252,utf-8编码的西欧语言文档)
Joel

1
此juniversalchardet不起作用。即使文件是100%Windows-1212编码的,它也大部分时间提供UTF-8。
Brain

1
juniversalchardet 现在位于GitHub上
迪蒙

它无法检测到东欧Windows-1250
BernhardDöbler,18年

我尝试按照以下代码段从“ cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt ”中检测文件,但检测到的字符集为空。ud =新的UniversalDetector(null); byte [] bytes = FileUtils.readFileToByteArray(new File(file)); ud.handleData(bytes,0,bytes.length); ud.dataEnd(); detectedCharset = ud.getDetectedCharset();
Rohit Verma

105

您无法确定任意字节流的编码。这就是编码的本质。编码是指字节值及其表示形式之间的映射。因此,每种编码“都可以”是正确的。

getEncoding()方法将返回其设置的编码方式(读取的JavaDoc),用于该流。它不会为您猜测编码。

一些流告诉您创建时使用了哪种编码:XML,HTML。但不是任意字节流。

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

或者:您可以询问用户。我已经看过一些应用程序,这些应用程序以不同的编码形式向您显示该文件的摘要,并要求您选择“正确的”文件。



23
那么我的编辑器notepad ++如何知道如何打开文件并显示正确的字符?
mmm

12
@Hamidam幸运的是,它可以为您显示正确的字符。当它猜错时(通常是这样),有一个选项(菜单>>编码)可让您更改编码。
Pacerier,2012年

15
@Eduard:“所以每个编码“都可以”是正确的。” 不太正确。许多文本编码具有几种无效的模式,这些模式标志着文本可能不是该编码。实际上,给定文件的前两个字节,只有38%的组合是有效的UTF8。前5个代码点成为有效UTF8的几率小于0.77%。同样,UTF16BE和LE通常很容易通过大量零字节及其位置来识别。
Mooing Duck 2012年

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
我尝试了一下,但是失败了:我在Eclipse中制作了2个文本文件,都包含“öäüß”。一组设置为iso编码,另一组设置为utf8-都被检测为utf8!因此,我尝试在HD(Windows)上的某个位置保存了文件-该文件被正确检测到(“ Windows-1252”)。然后我在高清上创建了两个新文件,一个用编辑器编辑,另一个用记事本++编辑。在两种情况下都检测到“ Big5”(中文)!
dermoritz

2
编辑:好吧,我应该检查cm.getConfidence() -我的短“äöüß”信心是10.所以我必须决定什么信心是好的足够多的-但是那是绝对OK的这项工作(字符集检测)
dermoritz


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

猜测编码

依赖关系:

<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。
Brain

1
@Brain:您测试的文件实际上是否为UTF-8格式,并且是否包含BOM(en.wikipedia.org/wiki/Byte_order_mark)?
Benny Neugebauer

@BennyNeugebauer文件是没有BOM的UTF-8。我通过更改编码并断言“ Umlaute”仍然可见,使用Notepad ++对其进行了检查。
Brain

13

您当然可以通过使用对其进行解码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方法完全读取输入流。对于大型输入流,这可能是一个问题。所有这些库将读取整个输入流。这将意味着要花费大量时间来检测字符集。

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



6

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

我已经编写了一个元Java工具,使用IBM ICU4j和Mozilla JCharDet作为内置组件来检测HTML网页的字符集编码。在这里您可以找到我的工具,请先阅读自述文件部分。另外,您可以在本文中找到此问题的一些基本概念及其参考文献中。

在贝娄,我提供了一些有益的意见,这些意见对我的工作很有帮助:

  • 字符集检测不是一个万无一失的过程,因为它基本上是基于统计数据的,而实际上发生的是猜测没有检测到
  • 在这种情况下,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都无法处理Windows-1251和Windows-1256编码的HTML页面。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();
    }
}

记住要把所有的try-catch放入它。

希望这对您有用。


海事组织,这个答案是完美的。如果要使用ICU4j,请尝试以下一种方法:stackoverflow.com/a/4013565/363573
斯蒂芬


2

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

UTF-8和UTF-16文件在文件的最开头包括字节顺序标记(BOM)。BOM是零宽度的不间断空间。

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

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

对于Java,建议您检查以下代码,该代码将检测常见的文件格式并选择正确的编码: 如何读取文件并自动指定正确的编码


15
并非所有UTF-8或UTF-16文件都具有BOM表,因为它不是必需的,因此不建议使用UTF-8 BOM表。
ChristofferHammarström,

1

TikaEncodingDetector的替代方法是使用Tika AutoDetectReader

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

Tike AutoDetectReader使用随ServiceLoader加载的EncodingDetector。您使用哪个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(在您未列出的其他许多文件中)将始终成功。而且,当然,这只是猜测,无法恢复丢失的元数据,这对于文本文件通信至关重要。
汤姆·布洛杰特'18

@TomBlodget,您好,您是否建议编码顺序应该不同?
安德烈斯(Andres)'18

3
我说很多人会“工作”,但只有一个是“正确的”。而且您不需要测试ISO-8859-1,因为它总是可以工作。
汤姆·布洛杰特'18

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.