Подтвердить что ты не робот

UnicodeDecodeError при выполнении os.walk

Я получаю сообщение об ошибке:

'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, и все файлы и файлы, которые он генерирует, являются именами unicode, пока они не попадут в имя не-utf8, а затем по какой-то причине не преобразуют эти имена в unicode, а затем код зажимает имена utf-16. Есть ли все-таки решение проблемы, которая не позволяет вручную найти и изменить все оскорбительные имена?

Если в python2.7 нет решения, можно ли написать script в 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 (строка 276 os.walk):

names = listdir(top)

Имена с символами > 128 возвращаются как строки без юникода.

4b9b3361

Ответ 1

Эта проблема связана с двумя фундаментальными проблемами. Во-первых, факт, что кодировка по умолчанию Python 2.x - "ascii", а стандартная Linux-кодировка - "utf8". Вы можете проверить эти кодировки с помощью:

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

Когда функции модуля os возвращают содержимое каталога, а именно os.walk и os.listdir возвращают список файлов, содержащих только имена файлов ascii и имена файлов, отличных от ascii, имена файлов ascii-encoding автоматически преобразуются в unicode. Другие - нет. Следовательно, результатом является список, содержащий комбинацию объектов unicode и str. Это объекты str могут вызывать проблемы на линии. Поскольку они не являются ascii, python не может знать, какую кодировку использовать, и поэтому они не могут быть автоматически декодированы в unicode.

Следовательно, при выполнении общих операций, таких как os.path(dir, file), где dir является unicode, а файл является закодированной строкой, этот вызов завершится неудачно, если файл не имеет ascii-кодировку (по умолчанию). Решение состоит в том, чтобы проверить каждое имя файла, как только они будут извлечены, и декодировать объекты str (закодированные) в unicode с использованием соответствующей кодировки.

Это первая проблема и ее решение. Второй немного сложнее. Поскольку файлы изначально поступали из системы Windows, их имена файлов, вероятно, используют кодировку под названием windows-1252. Легким средством проверки является вызов:

filename.decode('windows-1252')

Если действительная версия юникода приводит к тому, что вы, вероятно, имеете правильную кодировку. Вы также можете проверить, вызывая печать в версии юникода, и посмотрите правильное имя файла.

Последняя морщина. В системе Linux с файлами происхождения Windows возможно или даже возможно иметь сочетание кодировок 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.

Ответ 2

Я могу воспроизвести поведение os.listdir(): os.listdir(unicode_name) возвращает недоказуемые записи в виде байтов на Python 2.7:

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

Обратите внимание: второе имя является байтовой, несмотря на то, что аргумент listdir() является строкой Unicode.

Однако остается большой вопрос - как это можно решить, не прибегая к этому взлому?

Python 3 разрешает недоказуемые байты (используя кодировку символов файловой системы) байтов в именах файлов с помощью обработчика ошибок surrogateescape (os.fsencode/os.fsdecode). См. PEP-383: Неразборные байты в интерфейсах системных символов:

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

Примечание: обе строки - Unicode (Python 3). А для второго имени использовался обработчик ошибок surrogateescape. Чтобы вернуть исходные байты:

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

В Python 2 используйте строки Unicode для имен файлов в Windows (Unicode API), OS X (применяется utf-8) и используйте bytestrings в Linux и других системах.

Ответ 3

Правильно, я просто потратил некоторое время на сортировку этой ошибки, и ответы на слово ниже не затрагивают основной проблемы:

Проблема заключается в том, что если вы передаете строку unicode в os.walk(), то os.walk начинает получать unicode обратно из os.listdir() и пытается сохранить его как ASCII (следовательно, ошибка декодирования ascii). Когда он попадает в unicode только специальный символ, который str() не может перевести, он выдает исключение.

решение заключается в том, чтобы заставить исходный путь, к которому вы передаете os.walk, быть обычной строкой, то есть os.walk(str (somepath)). Это означает, что os.listdir возвращает регулярные байтовые строки, и все работает так, как должно быть.

Вы можете воспроизвести эту проблему (и показать ее решения) тривиально:

  • Перейдите в bash в какой-то каталог и запустите touch $(echo -e "\x8b\x8bThis is a bad filename"), который сделает несколько тестовых файлов.

  • Теперь запустите следующий код 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
    

И вы получите UnicodeDecodeError.

  1. Теперь попробуйте запустить:

    l = []
    for root,dir,filenames in os.walk('.'):
        l.extend([ os.path.join(root, f) for f in filenames ])
    print l
    

Нет ошибок, и вы получите распечатку!

Таким образом, безопасный способ в Python 2.x состоит в том, чтобы убедиться, что вы только передаете исходный текст в os.walk(). Вы абсолютно не должны пропускать unicode или вещи, которые могут быть unicode для него, потому что os.walk затем задохнется, когда внутреннее преобразование ascii завершится с ошибкой.

Ответ 4

\ x8 не является допустимым символом кодирования utf-8. os.path ожидает, что имена файлов будут в utf-8. Если вы хотите получить доступ к недопустимым именам файлов, вам необходимо передать os.path.walk стартовую строку, отличную от unicode; таким образом, модуль os не будет выполнять декодирование utf8. Вам нужно будет сделать это самостоятельно и решить, что делать с именами файлов, которые содержат неправильные символы.

то есть:.

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

Ответ 5

После изучения источника ошибки, что-то происходит в подпрограмме listdir C-кода, которая возвращает имена файлов, отличных от unicode, когда они не являются стандартными ascii. Единственное исправление заключается в том, чтобы сделать принудительное декодирование списка каталогов в 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 и unicode, и все работает правильно.

Однако остается большой вопрос - как это можно решить, не прибегая к этому взлому?

Ответ 6

У меня возникла проблема при использовании os.walk в некоторых каталогах с китайскими (юникодными) именами. Я реализовал функцию walk непосредственно следующим образом, которая отлично работала с именами dir/file в 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)))