如何检测文本文件的编码/代码页?

311
在我们的应用程序中,我们从不同的来源接收文本文件( .txt .csv 等)。在读取这些文件时,由于这些文件是使用不同/未知的代码页创建的,因此有时会包含垃圾内容。
是否有一种方法可以自动检测文本文件的代码页? StreamReader 构造函数中的 detectEncodingFromByteOrderMarks 对于 UTF8 和其他标记文件的 unicode 有效,但我正在寻找一种检测代码页(例如 ibm850 windows1252 )的方法。
感谢您的回答,这是我所做的事情。
我们收到的文件来自终端用户,他们对代码页毫不了解。接收器也是终端用户,到目前为止,这就是他们对代码页的了解:代码页存在,并且很麻烦。
解决方案:
  • 在记事本中打开接收到的文件,查看乱码文本。如果某个人被称为弗朗索瓦或其他什么,请利用您的人类智慧猜测一下。
  • 我创建了一个小型应用程序,用户可以使用它来打开文件,并输入用户知道将出现在该文件中的文本,当使用正确的代码页时。
  • 循环遍历所有代码页,并显示提供用户文本解决方案的代码页。
  • 如果有多个代码页弹出,请要求用户指定更多文本。
21个回答

268

您无法检测编码页,需要告知该编码页。您可以分析字节并猜测,但这可能会导致一些奇怪(有时很有趣)的结果。我现在找不到它了,但我相信记事本可以被欺骗以显示中文的英文文本。

无论如何,这就是您需要阅读的内容: 软件开发人员绝对必须了解的 Unicode 和字符集的绝对最低限度(无借口!)

具体而言,Joel表示:

编码的单个最重要事实

如果您完全忘记我刚才解释的所有内容,请记住一个极其重要的事实。没有知道使用哪种编码的字符串是毫无意义的。不能再把头埋在沙子里假装“纯文本”是 ASCII。 没有纯文本这回事。

如果您拥有一个字符串,无论是在内存中、文件中还是电子邮件中,您都必须知道它使用的编码才能正确地解释或向用户显示它。


57
我对这个答案点了踩,有两个原因。首先,说“你需要被告知”没有帮助。谁会告诉我,他们会通过什么方式这样做?如果是我保存这个文件的人,我要问谁?问我自己吗?其次,该文章并不特别有助于回答问题。这篇文章更像是一个以David Sedaris风格叙述编码历史的资源。我欣赏这种叙述方式,但它并不能简单/直接地回答问题。 - geneorama
9
@ geneorama,我认为Joel的文章比我能做到的更好地回答了你的问题,但还是有些要说的...媒介肯定取决于接收文本的环境。最好在文件(或其他)中包含这些信息(我想到的是HTML和XML)。否则,发送文本的人应该被允许提供这些信息。如果您是创建文件的人,那么您怎么可能不知道它使用的编码方式呢? - JV.
5
@geneorama,继续说... 最后,我想文章没有简单回答那个问题的主要原因是因为这个问题没有简单的答案。如果问题是“我怎样才能猜测......”,那么我的回答会有所不同。 - JV.
1
@JV,后来我了解到XML/HTML可以指定字符编码,感谢你提到这个有用的小贴士。 - geneorama
1
@JV “创建文件”可能不是一个好的选择。我假设用户可以指定生成的文件的编码方式。最近,我使用Hive从Hadoop集群中“创建”了一个文件,并将其传递给FTP,然后下载到各个客户端机器上。结果里面有一些Unicode垃圾,但我不知道哪一步出了问题。我从未明确指定过编码方式。我希望能够在每个步骤中检查编码方式。 - geneorama
显示剩余2条评论

35

9
有趣的是,我的Firefox 3.05安装程序将该页面检测为UTF-8编码,并显示一些带有问号的菱形字符,尽管源代码中有一个Windows-1252的meta标签。手动更改字符编码后,文档显示正常。 - devstuff
5
您的句子“If you're looking to detect non-UTF encodings (i.e. no BOM)”有点误导性;Unicode标准不建议在UTF-8文档中添加BOM!(这个建议或缺乏建议是许多头疼问题的根源)。参考资料:http://en.wikipedia.org/wiki/Byte_order_mark#UTF-8 - Tao
1
这样做是为了使您可以连接UTF-8字符串,而不会累积冗余的BOM。此外,与UTF-16不同,UTF-8不需要字节顺序标记。 - sashoalm

