Шаг 93.
Python: сборник рецептов.
Файлы и ввод-вывод. Вывод "плохих" имен файлов

    На этом шаге мы рассмотрим способы решения этой задачи.

Задача

    Ваша программа получила список содержимого каталога, но когда она попыталась вывести эти имена файлов, то упала с исключением UnicodeEncodeError и загадочным сообщением "surrogates not allowed".

Решение

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

def bad_filename(filename): 
    return repr(filename)[1:-1]

try:
    print(filename) 
except UnicodeEncodeError:
    print(bad_filename(filename))


Обсуждение

    Этот рецепт решает редкую, но очень раздражающую проблему, которая касается программ, работающих с файловой системой. По умолчанию Python предполагает, что все имена файлов закодированы согласно установке, которая возвращается функцией sys.getfilesystemencoding(). Однако некоторые файловые системы не заставляют соблюдать это ограничение, позволяя создавать файлы с неправильной кодировкой. Это не частый случай, но все же есть опасность, что некий пользователь сделает что-то глупое и случайно создаст такой файл (например, передаст неправильное имя файла функции open() в какой-то забагованной программе).

    При выполнении команд типа os.listdir() неправильные имена файлов загоняют Python в безвыходную ситуацию. С одной стороны, он не может просто отбросить неправильное имя. С другой стороны, он не может превратить имя файла в правильную текстовую строку. Python действует так: берет недекодируемое байтовое значение \xhh в имени файла и отображает его в так называемую "суррогатную кодировку", представленную символом Unicode \udchh. Вот пример того, как неправильный список содержимого каталога может выглядеть, если он содержит имя файла bäd.txt, закодированное в Latin-1 вместо UTF-8:

>>> import os
>>> files = os.listdir('.')
>>> files
['spam.py', 'b\udce4d.txt', 'foo.txt']
>>>

    Если у вас есть код, который манипулирует именами файлов или даже передает их функциям (таким как open()), все работает нормально. Вы попадете в неприятности только в ситуациях, где вы хотите вывести имя файла (вывод, логирование и т. п.). Ваша программа упадет, если вы захотите вывести показанный выше список:

>>> for name in files:
	print(name)

spam.py
Traceback (most recent call last):
 .   .   .   .
UnicodeEncodeError: 'utf-8' codec can't encode character '\udce4' in
position 1: surrogates not allowed
>>>

    Причина падения в том, что символ \udce4 не является валидным в Unicode. Это вторая половина двухсимвольной комбинации, известной как "суррогатная пара". Поскольку первая часть отсутствует, это не валидный Unicode. Поэтому единственный способ успешно произвести вывод - предпринять корректирующее действие, если встретится неправильное имя файла. Например:

>>> for name in files:
	try:
		print(name)
	except UnicodeEncodeError:
		print(bad_filename(name))

		
spam.py
b\udce4d.txt
foo.txt
>>>

    Выбор того, что будет делать функция bad_filename(), во многом зависит от вас. Другая возможность - как-то перекодировать значение:

def bad_filename(filename):
    temp = filename.encode(sys.getfilesystemencoding(), errors='surrogateescape')
    return temp.decode('latin-1')

    При использовании этой версии вы получите следующее:

>>> for name in files:
	try:
		print(name)
	except UnicodeEncodeError:
		print(bad_filename(name))

		
spam.py
bad.txt
foo.txt
>>>

    Этот рецепт наверняка будет проигнорирован большинством. Однако если вы пишете критически важные скрипты, которым нужно надежно работать с именами файлов и файловой системой, об этом стоит подумать. В противном случае вы можете столкнуться с ситуацией, когда вам придется сражаться с какой-то непонятной ошибкой.

    На следующем шаге мы рассмотрим добавление или изменение кодировки уже открытого файла.




Предыдущий шаг Содержание Следующий шаг