如何可靠地猜测MacRoman、CP1252、Latin1、UTF-8和ASCII之间的编码。

103
在工作中,似乎每周都会发生一些与编码相关的问题,比如错误、灾难或者大错特错。这个问题通常源于那些认为可以在不指定编码的情况下可靠地处理“文本”文件的程序员。但实际上是不行的。
因此,我们决定从现在开始禁止文件名以*.txt*.text结尾。这样做的想法是,这些扩展名会误导普通程序员对编码的重视,从而导致处理不当。最好的情况是根本没有扩展名,因为这样至少你知道你不知道你有什么。
然而,我们并不打算走得那么远。相反,我们希望您使用以编码结尾的文件名。例如,对于文本文件,文件名应该类似于README.asciiREADME.latin1README.utf8等等。

对于需要特定扩展名的文件,如果可以在文件本身中指定编码,例如在Perl或Python中,则应该这样做。对于像Java源代码这样没有内部设施的文件,您将在扩展名之前放置编码,例如SomeClass-utf8.java

对于输出,强烈推荐使用UTF-8。

但是对于输入,我们需要找出如何处理我们代码库中数千个名为*.txt的文件。我们想将它们全部重命名以适应我们的新标准。但我们不可能一个个查看它们。因此,我们需要一个实际有效的库或程序。

这些文件的编码方式各不相同,包括ASCII、ISO-8859-1、UTF-8、Microsoft CP1252或Apple MacRoman。虽然我们知道我们可以判断某些内容是否为ASCII,并且我们有很大的机会知道某些内容可能是UTF-8,但我们对8位编码感到困惑。由于我们在混合Unix环境(Solaris、Linux、Darwin)中运行,大多数桌面都是Mac,因此我们有很多令人讨厌的MacRoman文件。而这些尤其是一个问题。

最近我一直在寻找一种编程方法来确定文件使用的是以下哪种编码格式:

  1. ASCII
  2. ISO-8859-1
  3. CP1252
  4. MacRoman
  5. UTF-8

我还没有找到一个可靠地区分这三种不同的 8 位编码格式的程序或库。我们可能有超过一千个 MacRoman 文件,所以无论我们使用哪种字符集检测器,它都必须能够检测出这些文件。 我看了看 ICU charset detector library,但它无法处理 MacRoman。我还看了 Perl 和 Python 中执行相同操作的模块,但总体上情况都一样:没有支持检测 MacRoman 的功能。

因此,我正在寻找一个可靠地确定文件属于这五种编码中的哪一种(最好不止这五种)的现有库或程序。特别是它必须区分我提到的三种3位编码,尤其是MacRoman。这些文件超过99%是英语文本;还有一些其他语言的文件,但数量不多。
如果是库代码,我们更喜欢使用Perl、C、Java或Python编写。如果只是一个程序,那么我们并不在乎它使用的语言,只要它具有完整的源代码,可以在Unix上运行,并且没有任何限制。
是否有其他人遇到过这种问题,即存在大量旧文本文件以随机编码方式编码?如果是这样,您是如何尝试解决的,成功了吗?这是我问题中最重要的方面,但我也想知道您是否认为鼓励程序员使用实际编码命名(或重新命名)文件将有助于我们避免未来的问题。是否有人曾经试图在机构层面上强制执行这一点?如果是这样,那么它是否成功,为什么?

是的,我完全理解为什么在问题的性质下无法保证确定的答案。特别是对于小文件而言,您没有足够的数据可供使用。幸运的是,我们的文件很少是小文件。除了随机的README文件外,大多数文件的大小范围在50k到250k之间,许多文件更大。任何超过几K的文件大小都保证是英文。

