Python处理中文字符时的编码错误

4 投票
3 回答
17832 浏览
提问于 2025-04-16 05:09

我是一名初学者,正在尝试用Python 2.7解码几十个包含数字和简体中文字符的CSV文件,想把它们转换成UTF-8格式。

我不知道这些输入文件的编码是什么,所以我尝试了我知道的所有可能的编码方式——包括GB18030、UTF-7、UTF-8、UTF-16和UTF-32(小端和大端)。为了保险起见,我还试了GBK和GB3212,虽然这两个应该是GB18030的子集。所有的UTF编码在遇到第一个中文字符时就停止了。其他编码在第一行的某个地方也会停止,只有GB18030能够继续。我原以为GB18030会是解决方案,因为它能顺利读取前几个文件并解码成功。我的代码的一部分是逐行读取的:

line = line.decode("GB18030")

我尝试解码的前两个文件都没问题。但在第三个文件的中间,Python却报错了:

UnicodeDecodeError: 'gb18030' codec can't decode bytes in position 168-169: illegal multibyte sequence

在这个文件里,大约有五个这样的错误,文件总共有大约一百万行。

我在文本编辑器中打开了输入文件,检查了哪些字符导致了解码错误,发现前面几个错误都是因为某一列的欧元符号。我很确定这些是输入错误,所以我想直接删除这些欧元符号。我想逐个检查编码错误,想先处理掉所有的欧元错误,但不想在没看其他错误之前就忽略它们。

编辑:我使用了chardet,它给出的编码是GB2312,置信度为0.99,适用于所有文件。我尝试用GB2312进行解码,结果是:

UnicodeDecodeError: 'gb2312' codec can't decode bytes in position 108-109: illegal multibyte sequence

3 个回答

0

试试这个:

codecs.open(file, encoding='gb18030', errors='replace')

别忘了参数 errors,你也可以把它设置为 'ignore'

0

你可以试试 chardet 这个工具。

9

""" ... GB18030。我以为这会是解决办法,因为它能顺利读取前几个文件并解码它们。""" -- 请解释一下你的意思。对我来说,成功解码有两个标准:第一,使用 raw_bytes.decode('some_encoding') 没有失败;第二,显示出来的结果在某种语言中是有意义的。宇宙中的每个文件在用 latin1iso_8859_1 解码时都会通过第一个测试。很多东亚语言的文件在用 gb18030 解码时也能通过第一个测试,因为大多数常用的中文、日文和韩文字符都是用相同的两字节序列编码的。你对第二个测试做了多少呢?

别在 IDE 或文本编辑器里瞎折腾数据。用网页浏览器查看它;浏览器通常能更好地检测编码。

你怎么知道这是个欧元字符?是通过什么编码在文本编辑器的屏幕上看的?cp1252?

你怎么知道它包含中文字符?你确定不是日文或韩文吗?你从哪里得到这个文件的?

在香港、台湾、澳门等地创建的中文文件通常使用 big5big5_hkscs 编码 -- 试试这个。

无论如何,听从 Mark 的建议,用 chardet 来检测一下;如果文件足够大且正确编码的中文/日文/韩文,chardet 通常能相对准确地检测出使用的编码。不过,如果有人在文本编辑器中手动编辑文件,使用了单字节字符集,几个非法字符可能会导致其他99.9%的字符的编码无法被检测到。

你可以对文件中的5行使用 print repr(line),并把输出结果编辑到你的问题中。

如果文件不涉及机密,你可以考虑提供下载链接。

这个文件是在 Windows 上创建的吗?你是怎么在 Python 中读取它的?(展示代码)

更新,基于 OP 的评论:

记事本等程序不会尝试猜测编码;“ANSI”是默认设置。你必须告诉它该怎么做。你所称的欧元字符是原始字节 "\x80" 被你的编辑器用你环境的默认编码解码的结果 -- 通常是“cp1252”。不要用这样的编辑器来编辑你的文件。

你之前提到的是“前几个错误”。现在你说总共有5个错误。请解释一下。

如果文件确实几乎是正确的 gb18030,你应该能够逐行解码文件,当遇到错误时,捕获它,打印错误信息,从信息中提取字节偏移量,打印 repr(two_bad_bytes),然后继续。我很想知道 \x80 出现在哪两个字节中。如果它根本没有出现,那么“欧元字符”就不是你的问题。请注意,\x80 在 gb18030 文件中可以有效出现,但只能作为以 \x81\xfe 开头的两字节序列的第二个字节。

在尝试修复问题之前,了解你的问题是什么是个好主意。用记事本等程序在“ANSI”模式下乱搞并不是个好主意。

你对 gb18030 解码结果是否合理的判断一直很含糊。特别是我会仔细检查 gbk 失败但 gb18030 “有效”的行 -- 里面肯定有一些极其罕见的汉字,或者一些非汉字的非 ASCII 字符...

