如何检测和修复不正确的字符编码

Posted

技术标签:

【中文标题】如何检测和修复不正确的字符编码【英文标题】:How to detect and fix incorrect character encoding 【发布时间】:2019-06-25 20:32:53 【问题描述】:

上游服务读取 UTF-8 字节流,假设它们是 ISO-8859-1,将 ISO-8859-1 应用到 UTF-8 编码,并将它们发送到我的服务,标记为 UTF-8。

上游服务不受我控制。他们可能会修复它,它可能永远不会修复。

我知道我可以通过将 UTF-8 应用于 ISO-8859-1 编码然后将字节标记为 UTF-8 来修复编码。但是,如果我的上游解决了他们的问题会怎样?

有什么方法可以检测到这个问题并仅在我发现错误的编码时修复编码?

我也不确定上游编码是 ISO-8859-1。我认为上游是 perl,因此编码是有意义的,并且当我应用 ISO-8859-1 编码时,我尝试正确解码的每个样本。


当源向我的上游发送e4 9c 94 (✔) 时,我的上游向我发送c3 a2 c2 9c c2 94 (â)。

utf-8 字符串 作为字节:e4 9c 94 bytes e4 9c 94 as latin1 string: â utf-8 字符串 â 作为字节:c3 a2 c2 9c c2 94

我可以应用upstream.encode('ISO-8859-1').force_encoding('UTF-8') 修复它,但一旦上游问题得到修复,它就会中断。

【问题讨论】:

有趣的是,*** 在编辑器和预览中将 e4 9c 94 呈现为三个字符(â 和两个空方块),但在此页面上呈现为一个字符。 这个确实没有好的解决办法。你必须知道数据被破坏了,以及它是如何被破坏的,以便扭转它。我的意思是,您可以对 UTF-8 解码一次,如果生成的 Unicode 代码点 类似于 UTF-8 的位模式,则再次解码。但这是有风险的。我可能只是实现双重解码并将其放在一个可配置的标志和一个可配置的字符集内(如果重整并不总是使用 ISO-8859-1),当你知道上游已经修复然后转国旗关闭。 错字:e2 9c 94 0x80-0x9F 范围内的字符代码在 ISO 8859-1 中是不可打印的。有些字体将它们显示为空框,而另一些则显示为完全不可见的字符。它们没有明确定义的标准或规范的图形表示。 【参考方案1】:

既然你知道它是如何被破坏的,你可以尝试通过解码接收到的 UTF-8 字节,编码为 latin1,然后再次解码为 UTF-8 来解开它。只有您的错位字符串、纯 ASCII 字符串或不太可能的 latin-1 字符串组合才能成功解码两次。如果解码失败,假设上游是固定的,只解码一次为 UTF-8。纯 ASCII 字符串将使用任何一种方法正确解码,因此也没有问题。有有效的 UTF-8 编码序列可以在双重解码后幸存下来,但它们不太可能出现在普通文本中。

这是 Python 中的一个示例(您没有提到语言...):

# Assume bytes are latin1, but return encoded UTF-8.
def bad(b):
    return b.decode('latin1').encode('utf8')

# Assume bytes are UTF-8, and pass them along.
def good(b):
    return b

def decoder(b):
    try:
        return b.decode('utf8').encode('latin1').decode('utf8')
    except UnicodeError:
        return b.decode('utf8')

b = '✔'.encode('utf8')
print(decoder(bad(b)))
print(decoder(good(b)))

输出:

✔
✔

【讨论】:

我很惊讶你没有得到更多的支持。就我而言,我的脚本有时会输入 cp1252 编码为 UTF8 - 例如,Ø 变为 Ø。这个 sn-p 成为“maybe_fix_encoding”函数的基础。谢谢!【参考方案2】:

几乎可以保证裸 ISO 8859-1 是无效的 UTF-8。尝试解码为 ISO 8859-1,然后解码为 UTF-8,如果这会产生无效的字节序列,则回退到简单地解码为 UTF-8 应该适用于这种特定情况。

更详细地说,UTF-8 编码严格限制了允许哪些非 ASCII 字符序列。允许的模式在 ISO-8859-1 中是极不可能的,因为在这种编码中,它们表示像 Ã 这样的序列,后跟不可打印的控制字符或数学运算符,它们根本不会出现在任何有效的文本中。

【讨论】:

接下来你知道一些天才决定将他们的“web 3.0 人工智能众包区块链”公司称为 ÂÂ【参考方案3】:

