UnicodeDecodeError при перенаправлении в файл

Я запускаю этот фрагмент дважды, на терминале Ubuntu (кодировка, установленная на utf-8), один раз с ./test.py, а затем с ./test.py >out.txt:

uni = u"\u001A\u0BC3\u1451\U0001D10C"
print uni

Без перенаправления он печатает мусор. При перенаправлении я получаю UnicodeDecodeError. Может кто-нибудь объяснить, почему я получаю ошибку только во втором случае, или даже лучше дать подробное объяснение того, что происходит за занавесом в обоих случаях?

Ответ 1

Весь ключ к таким проблемам кодирования состоит в том, чтобы понять, что в принципе есть два разных понятия "строка" : (1) строка символов и (2) строка/массив байтов. Это различие в основном игнорировалось в течение длительного времени из-за исторической вездесущности кодировок не более 256 символов (ASCII, Latin-1, Windows-1252, Mac OS Roman,...): эти кодировки отображают набор общих символов для числа от 0 до 255 (т.е. байты); относительно ограниченный обмен файлами до появления Интернета сделал эту ситуацию несовместимыми кодировками допустимой, так как большинство программ могли игнорировать тот факт, что было несколько кодировок, пока они создавали текст, который оставался в одной и той же операционной системе: такие программы просто обрабатывать текст как байты (через кодировку, используемую операционной системой). Правильное, современное представление должным образом разделяет эти две строковые понятия, основываясь на следующих двух моментах:

  • Символы в основном не связаны с компьютерами: их можно нарисовать на доске и т.д., например, بايثون, 中 蟒 и 🐍. "Персонажи" для машин также включают в себя "инструкции рисования", например, пробелы, возврат каретки, инструкции по настройке направления письма (для арабского и т.д.), Акценты и т.д. очень большой список символов включен в стандарт Unicode; он охватывает большинство известных символов.

  • С другой стороны, компьютеры должны каким-то образом представлять абстрактные символы: для этого они используют массивы байтов (числа от 0 до 255 включены), поскольку их память приходит в байтовых кусках. Необходимый процесс, который преобразует символы в байты, называется кодировкой. Таким образом, компьютер должен кодировать, чтобы представлять символы. Любой текст, присутствующий на вашем компьютере, кодируется (пока он не отображается), будет ли он отправлен на терминал (который ожидает символов, закодированных определенным образом), или сохранен в файле. Чтобы отображаться или правильно "пониматься" (например, интерпретатором Python), потоки байтов декодируются в символы. Несколько кодировок (UTF-8, UTF-16,...) определены Unicode для его списка символов (Unicode таким образом определяет как список символы и кодировки для этих символов - все еще есть места, где вы видите выражение "кодировка Unicode" как способ ссылаться на вездесущий UTF-8, но это неправильная терминология, поскольку Unicode предоставляет несколько кодировок).

Таким образом, компьютерам необходимо внутренне представлять символы с байтами, и они делают это через две операции:

Кодирование: символы → байты

Декодирование: байты → символы

Некоторые кодировки не могут кодировать все символы (например, ASCII), в то время как (некоторые) кодировки Unicode позволяют кодировать все символы Unicode. Кодирование также не обязательно уникально, поскольку некоторые символы могут быть представлены либо непосредственно, либо как комбинация (например, базового символа и акцентов).

Обратите внимание, что концепция новой строки добавляет уровень сложности, поскольку он может быть представлен различными (контроль), которые зависят от операционной системы (вот почему Python универсальный режим чтения строк в новой строке).

Теперь то, что я назвал "символом" выше, - это то, что Unicode вызывает " воспринимаемый пользователем символ". Один пользовательский воспринимаемый персонаж иногда может быть представлен в Юникоде, комбинируя части символов (базовый символ, акценты,...), найденный в разных индексах в Unicode список, который называется " кодовые точки "- эти кодовые точки можно объединить вместе, чтобы сформировать "кластер графем", Таким образом, Unicode приводит к третьей концепции строки, состоящей из последовательности кодовых точек Unicode, которая находится между байтовыми и символьными строками и которая ближе к последней. Я назову их " Unicode string" (например, в Python 2).