问题领域是生物医学文本挖掘,因此我们有时需要处理广泛且极大的语料库,例如PubMedCentral的开放获取资源库。一个相当巨大的文件是BioThesaurus 6.0,大小为5.7 GB。这个文件特别恼人,因为它几乎全部是UTF-8。然而,一些蠢货把其中几行编码成了某种8位编码——我相信是Microsoft CP1252。在发现这个问题之前需要花费相当长的时间。:(


请参考以下链接以获取解决方案:https://dev59.com/p2855IYBdhLWcg3wlVca#4255439 - mpenkov
8个回答

88

首先,简单的情况:

ASCII

如果您的数据中没有超过0x7F的字节,则它是ASCII码。 (或者是7位ISO646编码,但这些已经非常过时了。)

UTF-8

如果您的数据通过UTF-8验证,则可以安全地假定它是UTF-8编码。由于UTF-8的严格验证规则,误报的可能性极其罕见。

ISO-8859-1与Windows-1252

这两种编码唯一的区别就是ISO-8859-1具有C1控制字符,而Windows-1252具有可打印字符€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“”•–—˜™š›œžŸ。我见过很多使用弯引号或短横线的文件,但没有一个使用C1控制字符。因此,不要考虑ISO-8859-1,只需检测Windows-1252即可。

现在只剩下一个问题。

如何区分MacRoman和cp1252?

这个比较棘手。

未定义字符

字节0x81、0x8D、0x8F、0x90、0x9D在Windows-1252中未使用。如果它们出现,则假定数据是MacRoman编码。

相同的字符

字节0xA2 (¢)、0xA3 (£)、0xA9 (©)、0xB1 (±)、0xB5 (µ)在两种编码中都相同。如果这些是唯一的非ASCII字节,则选择MacRoman或cp1252并不重要。

统计方法

计算您知道的UTF-8数据中的字符(而不是字节!)频率。确定最常见的字符。然后使用这些数据来确定cp1252或MacRoman字符哪个更常见。

例如,在我刚刚对100个随机英文维基百科文章进行的搜索中,最常见的非ASCII字符是·•–é°®’èö—。基于这个事实,
  • 字节0x92、0x95、0x96、0x97、0xAE、0xB0、0xB7、0xE8、0xE9或0xF6表示windows-1252。
  • 字节0x8E、0x8F、0x9A、0xA1、0xA5、0xA8、0xD0、0xD1、0xD5或0xE1表示MacRoman。
计算cp1252-suggesting字节和MacRoman-suggesting字节的数量,并选择其中数量最多的编码方式。

6
我接受了你的答案,因为没有更好的答案出现,而且你很好地写出了我一直在研究的问题。我确实有程序来嗅探那些字节,虽然你提出的数量大约是我自己想出的两倍。 - tchrist
10
终于开始实施了。结果发现维基百科不是好的训练数据。从1k个随机英文维基百科文章中,不包括语言部分,我得到了50k个非ASCII码点,但是分布并不可靠:中间点和子弹点过高等等。因此,我使用了全部采用UTF8编码的PubMed开放获取语料库,挖掘了超过14M个非ASCII码点。我利用这些数据建立了一个所有8位编码的相对频率模型,比你们的更加先进而且基于相同的思路。这证明在生物医学文本这个目标领域,这种方法具有高度预测性。我应该要发表一下这项工作。谢谢! - tchrist
5
我目前还没有任何MacRoman文件,但是使用CR作为行分隔符会不会提供一个有用的测试呢?这适用于较旧版本的Mac OS,尽管我不知道OS9是否也适用。 - Milliways
嗨,回答永远不会太晚。阅读了这个答案后,我扩展了Ruby Charlotte宝石。现在它可以很好地检测上述格式。我在此问题https://dev59.com/n2855IYBdhLWcg3wsWhq#64276978中附加了一个新的答案,其中包含主要代码。 - Tom Freudenberg
如果只涉及到0x00至0x7f,那么它可以是任何编码 - 但这并不重要,因为无论您假设什么编码,所有内容都将被相同地解码。 - gnasher729

10

更多的文档可以在这里找到:http://www.mozilla.org/projects/intl/detectorsrc.html,从那里,它建议如果您深入挖掘文档,您可以找到支持的字符集。 - Joel Berger
@Joel:我已经深入研究了源代码。那是一个修辞问题。x-mac-cyrillic 得到支持,x-mac-hebrew 在评论中有详细讨论,而 x-mac-anything-else 则没有提及。 - John Machin
@John Machin:很奇怪西里尔文和希伯来文得到了认可,但其他语言却没有。我只是提供了另一个文档来源,我还没有继续阅读,感谢你的努力! - Joel Berger

7

我尝试使用一种启发式方法(假设您已排除了ASCII和UTF-8):

  • 如果根本没有出现0x7f到0x9f,那么它很可能是ISO-8859-1,因为这些极少用于控制码。
  • 如果0x91到0x94经常出现,那么它很可能是Windows-1252,因为那些是“智能引号”,在该范围内最有可能用于英文文本的字符。要更加确定,可以寻找成对出现的情况。
  • 否则,它就是MacRoman,特别是如果您看到很多0xd2到0xd5(那是MacRoman中排版引号的位置)。

附注:

对于像Java源文件这样没有内部设施的文件,您将在扩展名之前放置编码,例如SomeClass-utf8.java

不要这样做!

Java编译器期望文件名与类名匹配,因此重命名文件将使源代码无法编译。正确的方法是猜测编码,然后使用native2ascii工具将所有非ASCII字符转换为Unicode转义序列


7
愚蠢的编译器!不,我们不能告诉人们他们只能使用ASCII;这已经不是20世纪60年代了。如果有@encoding注释,那么源代码在特定编码下的事实就不必被强制存储为外部信息,这是Java的一个非常愚蠢的缺陷,但Perl和Python都没有这个问题。这应该放在源代码中。不过,这不是我们主要的问题;我们的主要问题是成千上万的*.text文件。 - tchrist
3
实际上,编写自己的注解处理器来支持这样的注解并不是很难。但标准API没有包含这个功能仍然是一个令人尴尬的疏忽。 - Michael Borgwardt
即使Java支持@encoding,也不能确保编码声明是正确的 - dan04
4
你可以在 XML、HTML 或其他任何地方中提到编码声明的问题。但是,如果这些内容被定义在标准 API 中,与源代码相关的工具(特别是编辑器和集成开发环境)将会支持,这将可靠地防止人们“意外”创建不匹配声明的文件。请注意,这并不会改变原有含义。 - Michael Borgwardt
4
Java编译器希望文件名与类名匹配。只有在文件定义顶层公共类时才适用此规则。 - Matthew Flaschen

6

"Perl, C, Java, 或者 Python,按照这个顺序": 有趣的态度 :-)