基于 Mark Tolonen 的回答,再次在 Python 3 中:

    def maybe_fix_encoding(utf8_string, possible_codec="cp1252"):
        """Attempts to fix mangled text caused by interpreting UTF8 as cp1252
        (or other codec: https://docs.python.org/3/library/codecs.html)"""
        try:
            return utf8_string.encode(possible_codec).decode('utf8')
        except UnicodeError:
            return utf8_string
>>> maybe_fix_encoding("some normal text and some scandinavian characters æ ø å Æ Ø Å")
'some normal text and some scandinavian characters æ ø å Æ Ø Å'

【讨论】:

【参考方案4】:

根据turpachull 的回答和python3 list of standard encodings(&Mark Amery 的answer listing the set for various versions of python),这里有一个脚本,它将尝试在标准输入上进行每个编码转换,并输出每个版本(如果它与普通版本不同) utf_8.

#!/usr/bin/env python3

import sys
import fileinput

encodings = ["ascii", "big5hkscs", "cp1006", "cp1125", "cp1250", "cp1252", "cp1254", "cp1256", "cp1258", "cp273", "cp437", "cp720", "cp775", "cp852", "cp856", "cp858", "cp861", "cp863", "cp865", "cp869", "cp875", "cp949", "euc_jis_2004", "euc_kr", "gbk", "hz", "iso2022_jp_1", "iso2022_jp_2004", "iso2022_jp_ext", "iso8859_11", "iso8859_14", "iso8859_16", "iso8859_3", "iso8859_5", "iso8859_7", "iso8859_9", "koi8_r", "koi8_u", "latin_1", "mac_cyrillic", "mac_iceland", "mac_roman", "ptcp154", "shift_jis_2004", "utf_16_be", "utf_32", "utf_32_le", "utf_7", "utf_8_sig", "big5", "cp037", "cp1026", "cp1140", "cp1251", "cp1253", "cp1255", "cp1257", "cp424", "cp500", "cp737", "cp850", "cp855", "cp857", "cp860", "cp862", "cp864", "cp866", "cp874", "cp932", "cp950", "euc_jisx0213", "euc_jp", "gb18030", "gb2312", "iso2022_jp", "iso2022_jp_2", "iso2022_jp_3", "iso2022_kr", "iso8859_10", "iso8859_13", "iso8859_15", "iso8859_2", "iso8859_4", "iso8859_6", "iso8859_8", "johab", "koi8_t", "kz1048", "mac_greek", "mac_latin2", "mac_turkish", "shift_jis", "shift_jisx0213", "utf_16", "utf_16_le", "utf_32_be", "utf_8"]

def maybe_fix_encoding(utf8_string, possible_codec="utf_8"):
  try:
    return utf8_string.encode(possible_codec).decode('utf_8')
  except UnicodeError:
    return utf8_string

for line in sys.stdin:
  for e in encodings:
    i=line.rstrip('\n')
    result=maybe_fix_encoding(i, e)
    if result != i or e == 'utf_8':
      print("\t".join([e, result]))
  print("\n")

用法例如:

$ echo 'Requiem der morgenröte' | ~/decode_string.py
cp1252  Requiem der morgenröte
cp1254  Requiem der morgenröte
iso2022_jp_1    Requiem der morgenr(D**B"yte
iso2022_jp_2    Requiem der morgenr(D**B"yte
iso2022_jp_2004 Requiem der morgenr(Q):B"yte
iso2022_jp_3    Requiem der morgenr(O):B"yte
iso2022_jp_ext  Requiem der morgenr(D**B"yte
latin_1 Requiem der morgenröte
iso8859_9   Requiem der morgenröte
iso8859_14  Requiem der morgenröte
iso8859_15  Requiem der morgenröte
mac_iceland Requiem der morgenr̦te
mac_roman   Requiem der morgenr̦te
mac_turkish Requiem der morgenr̦te
utf_7   Requiem der morgenr+AMMAtg-te
utf_8   Requiem der morgenröte
utf_8_sig   Requiem der morgenröte

【讨论】:

以上是关于如何检测和修复不正确的字符编码的主要内容,如果未能解决你的问题,请参考以下文章

如何修复空格的 UTF 编码?

如何在mysql中存取utf8mb4编码的字符

Chilkat CkEmail 如何正确检测编码?

如何修复双编码 UTF8 字符(在 utf-8 表中)

如何知道std :: string是否正确编码?

如何对字符串进行URL编码