Python – tratar con archivos de encoding mixta

Tengo un archivo que es en su mayoría UTF-8, pero algunos caracteres de Windows-1252 también se han introducido.

Creé una tabla para asignar desde los caracteres de Windows-1252 (cp1252) a sus contrapartes de Unicode, y me gustaría usarla para corregir los caracteres mal codificados, por ejemplo

cp1252_to_unicode = { "\x85": u'\u2026', # … "\x91": u'\u2018', # ' "\x92": u'\u2019', # ' "\x93": u'\u201c', # “ "\x94": u'\u201d', # ” "\x97": u'\u2014' # — } for l in open('file.txt'): for c, u in cp1252_to_unicode.items(): l = l.replace(c, u) 

Pero al intentar realizar la sustitución de esta manera, se genera un error UnicodeDecodeError, por ejemplo:

 "\x85".replace("\x85", u'\u2026') UnicodeDecodeError: 'ascii' codec can't decode byte 0x85 in position 0: ordinal not in range(128) 

¿Alguna idea de cómo lidiar con esto?

Si intenta decodificar esta cadena como utf-8, como ya sabe, obtendrá un error “UnicodeDecode”, ya que estos caracteres espurios cp1252 no son válidos utf-8 –

Sin embargo, los codecs de Python le permiten registrar una callback para manejar los errores de encoding / deencoding , con la función codecs.register_error – obtiene el parámetro UnicodeDecodeerror aa – puede escribir un controlador que intente descifrar los datos como “cp1252”, y continúa la deencoding en utf-8 para el rest de la cadena.

En mi terminal utf-8, puedo construir una cadena incorrecta mixta como esta:

 >>> a = u"maçã ".encode("utf-8") + u"maçã ".encode("cp1252") >>> print a maçã ma   >>> a.decode("utf-8") Traceback (most recent call last): File "", line 1, in  File "/usr/lib/python2.6/encodings/utf_8.py", line 16, in decode return codecs.utf_8_decode(input, errors, True) UnicodeDecodeError: 'utf8' codec can't decode bytes in position 9-11: invalid data 

Escribí la mencionada función de callback aquí y encontré una captura: incluso si incrementas la posición desde la que decodificas la cadena en 1, para que comience en el siguiente chratcer, si el siguiente carácter tampoco es utf-8 y sale del rango (128), el error se genera en el primer carácter fuera del rango (128), lo que significa que la deencoding “retrocede” si se encuentran caracteres consecutivos no ascii, no utf-8.

El objective de esto es tener una variable de estado en el error_handler que detecte este “retroceso” y reanudar la desencoding desde la última llamada a ella. En este breve ejemplo, lo implementé como una variable global (tendrá que ser manual). reajuste a “-1” antes de cada llamada al decodificador):

 import codecs last_position = -1 def mixed_decoder(unicode_error): global last_position string = unicode_error[1] position = unicode_error.start if position <= last_position: position = last_position + 1 last_position = position new_char = string[position].decode("cp1252") #new_char = u"_" return new_char, position + 1 codecs.register_error("mixed", mixed_decoder) 

Y en la consola:

 >>> a = u"maçã ".encode("utf-8") + u"maçã ".encode("cp1252") >>> last_position = -1 >>> print a.decode("utf-8", "mixed") maçã maçã 

Gracias a jsbueno y a otras búsquedas de Google y otros golpes, lo resolví de esta manera.

 #The following works very well but it does not allow for any attempts to FIX the data. xmlText = unicode(xmlText, errors='replace').replace(u"\uFFFD", "?") 

Esta versión permite una oportunidad limitada para reparar caracteres no válidos. Los caracteres desconocidos se reemplazan con un valor seguro.

 import codecs replacement = { '85' : '...', # u'\u2026' ... character. '96' : '-', # u'\u2013' en-dash '97' : '-', # u'\u2014' em-dash '91' : "'", # u'\u2018' left single quote '92' : "'", # u'\u2019' right single quote '93' : '"', # u'\u201C' left double quote '94' : '"', # u'\u201D' right double quote '95' : "*" # u'\u2022' bullet } #This is is more complex but allows for the data to be fixed. def mixed_decoder(unicodeError): errStr = unicodeError[1] errLen = unicodeError.end - unicodeError.start nextPosition = unicodeError.start + errLen errHex = errStr[unicodeError.start:unicodeError.end].encode('hex') if errHex in replacement: return u'%s' % replacement[errHex], nextPosition return u'%s' % errHex, nextPosition # Comment this line out to get a question mark return u'?', nextPosition codecs.register_error("mixed", mixed_decoder) xmlText = xmlText.decode("utf-8", "mixed") 

Básicamente bash convertirlo en utf8. Para cualquier personaje que falle, simplemente lo convierto a HEX para que pueda mostrarlo o buscarlo en mi propia tabla.

Esto no es bonito pero me permite dar sentido a los datos desordenados