Хотя Python может печатать строки из (воспринимаемых пользователем) символов, Небайтовые строки Python на самом деле являются последовательностями кодовых точек Unicode, а не воспринимаемых пользователем символов. Значения кодовой точки - это те, которые используются в строковых синтаксисах Python \u и \u Unicode. Их не следует путать с кодировкой символа (и не нужно иметь никаких отношений с ним: кодовые точки Unicode могут быть закодированы различными способами).

Конкретно это означает, что длина строки Python (Unicode) не всегда равна количеству воспринимаемых пользователем символов: таким образом s = "\u1100\u1161\u11a8"; print(s, "len", len(s)) (Python 3) дает 각 len 3, несмотря на s имеющий один воспринимаемый пользователем (корейский) символ (потому что он представлен тремя кодовыми точками - даже если это не обязательно, как показывает print("\uac01")). Однако во многих практических случаях длина строки - это количество воспринимаемых пользователем символов, поскольку многие символы обычно хранятся Python как единая кодовая точка Unicode.

В Python 2 строки Unicode вызывают... "Строки Unicode" (unicode type, literal form u"…"), а массивы байтов - "строки" (str type, где массив байтов, например, может быть построен со строковыми литералами "…"). В Python 3 строки Unicode просто называются "строками" (str type, literal form "…"), а массивы байтов - "байты" (bytes type, literal form b"…").

С этими несколькими ключевыми моментами вы должны понимать большинство вопросов, связанных с кодировкой!


Обычно, когда вы печатаете u"…" на терминале, вы не должны получать мусор: Python знает кодировку вашего терминала. Фактически, вы можете проверить, какую кодировку ожидает терминал:

% python
Python 2.7.6 (default, Nov 15 2013, 15:20:37) 
[GCC 4.2.1 Compatible Apple LLVM 5.0 (clang-500.2.79)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> print sys.stdout.encoding
UTF-8

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

Если ваши входные символы не могут быть закодированы с помощью терминального кодирования, это означает, что терминал не настроен для отображения этих символов. Python будет жаловаться (в Python с UnicodeEncodeError, поскольку строка символов не может быть закодирована таким образом, который подходит вашему терминалу). Единственное возможное решение - использовать терминал, который может отображать символы (либо путем настройки терминала, чтобы он принимал кодировку, которая может представлять ваши символы, либо используя другую терминальную программу). Это важно при распространении программ, которые могут использоваться в разных средах: сообщения, которые вы печатаете, должны быть представлены в пользовательском терминале. Иногда лучше придерживаться строк, содержащих только символы ASCII.

Однако, когда вы перенаправляете или транслируете вывод вашей программы, тогда, как правило, невозможно узнать, что такое входная кодировка принимающей программы, а приведенный выше код возвращает некоторую стандартную кодировку: None (Python 2.7) или UTF-8 (Python 3):

% python2.7 -c "import sys; print sys.stdout.encoding" | cat
None
% python3.4 -c "import sys; print(sys.stdout.encoding)" | cat
UTF-8

Однако кодирование stdin, stdout и stderr может установить через переменную среды PYTHONIOENCODING, если необходимо:

% PYTHONIOENCODING=UTF-8 python2.7 -c "import sys; print sys.stdout.encoding" | cat
UTF-8

Если печать на терминал не дает то, что вы ожидаете, вы можете проверить правильность кодировки UTF-8, которую вы ввели вручную; например, ваш первый символ (\u001A) не может быть распечатан, если я не ошибаюсь.

Для получения дополнительной информации: http://wiki.python.org/moin/PrintFails. Из этой ссылки вы можете найти такое решение для Python 2.x:

import codecs
import locale
import sys

# Wrap sys.stdout into a StreamWriter to allow writing unicode.
sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout) 

