Python - обработка файлов с смешанным кодированием

У меня есть файл, который в основном UTF-8, но некоторые символы Windows-1252 также нашли там путь.

Я создал таблицу для сопоставления символов Windows-1252 (cp1252) с их юникодными аналогами и хотел бы использовать ее для исправления некодированных символов, например.

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)

Но попытка выполнить эту замену приводит к тому, что создается UnicodeDecodeError, например:

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

Любые идеи о том, как справиться с этим?

Ответы

Ответ 1

Если вы попытаетесь декодировать эту строку как utf-8, как вы уже знаете, вы получите ошибку "UnicodeDecode", так как эти ложные символы cp1252 недействительны utf-8 -

Однако кодеки Python позволяют зарегистрировать обратный вызов

В моем терминале utf-8 я могу создать смешанную неправильную строку:

>>> a = u"maçã ".encode("utf-8") + u"maçã ".encode("cp1252")
>>> print a
maçã ma�� 
>>> a.decode("utf-8")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  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

Здесь я написал указанную функцию обратного вызова и нашел catch: даже если вы увеличиваете позицию, из которой следует декодировать строку на 1, чтобы она начиналась на следующем chratcer, если следующий символ также не является utf- 8 и вне диапазона (128) ошибка возникает при значении первого вне диапазона (128) - это означает, что декодирование "возвращается", если найдены последовательные символы non-ascii, non-utf-8.

Работа над этим состояла в том, чтобы иметь переменную состояния в error_handler, которая обнаруживает эту "ходьбу назад" и возобновляет декодирование от последнего вызова к ней - в этом кратком примере я реализовал ее как глобальную переменную - (она будет иметь для ручного reset до "-1" перед каждым вызовом декодера):

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)

И на консоли:

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

Ответ 2

Благодаря jsbueno и удару других поисковых запросов Google и других шагов я решил это так.

#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", "?")

Эта версия позволяет ограничить возможность исправления недопустимых символов. Неизвестные символы заменяются безопасным значением.

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")

В основном я пытаюсь превратить его в utf8. Для любых символов, которые выходят из строя, я просто конвертирую его в HEX, чтобы я мог отображать или искать его в собственной таблице.

Это не очень, но это позволяет мне разобраться в испорченных данных.