python - open - UnicodeDecodeError al realizar os.walk
python unicode to utf8 (6)
Estoy recibiendo el error:
''ascii'' codec can''t decode byte 0x8b in position 14: ordinal not in range(128)
al intentar hacer os.walk. El error se produce porque algunos de los archivos de un directorio tienen el carácter 0x8b (non-utf8) en ellos. Los archivos provienen de un sistema Windows (de ahí los nombres de archivo de utf-16), pero he copiado los archivos a un sistema Linux y estoy usando python 2.7 (ejecutándose en Linux) para atravesar los directorios.
He intentado pasar una ruta de inicio de Unicode a os.walk, y todos los archivos y dirs que genera son nombres de Unicode hasta que se trata de un nombre que no es utf8, y por alguna razón, no convierte esos nombres a Unicode y entonces el código se ahoga en los nombres utf-16. ¿Hay alguna forma de resolver el problema antes de encontrar y cambiar manualmente todos los nombres ofensivos?
Si no hay una solución en python2.7, ¿se puede escribir un script en python3 para atravesar el árbol de archivos y corregir los nombres de archivo incorrectos convirtiéndolos a utf-8 (eliminando los caracteres que no son utf8)? NB: hay muchos caracteres que no son utf8 en los nombres, además de 0x8b, por lo que debería funcionar de manera general.
ACTUALIZACIÓN: el hecho de que 0x8b todavía sea solo un btye char (simplemente no es válido ascii) lo hace aún más desconcertante. He verificado que hay un problema al convertir una cadena de este tipo a Unicode, pero que se puede crear una versión de Unicode directamente. Esto es:
>>> 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''
Aquí hay un seguimiento del error que estoy recibiendo:
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)
La raíz del problema se produce en la lista de archivos devueltos desde listdir (en la línea 276 de os.walk):
names = listdir(top)
Los nombres con caracteres> 128 se devuelven como cadenas no Unicode.
Correcto, acabo de pasar un tiempo clasificando este error, y las respuestas más breves aquí no están abordando el problema subyacente:
El problema es que si pasa una cadena Unicode a os.walk (), entonces os.walk comienza a recuperar Unicode de os.listdir () y trata de mantenerlo como ASCII (por lo tanto, el error de decodificación ''ascii''). Cuando llega a un código único, solo un carácter especial que str () no puede traducir, lanza la excepción.
La solución es forzar que la ruta de inicio que pase a os.walk sea una cadena regular, es decir, os.walk (str (somepath)). Esto significa que os.listdir devuelve cadenas regulares de tipo byte y todo funciona como debería.
Puede reproducir este problema (y mostrar que su solución funciona) de manera trivial como:
Vaya a bash en algún directorio y ejecute
touch $(echo -e "/x8b/x8bThis is a bad filename")
que creará algunos archivos de prueba.Ahora ejecute el siguiente código de Python (iPython Qt es útil para esto) en el mismo directorio:
l = [] for root,dir,filenames in os.walk(unicode(''.'')): l.extend([ os.path.join(root, f) for f in filenames ]) print l
Y obtendrás un UnicodeDecodeError.
Ahora intenta correr:
l = [] for root,dir,filenames in os.walk(''.''): l.extend([ os.path.join(root, f) for f in filenames ]) print l
No hay error y se imprime!
Por lo tanto, la forma segura en Python 2.x es asegurarse de que solo pase el texto sin formato a os.walk (). Absolutamente no debe pasar unicode o cosas que puedan ser unicode, porque os.walk se ahogará cuando falle una conversión interna de ascii.
Después de examinar la fuente del error, algo sucede dentro de la rutina de código C listdir que devuelve nombres de archivo no Unicode cuando no son ascii estándar. Por lo tanto, la única solución es realizar una decodificación forzada de la lista de directorios dentro de os.walk, que requiere un reemplazo de os.walk. Esta función de reemplazo funciona:
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
Al agregar la línea: nombres = [nombre.decódigo (''utf8'', ''ignorar'') para nombre en nombres] todos los nombres son ascii y unicode, y todo funciona correctamente.
Sin embargo, queda una gran pregunta: ¿cómo se puede resolver esto sin recurrir a este truco?
Este problema se deriva de dos problemas fundamentales. El primero es el hecho de que la codificación predeterminada de Python 2.x es ''ascii'', mientras que la codificación predeterminada de Linux es ''utf8''. Puedes verificar estas codificaciones a través de:
sys.getdefaultencoding() #python
sys.getfilesystemencoding() #OS
Cuando las funciones del módulo os devuelven el contenido del directorio, es decir, os.walk & os.listdir devuelven una lista de archivos que contienen solo nombres de archivo ascii y nombres de archivo no ascii, los nombres de archivo de codificación ascii se convierten automáticamente a Unicode. Los otros no lo son. Por lo tanto, el resultado es una lista que contiene una mezcla de objetos unicode y str. Son los objetos str los que pueden causar problemas en la línea. Como no son ascii, Python no tiene forma de saber qué codificación usar, y por lo tanto no pueden ser descodificados automáticamente en Unicode.
Por lo tanto, al realizar operaciones comunes como os.path (dir, archivo), donde dir es unicode y el archivo es una cadena codificada, esta llamada fallará si el archivo no está codificado en ascii (el valor predeterminado). La solución es verificar cada nombre de archivo tan pronto como se recuperan y decodificar los objetos str (codificados) a Unicode usando la codificación apropiada.
Ese es el primer problema y su solución. El segundo es un poco más complicado. Dado que los archivos provinieron originalmente de un sistema Windows, es probable que sus nombres de archivo utilicen una codificación llamada windows-1252 . Un medio fácil de verificar es llamar a:
filename.decode(''windows-1252'')
Si resulta una versión válida de Unicode, probablemente tenga la codificación correcta. También puede verificarlo llamando a print en la versión Unicode y ver el nombre de archivo correcto representado.
Una última arruga. En un sistema Linux con archivos de origen de Windows, es posible o incluso probable tener una combinación de codificaciones de windows-1252 y utf8 . Hay dos formas de lidiar con esta mezcla. Lo primero y preferible es ejecutar:
$ convmv -f windows-1252 -t utf8 -r DIRECTORY --notest
donde DIRECTORIO es el que contiene los archivos que necesitan conversión. Este comando convertirá cualquier nombre de archivo codificado de windows-1252 a utf8. Hace una conversión inteligente, ya que si un nombre de archivo ya es utf8 (o ascii), no hará nada.
La alternativa (si uno no puede hacer esta conversión por alguna razón) es hacer algo similar sobre la marcha en python. Esto es:
def decodeName(name):
if type(name) == str: # leave unicode ones alone
try:
name = name.decode(''utf8'')
except:
name = name.decode(''windows-1252'')
return name
La función intenta primero una decodificación utf8. Si falla, entonces vuelve a la versión de Windows-1252. Utilice esta función después de una llamada al sistema operativo que devuelve una lista de archivos:
root, dirs, files = os.walk(path):
files = [decodeName(f) for f in files]
# do something with the unicode filenames now
Personalmente, encontré todo el tema de Unicode y la codificación muy confuso, hasta que leí este tutorial maravilloso y simple:
http://farmdev.com/talks/unicode/
Lo recomiendo altamente a cualquiera que esté luchando con problemas de Unicode.
Puedo reproducir el comportamiento os.listdir()
: os.listdir(unicode_name)
devuelve entradas no codificables como bytes en Python 2.7:
>>> import os
>>> os.listdir(u''.'')
[u''abc'', ''<--/x8b-->'']
Aviso: el segundo nombre es una serie a pesar de que el listdir()
es una cadena Unicode.
Sin embargo, queda una gran pregunta: ¿cómo se puede resolver esto sin recurrir a este truco?
Python 3 resuelve bytes no codificables (utilizando la codificación de caracteres del sistema de archivos) en nombres de archivo a través del manejador de errores surrogateescape
( os.fsencode/os.fsdecode
). Consulte PEP-383: Bytes no decodificables en las interfaces de caracteres del sistema :
>>> os.listdir(u''.'')
[''abc'', ''<--/udc8b-->'']
Aviso: ambas cadenas son Unicode (Python 3). Y el controlador de errores surrogateescape
se utilizó para el segundo nombre. Para recuperar los bytes originales:
>>> os.fsencode(''<--/udc8b-->'')
b''<--/x8b-->''
En Python 2, use cadenas Unicode para los nombres de archivo en Windows (API Unicode), OS X (se aplica utf-8) y use secuencias de bytes en Linux y otros sistemas.
Tengo este problema cuando uso os.walk
en algunos directorios con nombres chinos (Unicode). Yo mismo implementé la función de caminar de la siguiente manera, que funcionó bien con los nombres de dir / file de 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)))
/ x8 no es un caracter de codificacion utf-8 valido os.path espera que los nombres de archivo estén en utf-8. Si desea acceder a los nombres de archivo no válidos, debe pasar os.path.walk por la ruta de acceso no unicode; De esta manera, el módulo os no hará la decodificación utf8. Tendría que hacerlo usted mismo y decidir qué hacer con los nombres de archivo que contienen caracteres incorrectos.
Es decir:
for root, dirs, files in os.walk(startpath.encode(''utf8'')):