uni = u"\u001A\u0BC3\u1451\U0001D10C"
print uni

Для Python 3 вы можете проверить один из вопросов, заданных ранее в StackOverflow.

Ответ 2

Python всегда кодирует строки Unicode при записи в терминал, файл, канал и т.д. При записи на терминал Python обычно может определять кодировку терминала и правильно использовать его. При записи в файл или канал Python по умолчанию используется кодировка "ascii", если явно не указано иное. Python может сказать, что делать, когда вывод трубопровода через переменную среды PYTHONIOENCODING. Оболочка может установить эту переменную перед перенаправлением вывода Python в файл или канал, чтобы было известно правильное кодирование.

В вашем случае вы напечатали 4 необычных символа, которые ваш терминал не поддерживал в своем шрифте. Вот несколько примеров, которые помогут объяснить поведение, с символами, которые фактически поддерживаются моим терминалом (который использует cp437, а не UTF-8).

Пример 1

Обратите внимание, что комментарий #coding указывает кодировку, в которой сохранен исходный файл. Я выбрал utf8, чтобы я мог поддерживать символы в источнике, которые мой терминал не мог. Кодировка перенаправляется на stderr, поэтому ее можно увидеть при перенаправлении в файл.

#coding: utf8
import sys
uni = u'αßΓπΣσµτΦΘΩδ∞φ'
print >>sys.stderr,sys.stdout.encoding
print uni

Выход (выполняется непосредственно из терминала)

cp437
αßΓπΣσµτΦΘΩδ∞φ

Python правильно определил кодировку терминала.

Выход (перенаправлен в файл)

None
Traceback (most recent call last):
  File "C:\ex.py", line 5, in <module>
    print uni
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-13: ordinal not in range(128)

Python не может определить кодировку (None), поэтому используется "ascii" по умолчанию. ASCII поддерживает преобразование первых 128 символов Unicode.

Выход (перенаправлен в файл, PYTHONIOENCODING = cp437)

cp437

и мой выходной файл был прав:

C:\>type out.txt
αßΓπΣσµτΦΘΩδ∞φ

Пример 2

Теперь я поставлю символ в источнике, который не поддерживается моим терминалом:

#coding: utf8
import sys
uni = u'αßΓπΣσµτΦΘΩδ∞φ马' # added Chinese character at end.
print >>sys.stderr,sys.stdout.encoding
print uni

Выход (выполняется непосредственно из терминала)

cp437
Traceback (most recent call last):
  File "C:\ex.py", line 5, in <module>
    print uni
  File "C:\Python26\lib\encodings\cp437.py", line 12, in encode
    return codecs.charmap_encode(input,errors,encoding_map)
UnicodeEncodeError: 'charmap' codec can't encode character u'\u9a6c' in position 14: character maps to <undefined>

Мой терминал не понял этого последнего китайского персонажа.

Выход (запускается напрямую, PYTHONIOENCODING = 437: заменить)

cp437
αßΓπΣσµτΦΘΩδ∞φ?

Обработчики ошибок могут быть указаны с помощью кодировки. В этом случае неизвестные символы были заменены на ?. ignore и xmlcharrefreplace - некоторые другие варианты. При использовании UTF8 (который поддерживает кодирование всех символов Unicode) замены никогда не будут выполнены, но шрифт, используемый для отображения символов, должен по-прежнему поддерживать их.

Ответ 3

Кодировать его во время печати

uni = u"\u001A\u0BC3\u1451\U0001D10C"
print uni.encode("utf-8")

Это связано с тем, что когда вы запускаете script ручной python, он кодирует его перед выводом его на терминал, когда вы его передаете, python не кодирует его сам, поэтому вам нужно кодировать вручную при выполнении операций ввода-вывода.