"我们有很大的机会知道某些东西可能是UTF-8": 实际上,包含使用高位字节编码的其他字符集中的有意义文本的文件能够成功解码为UTF-8的机会微乎其微。

UTF-8策略(以最不喜欢的语言为例):

# 100% Unicode-standard-compliant UTF-8
def utf8_strict(text):
    try:
        text.decode('utf8')
        return True
    except UnicodeDecodeError:
        return False

# looking for almost all UTF-8 with some junk
def utf8_replace(text):
    utext = text.decode('utf8', 'replace')
    dodgy_count = utext.count(u'\uFFFD') 
    return dodgy_count, utext
    # further action depends on how large dodgy_count / float(len(utext)) is

# checking for UTF-8 structure but non-compliant
# e.g. encoded surrogates, not minimal length, more than 4 bytes:
# Can be done with a regex, if you need it

一旦确定它不是ASCII或UTF-8:
据我所知,Mozilla起源的字符集探测器不支持MacRoman,无论如何,在8位字符集方面,特别是英语方面,它们都做得不好,因为我认为它们依赖于检查解码是否在给定语言中有意义,忽略标点符号,并基于该语言的大量文档选择。
正如其他人所指出的,你真正只能使用高位设置的标点符号来区分cp1252和macroman。我建议在自己的文件上训练类似Mozilla的模型,而不是莎士比亚、汉萨德或KJV圣经,并考虑所有256个字节。我假设您的文件中没有标记(HTML、XML等),否则这将使概率失真。
您提到的文件大多数是UTF-8格式但无法解码。您也应该非常怀疑以下情况:
(1)据称以ISO-8859-1编码但包含范围在0x80至0x9F之间的“控制字符”的文件...这种情况非常普遍,草案HTML5标准规定要使用cp1252解码所有声明为ISO-8859-1的HTML流。
(2)解码为UTF-8格式正确,但生成的Unicode包含范围在U+0080至U+009F之间的“控制字符”的文件...这可能是将cp1252 / cp850(我见过它发生!)/等文件从“ISO-8859-1”转换为UTF-8格式导致的。
背景:我有一个湿润的星期天下午的项目,要创建一个基于Python的字符集探测器,它是面向文件的(而不是面向Web的),并且在包括cp850和cp437等旧型n 8位字符集方面表现良好。它还不够完善。我对训练文件感兴趣;您的ISO-8859-1 / cp1252 / MacRoman文件是否像您期望任何代码解决方案一样“无阻碍”?