这里有个更好的检查损坏的方法:用 raw_bytes.decode(encoding, 'replace') 解码每个文件,并将结果(用 utf8 编码)写入另一个文件。通过 result.count(u'\ufffd') 计算错误数量。用你用来判断 gb18030 解码是否合理的工具查看输出文件。U+FFFD 字符应该显示为一个黑色菱形中的白色问号。

如果你决定可以丢弃那些无法解码的部分,最简单的方法是 raw_bytes.decode(encoding, 'ignore')

更新,基于进一步的信息

那些 \\ 让人困惑。看起来“获取字节”涉及 repr(repr(bytes)) 而不是仅仅 repr(bytes) ... 在交互提示符下,执行 bytes(你会得到一个隐式的 repr()),或者 print repr(bytes)(这不会得到隐式的 repr())

空白:我假设你是说 '\xf8\xf8'.decode('gb18030') 是你认为的某种全宽空格,而这个解释是通过使用某种无法命名的查看软件进行视觉检查得出的。对吗?

实际上,'\xf8\xf8'.decode('gb18030') -> u'\e28b'。U+E28B 在 Unicode 私有使用区(PUA)中。所谓的“空白”可能意味着查看软件显然在它使用的字体中没有 U+E28B 的字形。

也许文件的来源故意使用 PUA 来表示不在标准 gb18030 中的字符,或者用于注释,或者传递伪秘密信息。如果是这样,你需要使用解码 tambourine,这是一项最近的俄罗斯研究的衍生物 在这里报道

另一种选择:cp939-HKSCS 理论。根据香港政府的说法,HKSCS big5 代码 FE57 曾经映射到 U+E28B,但现在映射到 U+28804。

关于“欧元”:你说过 """由于数据原因我不能分享整行,但我所称的欧元字符在: \xcb\xbe\x80\x80" [我假设在开头省略了一个 \,而 " 是字面意思]。当“欧元字符”出现时,它总是在我不需要的同一列,所以我希望能用“忽略”。不幸的是,由于“欧元字符”恰好在文件中的引号旁边,有时“忽略”会同时去掉欧元字符和引号,这给 csv 模块确定列带来了问题"""

如果你能展示这些 \x80 字节在引号和中文字符之间的出现模式,那将大有帮助 -- 只展示十六进制,隐藏你的机密数据,例如用 C1 C2 来表示“我确定代表一个汉字的两个字节”。例如:

C1 C2 C1 C2 cb be 80 80 22 # `\x22` is the quote character

请提供示例(1)在使用 'replace' 或 'ignore' 时引号没有丢失的情况(2)引号丢失的情况。在你迄今为止的唯一示例中,引号没有丢失:

>>> '\xcb\xbe\x80\x80\x22'.decode('gb18030', 'ignore')
u'\u53f8"'

而且提供一些调试代码的提议(见下面的示例输出)仍然有效。

>>> import decode_debug as de
>>> def logger(s):
...    sys.stderr.write('*** ' + s + '\n')
...
>>> import sys
>>> de.decode_debug('\xcb\xbe\x80\x80\x22', 'gb18030', 'replace', logger)
*** input[2:5] ('\x80\x80"') doesn't start with a plausible code sequence
*** input[3:5] ('\x80"') doesn't start with a plausible code sequence
u'\u53f8\ufffd\ufffd"'
>>> de.decode_debug('\xcb\xbe\x80\x80\x22', 'gb18030', 'ignore', logger)
*** input[2:5] ('\x80\x80"') doesn't start with a plausible code sequence
*** input[3:5] ('\x80"') doesn't start with a plausible code sequence
u'\u53f8"'
>>>

发现: -- 有可能导致有时丢失引号字符的原因 --

看起来 gb18030 解码器的替换/忽略机制存在一个bug:\x80 不是有效的 gb18030 起始字节;当检测到它时,解码器应该尝试与下一个字节重新同步。然而,它似乎忽略了 \x80 和下一个字节:

>>> '\x80abcd'.decode('gb18030', 'replace')
u'\ufffdbcd' # the 'a' is lost
>>> de.decode_debug('\x80abcd', 'gb18030', 'replace', logger)
*** input[0:4] ('\x80abc') doesn't start with a plausible code sequence
u'\ufffdabcd'
>>> '\x80\x80abcd'.decode('gb18030', 'replace')
u'\ufffdabcd' # the second '\x80' is lost
>>> de.decode_debug('\x80\x80abcd', 'gb18030', 'replace', logger)
*** input[0:4] ('\x80\x80ab') doesn't start with a plausible code sequence
*** input[1:5] ('\x80abc') doesn't start with a plausible code sequence
u'\ufffd\ufffdabcd'
>>>

撰写回答