Python - 处理混合编码文件

19

我有一个文件,它主要是UTF-8编码的,但有一些Windows-1252字符也被包含在内。

我创建了一张表来映射Windows-1252(cp1252)字符到它们的Unicode对应项,并希望使用它来修复错误编码的字符,例如:

cp1252_to_unicode = {
    "\x85": u'\u2026', # …
    "\x91": u'\u2018', # ‘
    "\x92": u'\u2019', # ’
    "\x93": u'\u201c', # “
    "\x94": u'\u201d', # ”
    "\x97": u'\u2014'  # —
}

for l in open('file.txt'):
    for c, u in cp1252_to_unicode.items():
        l = l.replace(c, u)

尝试以这种方式进行替换会导致UnicodeDecodeError的抛出,例如:

"\x85".replace("\x85", u'\u2026')
UnicodeDecodeError: 'ascii' codec can't decode byte 0x85 in position 0: ordinal not in range(128)

有什么解决方法吗?

1
怀疑这不会解决你的问题,但是str.translate()比一堆替换更适合你尝试的操作。例如:cp1252_to_unicode = string.maketrans({...})然后l.translate(cp1252_to_unicode) - Gareth Latty
很难相信只有那些Windows标点字符最初是cp1252编码的...你知道这个混淆是如何发生的吗?你确定你的UTF8编码字符能够解码成有意义的Unicode吗?这段文本是用什么语言书写的? - John Machin
很不幸,我对文件最初是如何损坏的并没有太多信息。这些文件是用英语编写的,可能最初并不是以Unicode编码,而只是Ascii(99%的文本都是纯Ascii)。我猜测,使用Windows的某个人使用了编辑器插入了字符(em dash等),或者使用alt-快捷键。我手动查找了Unicode字符,因此如果将它们用作替换并且以Unicode读取文件,则应该可以正常工作。 - Keith Hughitt
5个回答

30

如果您尝试将此字符串解码为utf-8,就像您已经知道的那样,您将收到一个"UnicodeDecode"错误,因为这些虚假的cp1252字符是无效的utf-8 -

但是,Python编解码器允许您注册一个回调函数来处理编码/解码错误,使用codecs.register_error函数 - 它以UnicodeDecodeerror作为参数 - 您可以编写这样的处理程序,尝试将数据解码为"cp1252",并继续对其余字符串进行utf-8解码。

在我的utf-8终端中,我可以构建一个混合不正确的字符串,如下所示:

>>> a = u"maçã ".encode("utf-8") + u"maçã ".encode("cp1252")
>>> print a
maçã ma�� 
>>> a.decode("utf-8")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.6/encodings/utf_8.py", line 16, in decode
    return codecs.utf_8_decode(input, errors, True)
UnicodeDecodeError: 'utf8' codec can't decode bytes in position 9-11: invalid data

我在这里编写了所述的回调函数,并发现一个问题:即使您将要解码的字符串的位置增加1,以便它从下一个字符开始,如果下一个字符也不是utf-8且超出范围(128),则错误会在第一个超出范围(128)的字符处引发 - 这意味着,如果找到连续的非ascii、非utf-8字符,则解码会“回退”。解决这个问题的方法是在error_handler中添加一个状态变量,以检测这种“回退”,并从上次调用它时恢复解码 - 在这个简短的示例中,我将其实现为全局变量 - (每次调用解码器之前必须手动将其重置为“-1”):
import codecs

last_position = -1

def mixed_decoder(unicode_error):
    global last_position
    string = unicode_error[1]
    position = unicode_error.start
    if position <= last_position:
        position = last_position + 1
    last_position = position
    new_char = string[position].decode("cp1252")
    #new_char = u"_"
    return new_char, position + 1

codecs.register_error("mixed", mixed_decoder)

并在控制台上:

>>> a = u"maçã ".encode("utf-8") + u"maçã ".encode("cp1252")
>>> last_position = -1
>>> print a.decode("utf-8", "mixed")
maçã maçã 

