执行os.walk时出现UnicodeDecodeError

时间:2014-02-14 06:26:36

标签: python unicode encoding utf-8 utf-16

我收到错误:

'ascii' codec can't decode byte 0x8b in position 14: ordinal not in range(128)

试图做os.walk时。发生此错误是因为目录中的某些文件中包含0x8b(非utf8)字符。这些文件来自Windows系统(因此是utf-16文件名),但我已将文件复制到Linux系统,并使用python 2.7(在Linux中运行)遍历目录。

我已经尝试将一个unicode启动路径传递给os.walk,以及所有文件&它生成的dirs是unicode名称,直到它出现非utf8名称,然后由于某种原因,它不会将这些名称转换为unicode,然后代码会在utf-16名称上窒息。无论如何都要解决这个问题,而不是手动查找和更改所有令人反感的名字?

如果python2.7中没有解决方案,是否可以在python3中编写脚本来遍历文件树并通过将它们转换为utf-8来修复坏文件名(通过删除非utf8字符)?注:除了0x8b之外,名称中还有许多非utf8字符,因此它需要以一般方式工作。

更新:0x8b仍然只是一个btye char(只是无效的ascii)的事实使它更令人费解。我已经验证将这样的字符串转换为unicode存在问题,但是可以直接创建unicode版本。即:

>>> test = 'a string \x8b with non-ascii'
>>> test
'a string \x8b with non-ascii'
>>> unicode(test)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0x8b in position 9: ordinal not in  range(128)
>>> 
>>> test2 = u'a string \x8b with non-ascii'
>>> test2
u'a string \x8b with non-ascii'

这是我得到的错误的追溯:

80.         for root, dirs, files in os.walk(unicode(startpath)):
File "/usr/lib/python2.7/os.py" in walk
294.             for x in walk(new_path, topdown, onerror, followlinks):
File "/usr/lib/python2.7/os.py" in walk
294.             for x in walk(new_path, topdown, onerror, followlinks):
File "/usr/lib/python2.7/os.py" in walk
284.         if isdir(join(top, name)):
File "/usr/lib/python2.7/posixpath.py" in join
71.             path += '/' + b

Exception Type: UnicodeDecodeError at /admin/casebuilder/company/883/
Exception Value: 'ascii' codec can't decode byte 0x8b in position 14: ordinal not in range(128)

问题的根源发生在listdir返回的文件列表中(在os.walk的第276行):

names = listdir(top)

带字符的名字&gt; 128作为非unicode字符串返回。

6 个答案:

答案 0 :(得分:7)

对,我只是花了一些时间来整理这个错误,而这里的wordier答案没有解决潜在的问题:

问题是,如果你将一个unicode字符串传递给 os.walk(),那么os.walk开始从os.listdir()返回unicode并尝试将其保持为ASCII(因此'ascii'解码错误)。当它只击中一个unicode只有str()无法翻译的特殊字符时,它会抛出异常。

解决方案是强制您传递给os.walk的起始路径是常规字符串 - 即os.walk(str(somepath))。这意味着os.listdir返回类似于字节的常规字符串,一切都按照应有的方式工作。

你可以重现这个问题(并显示它的解决方案有效),如:

  1. 进入某个目录中的bash并运行touch $(echo -e "\x8b\x8bThis is a bad filename"),这将生成一些测试文件。

  2. 现在在同一目录中运行以下Python代码(iPython Qt非常方便):

    l = []
    for root,dir,filenames in os.walk(unicode('.')):
        l.extend([ os.path.join(root, f) for f in filenames ])
    print l
    
  3. 你会得到一个UnicodeDecodeError。

    1. 现在尝试运行:

      l = []
      for root,dir,filenames in os.walk('.'):
          l.extend([ os.path.join(root, f) for f in filenames ])
      print l
      
    2. 没有错误,你打印出来了!

      因此,Python 2.x中的安全方法是确保只将原始文本传递给os.walk()。你绝对不应该传递unicode或可能是unicode的东西,因为当内部ascii转换失败时os.walk将会阻塞。

答案 1 :(得分:6)

我可以重现os.listdir()行为:os.listdir(unicode_name)在Python 2.7上以字节形式返回不可解码的条目:

>>> import os
>>> os.listdir(u'.')
[u'abc', '<--\x8b-->']

注意:第二个名称是一个bytestring,尽管listdir()的参数是一个Unicode字符串。

  

然而,一个很大的问题仍然存在 - 如何在不诉诸此黑客的情况下解决这个问题?

Python 3通过surrogateescape错误处理程序(os.fsencode/os.fsdecode)解决文件名中不可解码的字节(使用文件系统的字符编码)字节。见PEP-383: Non-decodable Bytes in System Character Interfaces

>>> os.listdir(u'.')
['abc', '<--\udc8b-->']

注意:两个字符串都是Unicode(Python 3)。 surrogateescape错误处理程序用于第二个名称。要恢复原始字节:

>>> os.fsencode('<--\udc8b-->')
b'<--\x8b-->'