32

你尝试过Mozilla通用字符集检测的C#端口吗?

示例来源于http://code.google.com/p/ude/

public static void Main(String[] args)
{
    string filename = args[0];
    using (FileStream fs = File.OpenRead(filename)) {
        Ude.CharsetDetector cdet = new Ude.CharsetDetector();
        cdet.Feed(fs);
        cdet.DataEnd();
        if (cdet.Charset != null) {
            Console.WriteLine("Charset: {0}, confidence: {1}", 
                 cdet.Charset, cdet.Confidence);
        } else {
            Console.WriteLine("Detection failed.");
        }
    }
}    

1
在 Windows-1252 类型下完美运行。 - seebiscuit
你如何使用它来将文本文件读取为字符串?CharsetDetector以字符串格式返回编码名称。 - Bartosz
@Bartosz `private Encoding GetEncodingFromString(string encoding) { try { return Encoding.GetEncoding(encoding); } catch { return Encoding.ASCII; } }`@Bartosz `private Encoding GetEncodingFromString(string encoding) { try { return Encoding.GetEncoding(encoding); } catch { return Encoding.ASCII; } }` - PrivatePyle

17

你无法检测字符集

这是显然错误的。每个网络浏览器都有一种通用字符集探测器,以处理没有任何编码指示的页面。Firefox就有一个。您可以下载代码并查看它是如何工作的。在此处参阅一些文档:这里。基本上,这是一种启发式方法,但非常有效。

在合理数量的文本情况下,甚至可以检测语言。

这是另一个我刚刚使用Google找到的。


46
“启发式算法”- 因此浏览器并不能完全检测它,而是在做出“有根据的猜测”。 “非常有效”- 因此它并不总是有效?听起来我们有共识。 - JV.
10
HTML的标准规定,如果文档未定义字符集,则应将其视为以UTF-8编码。 - Jon Trauntvein
5
除非我们在阅读非标准的HTML文档或非HTML文档,否则这很酷。 - Kos
2
这个答案是错误的,所以我不得不投反对票。说无法检测代码页是错误的。你可以猜测并且你的猜测可能相当准确,但你不能“检测”一个代码页。 - z80crew
2
根据HTML5规范,即使使用US-ASCII编码,也需要提供字符编码声明。如果缺少声明,则会使用启发式算法,而不是回退到UTF8。@JonTrauntvein - z80crew
显示剩余2条评论

10

我知道这个问题现在问有些晚,而且这个解决方案可能不适用于某些人(因为它使用了英语中心的偏见和缺乏统计/经验测试),但对我来说效果非常好,特别是用于处理上传的CSV数据:

http://www.architectshack.com/TextFileEncodingDetector.ashx

优点:

  • 内置BOM检测
  • 默认/备选编码可自定义
  • 对包含一些异国情调数据(例如法国名字)的以西欧为基础的文件(混合UTF-8和Latin-1样式的文件)相当可靠(根本上是通过美国和西欧环境的大部分)。

注意:我就是写这个类的人,所以显然要适度考虑!:)


9

如果有人在寻找一种93.9%的解决方案,这个方法对我有效:

public static class StreamExtension
{
    /// <summary>
    /// Convert the content to a string.
    /// </summary>
    /// <param name="stream">The stream.</param>
    /// <returns></returns>
    public static string ReadAsString(this Stream stream)
    {
        var startPosition = stream.Position;
        try
        {
            // 1. Check for a BOM
            // 2. or try with UTF-8. The most (86.3%) used encoding. Visit: http://w3techs.com/technologies/overview/character_encoding/all/
            var streamReader = new StreamReader(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true), detectEncodingFromByteOrderMarks: true);
            return streamReader.ReadToEnd();
        }
        catch (DecoderFallbackException ex)
        {
            stream.Position = startPosition;

            // 3. The second most (6.7%) used encoding is ISO-8859-1. So use Windows-1252 (0.9%, also know as ANSI), which is a superset of ISO-8859-1.
            var streamReader = new StreamReader(stream, Encoding.GetEncoding(1252));
            return streamReader.ReadToEnd();
        }
    }
}

