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

关于以下主题: JavaApp: 无法正确读取 iso-8859-1编码的文件

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

我尝试使用以下方法:

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

但是对于一个我知道是用 ISO8859 _ 1编码的文件,上面的代码产生 ASCII,这是不正确的,并且不允许我正确地将文件的内容返回到控制台。

364015 次浏览

你能在 构造函数中选择合适的字符集吗:

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

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

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

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

不管怎样,如果有必要的话,你可以试着自己猜一个编码。每种语言对每个字符都有一个共同的频率。在英语中字符出现的频率很高,但是却很少出现。在 ISO-8859-1流中通常没有0x00字符。但是 UTF-16流中有很多这样的信息。

或者: 你可以问用户。我已经看到了一些应用程序,它们以不同的编码提供文件的片段,并要求您选择“正确的”一个。

如果您不知道数据的编码,那么就不太容易确定,但是您可以尝试使用 图书馆来猜。还有 一个类似的问题

当然,您可以通过 解码将特定字符集的文件转换为 CharsetDecoder,并注意“畸形输入”或“无法映射的字符”错误。当然,这只能告诉您字符集是否错误; 而不能告诉您它是否正确。为此,您需要一个比较的基础来评估解码的结果,例如,您事先知道字符是否限于某个子集,或者文本是否遵守某种严格的格式?归根结底,字符集探测只是猜测,没有任何保证。

对于 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,我建议您查看这段代码,它将检测常见的文件格式并选择正确的编码: 如何读取文件并自动指定正确的编码

我发现了一个不错的第三方库,可以检测到实际的编码: Http://glaforge.free.fr/wiki/index.php?wiki=guessencoding

我没有进行广泛的测试,但它似乎工作。

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

看看这个: 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()
}

我使用了这个类似于 jchardet 的库来检测 Java 中的编码: Https://github.com/albfernandez/juniversalchardet

如果你使用 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();
}
}

记得把所有的尝试接住需要它。

我希望这对你有用。

以下是我的最爱:

编码检测器

依赖性:

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

TikaEncoding ingDetector 的一个替代方案是使用 Tika AutoDetectReader

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

使用哪个库?

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

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

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

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

我使用 IBM ICU4j 和 Mozilla JCharDet 作为内置组件,编写了一个用于检测 HTML 网页字符集编码的元 Java 工具。给你你可以找到我的工具,请先阅读 README 部分,然后再做其他事情。此外,您可以在我的 纸张和它的参考文献中找到这个问题的一些基本概念。

下面我提供了一些我在工作中经历过的有用的意见:

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

用普通的爪哇语:

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所需的字符集实现)

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

我在 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**