在Python 2中,在Windows(Unicode API),OS X(强制使用utf-8)上使用Unicode字符串作为文件名,并在Linux和其他系统上使用字节串。

答案 2 :(得分:5)

这个问题源于两个基本问题。第一个事实是Python 2.x默认编码是'ascii',而默认的Linux编码是'utf8'。您可以通过以下方式验证这些编码:

sys.getdefaultencoding() #python
sys.getfilesystemencoding() #OS

当os模块函数返回目录内容时,即os.walk&amp; os.listdir返回包含ascii only filesnames和non-ascii文件名的文件列表,ascii-encoding文件名自动转换为unicode。其他人不是。因此,结果是包含unicode和str对象混合的列表。 str对象可能导致问题。由于它们不是ascii,因此python无法知道要使用的编码,因此它们无法自动解码为unicode。

因此,当执行os.path(dir,file)等常用操作时,其中 dir 是unicode而文件是编码的str,如果 dil 是编码的str,则此调用将失败该文件不是ascii编码(默认)。解决方案是检索每个文件名,并使用适当的编码将str(编码的)对象解码为unicode。

这是第一个问题及其解决方案。第二个有点棘手。由于文件最初来自Windows系统,因此它们的文件名可能使用名为 windows-1252 的编码。一种简单的检查方法是致电:

filename.decode('windows-1252')

如果有效的unicode版本可能会产生正确的编码。您还可以通过在unicode版本上调用 print 进一步验证,并查看正确的文件名。

最后一个皱纹。在具有Windows源文件的Linux系统中,可能甚至可能混合使用 windows-1252 utf8 编码。处理这种混合物有两种方法。第一个也是最好的是运行:

$ convmv -f windows-1252 -t utf8 -r DIRECTORY --notest

其中DIRECTORY是包含需要转换的文件的。此命令会将任何windows-1252编码的文件名转换为utf8。它进行了智能转换,因为如果文件名已经是utf8(或ascii),它将什么都不做。

替代方案(如果出于某种原因无法进行此转换)是在python中动态执行类似的操作。即:

def decodeName(name):
    if type(name) == str: # leave unicode ones alone
        try:
            name = name.decode('utf8')
        except:
            name = name.decode('windows-1252')
    return name

该函数首先尝试utf8解码。如果失败,那么它将回退到windows-1252版本。在os调用返回文件列表后使用此函数:

root, dirs, files = os.walk(path):
    files = [decodeName(f) for f in files]
    # do something with the unicode filenames now

我个人发现unicode和编码的整个主题非常令人困惑,直到我读到这个精彩而简单的教程:

http://farmdev.com/talks/unicode/

我强烈建议那些努力解决unicode问题的人。

答案 3 :(得分:1)

\ x8不是有效的utf-8编码字符。 os.path期望文件名在utf-8中。如果要访问无效的文件名,则必须将os.path.walk传递给非unicode startpath;这样os模块就不会进行utf8解码。您必须自己完成并决定如何处理包含错误字符的文件名。

即:

for root, dirs, files in os.walk(startpath.encode('utf8')):

答案 4 :(得分:1)

在检查错误源之后,在C代码例程listdir中发生了一些事情,当它们不是标准的ascii时会返回非unicode文件名。因此唯一的解决方法是在os.walk中对目录列表进行强制解码,这需要替换os.walk。这个替换功能有效:

def asciisafewalk(top, topdown=True, onerror=None, followlinks=False):
    """
    duplicate of os.walk, except we do a forced decode after listdir
    """
    islink, join, isdir = os.path.islink, os.path.join, os.path.isdir

    try:
        # Note that listdir and error are globals in this module due
        # to earlier import-*.
        names = os.listdir(top)
        # force non-ascii text out
        names = [name.decode('utf8','ignore') for name in names]
    except os.error, err:
        if onerror is not None:
            onerror(err)
        return

    dirs, nondirs = [], []
    for name in names:
        if isdir(join(top, name)):
            dirs.append(name)
        else:
            nondirs.append(name)

    if topdown:
        yield top, dirs, nondirs
    for name in dirs:
        new_path = join(top, name)
        if followlinks or not islink(new_path):
            for x in asciisafewalk(new_path, topdown, onerror, followlinks):
                yield x
    if not topdown:
        yield top, dirs, nondirs

添加以下行:     names = [name.decode('utf8','ignore')名称中的名称] 所有的名字都是正确的ascii&amp; unicode,一切正常。

然而,一个很大的问题仍然存在 - 如何在不诉诸此黑客的情况下解决这个问题?

答案 5 :(得分:0)

在使用中文(unicode)名称的某些目录上使用os.walk时遇到此问题。我自己实现了walk函数,如下所示,它可以很好地使用unicode目录/文件名。

import os

ft = list(tuple())

def walk(dir, cur):
    fl = os.listdir(dir)
    for f in fl:
        full_path = os.path.join(dir,f)    
        if os.path.isdir(full_path):
            walk(full_path, cur)
        else:
            path, filename = full_path.rsplit('/',1)
            ft.append((path, filename, os.path.getsize(full_path)))