1
语言选择的理由是环境。我们的大多数主要应用程序倾向于使用Java,小型实用程序和一些应用程序使用Perl。我们还有一些代码是用Python编写的。至少在首选方面,我主要是C和Perl程序员,因此我正在寻找一个可以插入到我们应用程序库中的Java解决方案,或者同样的Perl库。如果是C,我可以构建一个XS粘合层将其连接到Perl接口,但我以前从未在Python中这样做过。 - tchrist

3

正如您所发现的那样,没有一种完美的方法来解决这个问题,因为如果没有有关文件使用哪种编码的隐含知识,所有8位编码都完全相同:一组字节。所有字节对于所有8位编码都是有效的。

你能做到的最好的事情就是一些算法分析字节,基于某个字节在某种语言中使用某种编码的概率猜测文件使用的编码方式。但是它必须知道文件使用的语言,并且当您拥有混合编码的文件时,它变得完全无用。

好处是,如果您知道文件中的文本是用英语编写的,那么您不太可能注意到无论您决定为该文件使用哪种编码方式,所有提到的编码之间的差异都局限于指定英语语言中通常不使用的字符部分。您可能会遇到一些麻烦,比如文本使用特殊格式或特殊版本的标点符号(例如CP1252有几个引号字符的版本),但对于文本的要点,可能不会出现任何问题。


1
如果你能检测出除了macroman之外的所有编码,那么可以合理地假设那些无法解密的文件都是macroman编码。换句话说,只需制作一个无法处理的文件列表,并将其视为macroman处理。
另一种对这些文件进行分类的方法是创建一个基于服务器的程序,允许用户决定哪种编码没有乱码。当然,这将在公司内部进行,但是如果每天有100名员工处理几个文件,很快就会完成数千个文件。
最后,是否将所有现有文件转换为单一格式,并要求新文件采用该格式,这样会更好呢?

6
有趣!当我在被打断30分钟后第一次读到这个评论时,我将“macroman”读成“macro man”,直到我搜索了这个字符串才意识到它与“MacRoman”的联系。 - Adrian Pronk
+1 这个回答有些有趣。不确定它是好还是坏的想法。有人能想到一个现有的编码也可能不被检测到吗?未来是否可能会出现这样的编码? - username

1

根据https://dev59.com/n2855IYBdhLWcg3wsWhq#4200765所接受的答案,我可以改进Ruby宝石“Charlotte”以更准确地识别请求的编码。

我们在生产环境中使用它来检测CSV文件编码以进行导入

这是合理的部分(Ruby)

