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 возвращаются как строки без юникода.
Ответы
Ответ 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.
-
Теперь попробуйте запустить:
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)))