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

100

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

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

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

зеду
источник
Этот ответ тоже может помочь.
tzot
Когда я пытаюсь повторить ваш вывод, я получаю UnicodeEncodeError, а не UnicodeDecodeError. gist.github.com/jaraco/12abfc05872c65a4f3f6cd58b6f9be4d
Джейсон Р. Кумбс

Ответы:

252

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

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

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

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

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

Расшифровка : байты → символы

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

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

То, что я назвал «символом» выше, - это то, что Unicode называет « символом, воспринимаемым пользователем ». Один воспринимаемый пользователем символ иногда может быть представлен в Юникоде путем комбинирования частей символа (базовый символ, акценты и т. Д.), Находящихся в разных индексах в списке Юникода, которые называются « кодовыми точками » - эти кодовые точки могут быть объединены вместе, чтобы сформировать «графемный кластер». Таким образом, Unicode приводит к третьей концепции строки, состоящей из последовательности кодовых точек Unicode, которая находится между байтовыми и символьными строками и которая ближе к последней. Я назову их " строки Unicode" » (как в 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тип, буквальная форма u"…"), а байтовые массивы - «строками» ( strтипом, где массив байтов может быть, например, построен с помощью строковых литералов "…"). В Python 3 строки Unicode просто называются «строками» ( strтип, буквальная форма "…"), а байтовые массивы - «байтами» ( bytesтип, буквальная форма b"…"). Как следствие, что-то вроде "🐍"[0]дает другой результат в Python 2 ( '\xf0', байт) и Python 3 ( "🐍", первый и единственный символ).

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


Обычно, когда вы печатаете 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.

Однако, когда вы перенаправляете или передаете вывод своей программы по конвейеру , то, как правило, невозможно узнать, какова входная кодировка принимающей программы, и приведенный выше код возвращает некоторую кодировку по умолчанию: Нет (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
@singularity: Спасибо! Я добавил некоторую информацию о Python 3.
Эрик О Лебигот
2
Спасибо, мужик! Мне так долго было нужно это объяснение ... Жалко, что я могу дать вам только один голос.
mik01aj
3
Я рада, что мне помогли, @ m01! Одним из мотивов написания этого ответа было то, что в Интернете было много страниц о Unicode и Python, но я обнаружил, что, несмотря на то, что они были интересными, они никогда полностью не позволяли мне решать конкретные проблемы с кодированием ... Я искренне верю, что, имея в виду принципы, найденные в этом ответе, и уделение времени их использованию при решении конкретных проблем с кодированием очень помогают.
Eric O Lebigot
3
Это лучшее объяснение юникода и питона. Python Unicode HOWTO следует заменить на это.
stantonk
1
Здесь, позвольте мне нарисовать на этой доске символ «переопределение справа налево»…
icktoofay,
20

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) замены никогда не будут производиться, но шрифт, используемый для отображения символов, по-прежнему должен их поддерживать.

Марк Толонен
источник
Не совсем верно, что «При записи в файл или канал Python по умолчанию использует кодировку ascii, если явно не указано иное». Фактически, Python 3 использует UTF-8 в Mac OS X / Fink.
Эрик О Лебигот 05
2
Да, Python 3 по умолчанию имеет значение «utf8», но, судя по образцу OP, он использует Python 2.X, который по умолчанию имеет значение «ascii».
Марк Толонен 05
Я не мог получить правильный результат, манипулируя PYTHONIOENCODING. Выполнение print string.encode("UTF-8")рекомендаций @Ismail сработало для меня.
Tripleee
вы можете видеть китайские символы, если ваш шрифт поддерживает их, даже если chcpкодовая страница их не поддерживает. Чтобы избежать этого UnicodeEncodeError: 'charmap', вы можете установить win-unicode-consolepackage.
jfs
Моя проблема в том, что python-gitlab CLI хорошо печатает китайские символы в cmd, но символы являются мусором после перенаправления в файлы. PYTHONIOENCODING=utf-8решает проблему.
ElpieKay
12

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

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

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

Исмаил
источник
4
Он по-прежнему не отвечает на вопрос, что здесь происходит. Совершенно неожиданно он решает кодировать только при перенаправлении, когда это должно быть полностью прозрачно для процесса.
Максим Слойко
Почему Python не кодирует его при перенаправлении? Проверяет ли python явно и решает, что он будет делать что-то по-другому, чтобы было сложно?
Арафангион
1
Есть ли у Python способ различать эти две ситуации? Я думал (до сих пор ...), что он не может знать.
zedoo
4
Python может проверить, является ли вывод терминалом, если он выводится в канал, тогда тип терминала будет «тупым». Думаю, «тупой» должен сказать вам, почему Python не пытается делать что-либо автоматически в этом случае, это может потерпеть неудачу.
ismail
1
он создает моджибаке, если среда использует кодировку символов, несовместимую с utf-8 (например, это распространено в Windows). Не кодируйте кодировку символов вашего окружения внутри скрипта. Настройте свой языковой стандарт или PYTHONIOENCODING, или установите win-unicode-console(Windows), или примите параметр командной строки (если необходимо).
jfs