ChatGPT解决这个技术问题 Extra ChatGPT

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

参考以下线程: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,这是不正确的,并且不允许我将文件的内容正确地呈现回控制台。

Eduard 是对的,“您无法确定任意字节流的编码”。所有其他建议都为您提供了进行最佳猜测的方法(和库)。但最终他们仍然是猜测。
Reader.getEncoding 返回阅读器设置使用的编码,在您的情况下是默认编码。
System.getProperty("file.encoding") 它返回字符串。前 - FileInputStream fis = new FileInputStream(path); String encoding = System.getProperty("fis.encoding");

A
Agostino

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

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

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

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

或者:您可以询问用户。我已经看到应用程序以不同的编码向您显示文件的片段,并要求您选择“正确”的那个。


那么我的编辑器 notepad++ 是如何知道如何打开文件并向我显示正确的字符的呢?
@Hamidam 幸运的是它向您展示了正确的角色。当它猜错时(而且它经常猜错),有一个选项(菜单>>编码)允许您更改编码。
@Eduard:“所以每个编码都“可能”是正确的。”不太对。许多文本编码有几个无效的模式,这是文本可能不是那种编码的标志。事实上,给定文件的前两个字节,只有 38% 的组合是有效的 UTF8。前 5 个代码点偶然成为有效 UTF8 的几率小于 0.77%。同样,UTF16BE 和 LE 通常很容易通过大量的零字节和它们的位置来识别。
能够获得至少与 Notepad++ 或只是普通 Notepad 一样准确的方法会很好。没有人能告诉我们那是什么吗?
K
Kalle Richter

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


我发现这样更准确:jchardet.sourceforge.net(我正在测试以 ISO 8859-1、windows-1252、utf-8 编码的西欧语言文档)
这个 juniversalchardet 不起作用。它大部分时间都提供 UTF-8,即使文件是 100% windows-1212 编码的。
它没有检测到东欧 windows-1250
我尝试使用以下代码片段从“cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt”中检测文件,但检测到的字符集为空。通用检测器 ud = new UniversalDetector(null); byte[] bytes = FileUtils.readFileToByteArray(new File(file)); ud.handleData(bytes, 0, bytes.length); ud.dataEnd();检测到的字符集 = ud.getDetectedCharset();
Juniversalchardet 不支持 ISO-8859-1,这是最常见的字符集之一。
M
Maxim Veksler

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

我试过了,但失败了:我在 Eclipse 中制作了 2 个文本文件,都包含“öäüß”。一组设置为 iso 编码,一组设置为 utf8 - 两者都被检测为 utf8!所以我尝试了在我的高清(windows)某处保存的文件 - 这个文件被正确检测到(“windows-1252”)。然后我在 hd 上创建了两个新文件,一个用编辑器编辑,另一个用 notepad++ 编辑。在这两种情况下都检测到“Big5”(中文)!
编辑:好的,我应该检查 cm.getConfidence() - 用我的简短“äöüß”信心是 10。所以我必须决定什么信心足够好 - 但这对于这项工作来说绝对没问题(字符集检测)
示例代码的直接链接:userguide.icu-project.org/conversion/detection
使用 ICU4J 进行字符集检测的主要问题是 JAR 的大小为 13MB。我从 ICU4J 中提取了 chardet 功能并将其打包到 github.com/sigpwned/chardet4j 的一个独立的 75KB 库中。相同的代码,占用空间更小。
B
Benny Neugebauer

这是我的最爱:

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

注意: TikaEncodingDetector 1.1 实际上是 ICU4J 3.4 CharsetDectector 类的薄包装。
不幸的是,这两个库都不起作用。在一种情况下,它将德语变音符号识别为 ISO-8859-1 和 US-ASCII 的 UTF-8 文件。
@Brain:您的测试文件实际上是 UTF-8 格式吗?它是否包含 BOM(en.wikipedia.org/wiki/Byte_order_mark)?
@BennyNeugebauer 该文件是没有 BOM 的 UTF-8。我用记事本++检查了它,也通过更改编码并断言“元音变音”仍然可见。
Z
Zach Scrivena

您当然可以验证特定字符集的文件,方法是使用 decodingCharsetDecoder 并注意“格式错误的输入”或“不可映射的字符”错误。当然,这只会告诉您字符集是否错误;它不会告诉你它是否正确。为此,您需要一个比较基础来评估解码结果,例如,您是否事先知道字符是否限制在某个子集,或者文本是否遵循某种严格的格式?底线是字符集检测是没有任何保证的猜测。


C
Community

使用哪个库?

在撰写本文时,它们是出现的三个库:

猜测编码

ICU4j

朱尼韦尔沙尔德特

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

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


f
faghani

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

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

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

字符集检测不是一个万无一失的过程,因为它本质上是基于统计数据,而实际发生的是猜测而不是检测

icu4j 是 IBM 在这种情况下的主要工具,恕我直言

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部分的最后一段)


L
Lorrat

上面的库是简单的 BOM 检测器,当然只有在文件开头有 BOM 时才有效。看看 http://jchardet.sourceforge.net/,它确实扫描了文本


只是在提示,但此站点上没有“以上” - 考虑说明您所指的库。
f
falcon

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

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


“GuessEncoding”项目网站的链接是:xircles.codehaus.org/p/guessencoding
M
Miss Chanandler Bong

如果您使用 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 都需要它。

我希望这对你有用。


IMO,这个答案是完美的。如果您想使用 ICU4j,请改用这个:stackoverflow.com/a/4013565/363573
C
Community

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


j
jdknight

对于 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


并非所有 UTF-8 或 UTF-16 文件都有 BOM,因为它不是必需的,并且不鼓励使用 UTF-8 BOM。
S
Stephan

TikaEncodingDetector 的替代方法是使用 Tika AutoDetectReader

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

Tike AutoDetectReader 使用通过 ServiceLoader 加载的 EncodingDetector。您使用哪些 EncodingDetector 实现?
D
Daniel De León

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

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

A
Andres

在纯 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(还有很多你没有列出的)总是会成功的。当然,这只是猜测,它无法恢复丢失的对文本文件通信至关重要的元数据。
嗨@TomBlodget,您是否建议编码顺序应该不同?
我说很多人会“工作”,但只有一个人是“正确的”。而且您不需要测试 ISO-8859-1,因为它总是“有效”。
K
Kevin

您能否在 Constructor 中选择合适的字符集:

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

这里的重点是查看是否可以通过编程方式确定字符集。
不,它不会为你猜测。你必须提供它。
正如这里的一些答案所建议的,可能有一种启发式方法stackoverflow.com/questions/457655/java-charset-and-windows/…