UTF8HASBOM = /^\xEF\xBB\xBF/n      #  [239, 187, 191]
UTF32LEBOM = /^\xFF\xFE\x00\x00/n  # [255, 254, 0, 0]
UTF32BEBOM = /^\x00\x00\xFE\xFF/n  # [0, 0, 254, 255]

UTF16LEBOM = /^\xFF\xFE/n                # [255, 254]
UTF16BEBOM = /^\xFE\xFF/n                # [254, 255]

NOTIN1BYTE = /[\x00-\x06\x0B\x0E-\x1A\x1C-\x1F\x7F]/n
NOTISO8859 = /[\x00-\x06\x0B\x0E-\x1A\x1C-\x1F\x7F\x80-\x84\x86-\x9F]/n

# Information to identify MacRoman
# https://dev59.com/n2855IYBdhLWcg3wsWhq
NOTINCP1252 = /[\x81\x8D\x8F\x90\x9D]/n
CP1252CHARS = /[\x92\x95\x96\x97\xAE\xB0\xB7\xE8\xE9\xF6]/n
MCROMNCHARS = /[\x8E\x8F\x9A\xA1\xA5\xA8\xD0\xD1\xD5\xE1]/n

detect.force_encoding('BINARY') # Needed to prevent non-matching regex charset.
sample = detect[0..19]     # Keep sample string under 23 bytes.
detect.sub!(UTF8HASBOM, '') if sample[UTF8HASBOM] # Strip any UTF-8 BOM.

# See: http://www.daniellesucher.com/2013/07/23/ruby-case-versus-if/
if    sample.ascii_only? && detect.force_encoding('UTF-8').valid_encoding?

elsif sample[UTF32LEBOM] && detect.force_encoding('UTF-32LE').valid_encoding?
elsif sample[UTF32BEBOM] && detect.force_encoding('UTF-32BE').valid_encoding?
elsif sample[UTF16LEBOM] && detect.force_encoding('UTF-16LE').valid_encoding?
elsif sample[UTF16BEBOM] && detect.force_encoding('UTF-16BE').valid_encoding?

elsif detect.force_encoding('UTF-8').valid_encoding?

elsif detect.force_encoding('BINARY')[NOTISO8859].nil?
  detect.force_encoding('ISO-8859-1')

elsif detect.force_encoding('BINARY')[NOTIN1BYTE].nil?

  if  detect.force_encoding('BINARY')[NOTINCP1252].nil? &&
            detect.force_encoding('BINARY').scan(MCROMNCHARS).length < detect.force_encoding('BINARY').scan(CP1252CHARS).length

      detect.force_encoding('Windows-1252')
  else
      detect.force_encoding('MacRoman')
  end

else  detect.force_encoding('BINARY')
end

1
有没有人遇到过大量的遗留文本文件会随机编码的问题?如果有,你是如何尝试解决的,解决得如何?
我目前正在编写一个将文件转换为XML的程序。它必须自动检测每个文件的类型,这是确定文本文件编码的问题的超集。为了确定编码,我使用了贝叶斯方法。也就是说,我的分类代码计算了一个概率(可能性),即一种文本文件具有特定编码的概率,以及它所理解的所有编码。然后,程序选择最可能的解码器。对于每个编码,贝叶斯方法的工作方式如下。
  1. 根据每种编码的频率,设置文件在编码中的初始(先验)概率。
  2. 逐个检查文件中的每个字节。查找字节值以确定该字节值存在与文件实际上是否在该编码中之间的相关性。使用该相关性计算文件在编码中的新的(后验)概率。如果您有更多字节要检查,请在检查下一个字节时将后验概率作为先验概率。
  3. 当您到达文件结尾时(我只查看前1024个字节),您得到的概率就是文件在编码中的概率。

事实证明,如果您计算信息内容(即对数的比率),而不是计算概率,则贝叶斯定理变得非常容易: info = log(p / (1.0 - p))

您需要通过检查手动分类的文件语料库来计算初始先验概率和相关性。


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