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" як спосіб посилання на всюдисущий UTF-8, але це неправильна термінологія, оскільки Unicode надає кілька кодувань).

Підсумовуючи це, комп'ютери повинні внутрішньо представляти символи з байтами , і це роблять через дві операції:

Кодування : символи → байти

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

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

Зауважте, що концепція newline додає шар ускладнень , оскільки він може бути представлений різними (контрольними) символами, які залежать від операційної системи (це є причиною універсального режиму зчитування файлів нового рядка Python ).

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

Хоча Python може друкувати рядки (сприймаються користувачем) символів, небайтові рядки Python - це по суті послідовності точок коду Unicode , а не символи, сприйняті користувачем. Значення точки коду - це ті, які використовуються в синтаксисі рядків Python \uі \UUnicode. Їх не слід плутати з кодуванням символу (і не повинні мати з ним ніяких відносин: Точки коду 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.

Однак, коли ви перенаправляєте або передаєте висновок своєї програми, тоді, як правило, неможливо дізнатися, що таке кодування входу приймаючої програми, і вищевказаний код повертає деяке кодування за замовчуванням: None (Python 2.7) або UTF-8 ( Пітон 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, але я виявив, що, незважаючи на те, що було цікаво, вони ніколи не дозволили мені вирішити конкретні проблеми кодування ... Я справді вважаю, що пам'ятаючи про Принципи, знайдені в цій відповіді, і знаходження часу, щоб використовувати їх при вирішенні конкретних проблем кодування, допомагає дуже багато.
Ерік О Лебігот

3
Це руками найкраще пояснення унікоду та пітона коли-небудь. Python Unicode HOWTO слід замінити цим.
stantonk

1
Ось, дозвольте мені намалювати на цій дошці
символ "переодяг

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.
Ерік О Лебігот

2
Так, Python 3 за замовчуванням має значення 'utf8', але на основі вибірки ОП він використовує Python 2.X, який за замовчуванням має значення 'ascii'.
Марк Толонен

Я не зміг отримати правильний вихід, маніпулюючи PYTHONIOENCODING. Робити print string.encode("UTF-8")як запропоновано @Ismail працював для мене.
tripleee

ви можете бачити китайські символи, якщо ваш шрифт підтримує їх, навіть якщо chcpкодова сторінка їх не підтримує. Щоб уникнути UnicodeEncodeError: 'charmap', ви можете встановити win-unicode-consoleпакет.
jfs

Моя проблема полягає в тому, що python-gitlab CLI друкує китайські символи добре у cmd, але символи сміття після перенаправлення у файли. PYTHONIOENCODING=utf-8вирішує проблему.
ElpieKay

12

Кодуйте його під час друку

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

Це тому, що при запуску скрипту вручну python кодує його перед виведенням на термінал, коли ви передаєте його, python не кодує його сам, тому вам доведеться кодувати вручну під час виконання вводу-виводу.


4
Він досі не відповідає на питання, що тут відбувається WTH. Чому ж із синього кольору він вирішує кодувати лише при перенаправленнях, коли це має бути повністю прозорим для процесу.
Максим Слойко

Чому python не кодує його під час перенаправлення? Чи чітко перевіряє python і вирішує, що він буде робити інакше, тільки щоб було складно?
Арафангіон

1
чи навіть у пітона є спосіб розрізнити дві ситуації? Я гадаю (до цих пір ...), що немає способу це знати.
zedoo

4
Python може перевірити, чи є вихід терміналом, якщо його вихід на трубу, то тип терміналу буде "тупим". Я здогадуюсь, що "німий" повинен вам сказати, чому Python не намагається робити щось автоматичне в цьому випадку, це може вийти з ладу.
ismail

1
він створює mojibake, якщо середовище використовує кодування символів, несумісне з utf-8 (наприклад, воно поширене в Windows). Не жорстко кодуйте кодування символів вашого середовища всередині вашого сценарію. Налаштуйте свою локаль або PYTHONIOENCODING або встановіть win-unicode-console(Windows) або прийміть параметр командного рядка (якщо потрібно).
jfs
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.