我想知道你是否是第一个尝试编写这段代码的人?听起来像是个错误。很抱歉我不能为你的示例代码再点一次赞。 - Duncan
1
嗨 Keith - 抱歉我没有测试所有情况 - 元组中的项[2]不是我最初认为的错误起点。但我发现“unicode error”对象有一个“start”属性,这就是我期望的数字 - 现在试试吧。当然,它肯定还有改进的空间。 - jsbueno
太好了!看起来你可以通过使用这些参数来避免使用全局变量,例如:def cp1252_decoder(unicode_error): start = unicode_error.start end = unicode_error.end return unicode_error.object[start:end].decode("cp1252"), end或者你认为仍然有必要使用全局变量吗?感谢你的帮助。 - Keith Hughitt
我遇到了以下错误:TypeError: 'UnicodeDecodeError' object is not subscriptable。在代码中逐步调试,看起来是在 mixed_decoder 的第二行抛出的。有什么线索可以让它正常工作吗? - amadib
在许多情况下,当解析流时将Python的默认编码设置为utf-8可能是一个好主意。你需要导入sys模块才能使其生效:reload(sys).setdefaultencoding('utf-8') - Sprinterfreak
显示剩余6条评论

7
感谢jsbueno以及其他谷歌搜索结果和相关经验的帮助,我通过下列方法解决了这个问题。
#The following works very well but it does not allow for any attempts to FIX the data.
xmlText = unicode(xmlText, errors='replace').replace(u"\uFFFD", "?")

此版本允许有限的机会修复无效字符,未知字符将被替换为安全值。

import codecs    
replacement = {
   '85' : '...',           # u'\u2026' ... character.
   '96' : '-',             # u'\u2013' en-dash
   '97' : '-',             # u'\u2014' em-dash
   '91' : "'",             # u'\u2018' left single quote
   '92' : "'",             # u'\u2019' right single quote
   '93' : '"',             # u'\u201C' left double quote
   '94' : '"',             # u'\u201D' right double quote
   '95' : "*"              # u'\u2022' bullet
}

#This is is more complex but allows for the data to be fixed.
def mixed_decoder(unicodeError):
    errStr = unicodeError[1]
    errLen = unicodeError.end - unicodeError.start
    nextPosition = unicodeError.start + errLen
    errHex = errStr[unicodeError.start:unicodeError.end].encode('hex')
    if errHex in replacement:
        return u'%s' % replacement[errHex], nextPosition
    return u'%s' % errHex, nextPosition   # Comment this line out to get a question mark
    return u'?', nextPosition

codecs.register_error("mixed", mixed_decoder)

xmlText = xmlText.decode("utf-8", "mixed")

我尝试将其转换为UTF8格式。 对于任何失败的字符,我只需将其转换为十六进制,以便可以在自己的表格中显示或查找。

这不是很美观,但它确实使我能够理解混乱的数据。


1

今天刚遇到这个问题,以下是我的问题和解决方案:

original_string = 'Notifica\xe7\xe3o de Emiss\xe3o de Nota Fiscal Eletr\xf4nica.'

def mixed_decoding(s):
    output = ''
    ii = 0
    for c in s:
        if ii <= len(s)-1:
            if s[ii] == '\\' and s[ii+1] == 'x':
                b = s[ii:ii+4].encode('ascii').decode('unicode-escape')
                output = output+b
                ii += 3
            else:
                output = output+s[ii]
        ii += 1
    print(output)
    return output

decoded_string = mixed_decoding(original_string)

现在它会打印:
>>> 电子发票发行通知。

1

@jsbueno的解决方案很好,但是不需要全局变量last_position,请看:

def mixed_decoder(error: UnicodeError) -> (str, int):
     bs: bytes = error.object[error.start: error.end]
     return bs.decode("cp1252"), error.start + 1

import codecs
codecs.register_error("mixed", mixed_decoder)

a = "maçã".encode("utf-8") + "maçã".encode("cp1252")
# a = b"ma\xc3\xa7\xc3\xa3ma\xe7\xe3"

s = a.decode("utf-8", "mixed")
# s = "maçãmaçã"

1

这通常被称为乱码

有一个很好的Python库,可能会帮助您解决这些问题,叫做ftfy

示例:

>>> from ftfy import fix_text
>>> fix_text("Ð¨ÐµÐ¿Ð¾Ñ (напоминалки)")
'Шепот (напоминалки)'

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