非常好的解决方案。如果需要允许超过2种编码(UTF-8和ASCI 1252),可以轻松地将ReadAsString()的主体包装在允许编码的循环中。 - ViRuSTriNiTy
1
在尝试了许多示例后,我终于找到了你的代码。我现在感到非常开心呢。哈哈,谢谢!!! - SedJ601
这可能不是如何检测1252与1250的答案,但它绝对应该是“如何检测UTF-8”的答案,无论是否有BOM! - chuckc
@chuckc,没有一种好的方法可以检测不同的无BOM单字节编码之间的区别。在这个层面上,你只能纯粹地依靠启发式算法。 - Nyerguds

7
寻找不同的解决方案时,我发现这个https://code.google.com/p/ude/的方案有点重。我需要一些基本的编码检测,基于前4个字节,可能还要进行xml字符集检测 - 所以我从互联网上获取了一些示例源代码,并添加了略微修改过的版本http://lists.w3.org/Archives/Public/www-validator/2002Aug/0084.html ,它是为Java编写的。
    public static Encoding DetectEncoding(byte[] fileContent)
    {
        if (fileContent == null)
            throw new ArgumentNullException();

        if (fileContent.Length < 2)
            return Encoding.ASCII;      // Default fallback

        if (fileContent[0] == 0xff
            && fileContent[1] == 0xfe
            && (fileContent.Length < 4
                || fileContent[2] != 0
                || fileContent[3] != 0
                )
            )
            return Encoding.Unicode;

        if (fileContent[0] == 0xfe
            && fileContent[1] == 0xff
            )
            return Encoding.BigEndianUnicode;

        if (fileContent.Length < 3)
            return null;

        if (fileContent[0] == 0xef && fileContent[1] == 0xbb && fileContent[2] == 0xbf)
            return Encoding.UTF8;

        if (fileContent[0] == 0x2b && fileContent[1] == 0x2f && fileContent[2] == 0x76)
            return Encoding.UTF7;

        if (fileContent.Length < 4)
            return null;

        if (fileContent[0] == 0xff && fileContent[1] == 0xfe && fileContent[2] == 0 && fileContent[3] == 0)
            return Encoding.UTF32;

        if (fileContent[0] == 0 && fileContent[1] == 0 && fileContent[2] == 0xfe && fileContent[3] == 0xff)
            return Encoding.GetEncoding(12001);

        String probe;
        int len = fileContent.Length;

        if( fileContent.Length >= 128 ) len = 128;
        probe = Encoding.ASCII.GetString(fileContent, 0, len);

        MatchCollection mc = Regex.Matches(probe, "^<\\?xml[^<>]*encoding[ \\t\\n\\r]?=[\\t\\n\\r]?['\"]([A-Za-z]([A-Za-z0-9._]|-)*)", RegexOptions.Singleline);
        // Add '[0].Groups[1].Value' to the end to test regex

        if( mc.Count == 1 && mc[0].Groups.Count >= 2 )
        {
            // Typically picks up 'UTF-8' string
            Encoding enc = null;

            try {
                enc = Encoding.GetEncoding( mc[0].Groups[1].Value );
            }catch (Exception ) { }

            if( enc != null )
                return enc;
        }

        return Encoding.ASCII;      // Default fallback
    }

只需读取文件的前1024个字节就足够了,但我正在加载整个文件。


7

Notepad++ 默认支持此功能。它还支持更改。


4
工具“uchardet”可以使用每个字符集的字符频率分布模型来很好地完成此任务。更大的文件和更“典型”的文件具有更高的置信度(显而易见)。
在Ubuntu上,您只需运行命令apt-get install uchardet即可安装。
在其他系统上,请访问https://github.com/BYVoid/uchardet以获取源代码、用法和文档。

在Mac上通过Homebrew安装:brew install uchardet - Paul B

4
我之前用 Python 做过类似的事情。基本上,你需要从各种编码中获取大量的样本数据,将其通过滑动的两个字节窗口分解并存储在一个字典(哈希)中,以字节对为键提供值列表的编码。
有了这个字典(哈希),你就可以对输入文本进行以下操作:
- 如果它以任何 BOM 字符开头(UTF-16-BE 的 '\xfe\xff',UTF-16-LE 的 '\xff\xfe',UTF-8 的 '\xef\xbb\xbf' 等),我会按照建议处理。 - 如果没有,则取足够大的文本样本,取样本的所有字节对,并选择从字典中建议的最不常见的编码。
如果您还采样了不以任何 BOM 开头的 UTF 编码文本,则第二步将涵盖从第一步中滑落的内容。
到目前为止,这对我很有效(样本数据和随后的输入数据是各种语言的字幕),错误率逐渐减少。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接