Двойной декодирование юникода в python

Я работаю против приложения, которое, похоже, хочет вернуться, что я считаю двойным кодированным строкой UTF-8.

Я отправляю строку u'XüYß', закодированную с использованием UTF-8, становясь X\u00fcY\u00df (равным X\xc3\xbcY\xc3\x9f).

Сервер должен просто повторить то, что я ему отправил, но возвращает следующее: X\xc3\x83\xc2\xbcY\xc3\x83\xc2\x9f (должно быть X\xc3\xbcY\xc3\x9f). Если я декодирую его с помощью str.decode('utf-8') становится u'X\xc3\xbcY\xc3\x9f', который выглядит как... unicode-строка, содержащая исходную строку, кодированную с использованием UTF-8.

Но Python не позволит мне декодировать строку unicode, не перекодируя ее сначала - что по какой-то причине не срабатывает, что ускользает от меня:

>>> ret = 'X\xc3\x83\xc2\xbcY\xc3\x83\xc2\x9f'.decode('utf-8')
>>> ret
u'X\xc3\xbcY\xc3\x9f'
>>> ret.decode('utf-8')
# Throws UnicodeEncodeError: 'ascii' codec can't encode ...

Как убедить Python повторно декодировать строку? - и/или есть (практический) способ отладки, что на самом деле в строках, без его передачи, хотя используется все неявное преобразование print?

(И да, я сообщил об этом поведении с разработчиками серверной части.)

Ответ 1

ret.decode() пытается неявно кодировать ret с системным кодированием - в вашем случае ascii.

Если вы явно кодируете строку unicode, вы должны быть в порядке. Существует встроенная кодировка, которая делает то, что вам нужно:

>>> 'X\xc3\xbcY\xc3\x9f'.encode('raw_unicode_escape').decode('utf-8')
'XüYß'

Действительно, .encode('latin1') (или cp1252) будет в порядке, потому что это то, что сервер почти исключительно использует. Кодек raw_unicode_escape просто даст вам что-то узнаваемое в конце, а не поднимает исключение:

>>> '€\xe2\x82\xac'.encode('raw_unicode_escape').decode('utf8')
'\\u20ac€'

>>> '€\xe2\x82\xac'.encode('latin1').decode('utf8')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'latin-1' codec can't encode character '\u20ac' in position 0: ordinal not in range(256)

Если вы столкнетесь с подобными смешанными данными, вы можете снова использовать кодек, чтобы нормализовать все:

>>> '€\xe2\x82\xac'.encode('raw_unicode_escape').decode('utf8')
'\\u20ac€'

>>> '\\u20ac€'.encode('raw_unicode_escape')
b'\\u20ac\\u20ac'
>>> '\\u20ac€'.encode('raw_unicode_escape').decode('raw_unicode_escape')
'€€'

Ответ 2

То, что вы хотите, - это кодировка, в которой кодовая точка Юникода X закодирована в одно и то же значение байта X. Для кодовых точек внутри 0-255 вы имеете это в кодировке latin-1:

def double_decode(bstr):
    return bstr.decode("utf-8").encode("latin-1").decode("utf-8")

Ответ 3

Не используйте это! Используйте @hop solution.

Мой неприятный взлом: (съеживаешься, но тихо. Это не моя вина, это ошибка разработчиков сервера)

def double_decode_unicode(s, encoding='utf-8'):
    return ''.join(chr(ord(c)) for c in s.decode(encoding)).decode(encoding)

Затем

>>> double_decode_unicode('X\xc3\x83\xc2\xbcY\xc3\x83\xc2\x9f')
u'X\xfcY\xdf'
>>> print _
XüYß