编码时出现UnicodeDecodeError错误?

3
我们遇到了一个问题(在http://wiki.python.org/moin/UnicodeDecodeError中有描述)--请阅读第二段‘...矛盾的是...’。
具体地说,我们试图将字符串转换为Unicode,并且我们收到了UnicodeDecodeError错误。
例如:
   >>> unicode('\xab')
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
   UnicodeDecodeError: 'ascii' codec can't decode byte 0xab in position 0: ordinal not in range(128)

当然,这是没有任何问题的。
   >>> unicode(u'\xab')
   u'\xab'

当然,这段代码是为了演示转换问题。在我们的实际代码中,我们不使用字符串字面量,也不能仅仅添加unicode 'u'前缀,而是要处理从os.walk()返回的字符串,并且文件名包含上述值。由于我们无法将该值强制转换为Unicode,因此不确定该如何继续进行。
一种非常可怕的hack方法是编写自己的str2uni()方法,类似于:
def str2uni(val):
    r"""brute force coersion of str -> unicode"""
    try:
        return unicode(src)
    except UnicodeDecodeError:
        pass
    res = u''
    for ch in val:
       res += unichr(ord(ch))
    return res

但在此之前,我们想知道是否有其他人有任何见解?

更新

我看到大家都集中在我是如何得出我发布的示例上,而不是结果。唉——好吧,这里是导致我花费数小时将问题简化为我上面分享的最简形式的代码。

for _,_,files in os.walk('/path/to/folder'):
    for fname in files:
        filename = unicode(fname)

当文件名的值为“3\xab Floppy (A).link”时,该代码段会抛出UnicodeDecodeError异常。

要查看错误,请执行以下操作:

   >>> unicode('3\xab Floppy (A).link')
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
   UnicodeDecodeError: 'ascii' codec can't decode byte 0xab in position 1: ordinal not in range(128)

更新

我非常感谢大家的帮助。我也很感激大多数人在字符串/Unicode处理方面犯了一些简单的错误。但我想强调一下对UnicodeDecodeError异常的引用。我们在调用unicode()构造函数时遇到了这个异常!

我认为根本原因在于上述维基文章中描述的内容http://wiki.python.org/moin/UnicodeDecodeError。请从第二段开始阅读,关于"具有讽刺意味的是,在编码时可能会发生UnicodeDecodeError..."。维基文章非常准确地描述了我们所经历的问题——虽然它详细阐述了原因,但没有提出解决方法。

事实上,第三段以以下令人震惊的承认开始:"与UnicodeEncodeError类似的情况不同的是,这种失败并不能总是避免..."

由于作为开发者,我不习惯于“无法找到解决方法”的信息,因此我想在Stack Overflow上寻求他人的经验。


你能否添加代码部分,当你从os.walk()读取值并出现异常时?还有哪个字符让你遇到问题,这样我们就可以知道编码方式了吗?或者是\xab吗? - Paulo Bu
你是如何调用 os.walk() 的?我认为你混淆了 Unicode 和 UTF-8(以及其他编码)... - Tim Pietzcker
我已经更新了问题,包括对os.walk()的具体调用。至于哪个字符生成了异常——它是\xab。 - user590028
换句话说,你能否将文件名(从资源管理器中)复制/粘贴到这里? - Paulo Bu
让我们在聊天中继续这个讨论:http://chat.stackoverflow.com/rooms/31196/discussion-between-user590028-and-paulo-bu - user590028
显示剩余6条评论
4个回答

5
我认为你混淆了Unicode字符串和Unicode编码(如UTF-8)。 os.walk(".")返回的文件名(及目录名等)是使用当前代码页进行编码的字符串。它会默默地删除在当前代码页中不存在的字符(请参见这个问题的惊人例子)。
因此,如果您的文件/目录名包含超出您的编码范围的字符,则一定需要使用Unicode字符串来指定起始目录,例如通过调用os.walk(u".")。然后,您就不需要(也不应该)再对结果调用unicode(),因为它们已经是Unicode字符串。
如果您没有这样做,您首先需要解码文件名(例如mystring.decode("cp850")),这将为您提供一个Unicode字符串:
>>> "\xab".decode("cp850")
u'\xbd'

然后您可以将其编码为UTF-8或任何其他编码。

>>> _.encode("utf-8")
'\xc2\xbd'

如果你仍然不理解为什么unicode("\xab")会抛出一个解码错误,也许以下的解释可以帮到你: "\xab"是一个编码字符串。Python不知道它使用了哪种编码,但在将其转换为Unicode之前,需要先进行解码。如果没有任何指定,unicode()会假定该字符是以ASCII编码的,当它在这个假设下尝试解码时,会失败,因为\xab不是ASCII中的一部分。所以你需要找出文件系统正在使用的编码方式并调用unicode("\xab", encoding="cp850")(或其他编码方式),或者最好从一开始就使用Unicode字符串。

这个相关问题是一个有趣的阅读(关于如何将Unicode传递给os.walk()导致Unicode结果列表)--但不幸的是--它没有解决我们的问题--它只是将错误移动到了python2.7/posixpath.py模块内部。具体来说,它在join的第71行报告了错误路径+= '/' + b。执行抛出的异常再次是UnicodeDecodeError(请注意--这是一个DECODE错误)。我再次相信这与我链接的维基文章有关。 - user590028
所以你调用了 os.walk(u'/path/to/folder'),并且删除了对 unicode() 的调用?那么在你的代码中,哪一行触发了新错误? - Tim Pietzcker
我被要求详细说明我如何从os.walk()中获得这个结果,并且代码进行了演示。演示的代码就是我们最初遇到UnicodeDecodeError异常的原因。 - user590028
在 POSIX 上,不应从文件名中删除任何字节(如果在 Windows 上这样做,请使用 Unicode 名称(但在 cmd.exe 中可能只是显示问题)。 - jfs

4
for fname in files:
    filename = unicode(fname)

第二行代码会在fname不是ASCII时报错。如果你想将字符串转换为Unicode,而不是使用unicode(fname),你应该使用fname.decode('<此处输入编码>')
我建议告诉你使用的编码方式,但你没有说明.link文件中的\xab代表什么。无论如何,你可以在谷歌上搜索相关编码方式,代码保持不变:
for fname in files:
    filename = fname.decode('<encoding>')

更新: 例如,如果您的文件系统名称的编码为ISO-8859-1,那么字符\xa9将是“<<” 。要在python中读取它,您应该执行:

for fname in files:
    filename = fname.decode('latin1') #which is synonym to #ISO-8859-1

希望这有所帮助!

我们最终使用了上述的str2uni()方法(并进行了编辑以匹配我们的最终版本)...但与Paulo Bu的讨论非常有见地,因此把胜利拱手让人给了他。非常感谢! - user590028
哈哈,胜利 其实就是解决问题 :) 很高兴你成功了。 - Paulo Bu
@user590028:接受的答案应该解决你所看到的问题(但并不是;可能存在无法使用文件系统编码解码的字节)。如果你想奖励Paulo Bu的努力,可以发起一个悬赏。 - jfs

3
据我了解,您的问题是os.walk(unicode_path)无法将一些文件名解码为Unicode。这个问题在Python 3.1+中已经修复(参见PEP 383:系统字符接口中的不可解码字节):

POSIX中将文件名、环境变量和命令行参数定义为字符数据;但C API允许传递任意字节——无论这些字节是否符合某种编码。本PEP提出了一种处理这种不规则性的方法,即通过以一种方式将字节嵌入字符字符串中,从而允许重新创建原始字节串。

Windows提供了Unicode API来访问文件系统,因此不应该有这个问题。

Python 2.7(在Linux上使用utf-8文件系统):

>>> import os
>>> list(os.walk("."))
[('.', [], ['\xc3('])]
>>> list(os.walk(u"."))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/os.py", line 284, in walk
    if isdir(join(top, name)):
  File "/usr/lib/python2.7/posixpath.py", line 71, in join
    path += '/' + b
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 1: \
    ordinal not in range(128)

Python 3.3:

>>> import os
>>> list(os.walk(b'.'))
[(b'.', [], [b'\xc3('])]
>>> list(os.walk(u'.'))
[('.', [], ['\udcc3('])]

您的str2uni()函数试图(它引入了模糊的名称)解决与Python 3中的“surrogateescape”错误处理程序相同的问题。如果您希望使用sys.getfilesystemencoding()无法解码的文件名,请在Python 2中使用bytestrings。


2
'\xab'

一个字节,数字为171。

u'\xab'

一个字符是指U+00AB左双角引号(«)。 u'\xab'是一种简写,表示u'\u00ab'。它与字节'\xab'不同(甚至不是相同的数据类型)。在Unicode字符串文字中始终使用\u语法可能会更清晰易懂,但现在已经太晚了,无法更改。
从字节到字符的转换称为解码操作。从字符到字节的转换称为编码操作。对于任何一个方向,您都需要知道用于映射两者之间的编码方式。
>>> unicode('\xab')
UnicodeDecodeError

unicode是一个字符串,因此当你将字节传递给unicode()构造函数时,会有一个隐式的解码操作。如果你不告诉它你想要哪种编码,你会得到默认编码,通常是ascii。ASCII对于字节171没有意义,所以你会得到一个错误。

>>> unicode(u'\xab')
u'\xab'

由于u'\xab'(或u'\u00ab')已经是一个字符串,因此将其传递给unicode()构造函数时不会发生隐式转换-您会得到一个未更改的副本。

res = u''
for ch in val:
   res += unichr(ord(ch))
return res

将每个输入字节映射到具有相同序数值的Unicode字符的编码是ISO-8859-1。因此,您可以仅使用以下内容替换此循环:

return unicode(val, 'iso-8859-1')

请注意,如果涉及到Windows,则您需要的编码可能不是这个,而是相似的windows-1252编码。

一种非常可怕的方法是编写自己的str2uni()方法。

一般来说,这不是一个好主意。 UnicodeError是Python告诉您有关字符串类型误解的信息;忽略该错误而不是在源代码中进行修复意味着您更有可能隐藏细微的失败,这些失败将在以后咬你一口。

filename = unicode(fname)

如果您知道您的文件系统正在使用ISO-8859-1文件名,则最好使用以下代码替换:filename = unicode(fname, 'iso-8859-1')。如果您的系统区域设置正确,则应该可以找出文件系统正在使用的编码,并直接使用该编码:

filename = unicode(fname, sys.getfilesystemencoding())

实际上,如果设置正确,您可以通过要求Python将文件系统路径视为本机Unicode而不是字节字符串来跳过所有编码/解码麻烦。通过将Unicode字符串传递到os文件名接口中实现:

for _,_,files in os.walk(u'/path/to/folder'): # note u'' string
    for fname in files:
        filename = fname  # nothing more to do!

提示:在“3英寸软盘”中的字符应该是U+2033双引号,但ISO-8859-1中没有这个编码。长期来看最好使用UTF-8文件系统编码,这样就可以包含任何字符。


utf-8 不足以解决问题,文件名中可能存在无法解码的字节 - jfs
确实 - 在更长期的使用中,人们希望使用Python 3,在那里“surrogateescape”可以解决这个问题。 - bobince

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