Чому Python друкує символи unicode, коли кодування за замовчуванням - ASCII?


139

З оболонки Python 2.6:

>>> import sys
>>> print sys.getdefaultencoding()
ascii
>>> print u'\xe9'
é
>>> 

Я очікував, що після заяви про друк з'явиться або помилка, або помилка, оскільки символ "é" не є частиною ASCII, і я не вказав кодування. Я думаю, я не розумію, що означає кодування ASCII за замовчуванням.

EDIT

Я перемістив правку до розділу Відповіді та прийняв її, як було запропоновано.


6
Було б дуже добре , якби ви могли перетворити це змінити у відповідь замість цього і прийняти його.
меркатор

2
Друк '\xe9'у терміналі, налаштованому для UTF-8, не надрукується é. Він надрукує символ заміни (як правило, питання запитання), оскільки \xe9це не є дійсною послідовністю UTF-8 (у ньому відсутні два байти, які повинні були би слідувати за цим провідним байтом). Це, швидше за все, не буде трактуватися як латинська-1.
Martijn Pieters

2
@MartijnPieters Я підозрюю, що ти, можливо, перекинувся на частину, де я вказав, що термінал встановлений для декодування в ISO-8859-1 (латинська 1), коли я виходжу \xe9для друку é.
Майкл Екока

2
Ага так, я пропустив цю частину; термінал має конфігурацію, яка відрізняється від оболонки. Перевірка.
Martijn Pieters

Я проглянув відповідь, але насправді у мене є рядок без префікса u для python 2.7. чому це все ще обробляється як unicode? (мій sys.getdefaultencoding () - ascii)
dtc

Відповіді:


104

Завдяки шматочкам і фрагментам з різних відповідей, я думаю, що ми можемо скласти пояснення.

Намагаючись надрукувати рядок unicode, u '\ xe9', Python неявно намагається кодувати цю рядок за допомогою схеми кодування, яка зараз зберігається в sys.stdout.encoding. Python насправді вибирає цей параметр із середовища, з якого він був ініційований. Якщо він не може знайти належне кодування з навколишнього середовища, лише тоді він повертається до стандартного коду ASCII.

Наприклад, я використовую bash оболонку, кодуючу за замовчуванням UTF-8. Якщо я запускаю Python з нього, він підбирає та використовує це налаштування:

$ python

>>> import sys
>>> print sys.stdout.encoding
UTF-8

Давайте на мить вийдемо з оболонки Python і встановимо середовище bash з деяким фальшивим кодуванням:

$ export LC_CTYPE=klingon
# we should get some error message here, just ignore it.

Потім знову запустіть оболонку python і переконайтеся, що він дійсно повертається до стандартного кодування ascii.

$ python

>>> import sys
>>> print sys.stdout.encoding
ANSI_X3.4-1968

Бінго!

Якщо ви зараз спробуєте вивести якийсь символ unicode поза ascii, вам слід отримати гарне повідомлення про помилку

>>> print u'\xe9'
UnicodeEncodeError: 'ascii' codec can't encode character u'\xe9' 
in position 0: ordinal not in range(128)

Дозволяє вийти з Python і відкинути оболонку bash.

Зараз ми спостерігатимемо, що станеться після виходу рядків Python. Для цього ми спочатку запустимо bash оболонку в графічному терміналі (я використовую Gnome Terminal) і встановимо термінал для декодування виводу за допомогою ISO-8859-1 aka latin-1 (графічні термінали зазвичай мають можливість встановлення символу Кодування в одному зі спадних меню). Зауважте, що це не змінює кодування фактичного середовища оболонки , воно лише змінює спосіб, яким сам термінал розшифрує отриманий дані, як і веб-браузер. Тому ви можете змінити кодування терміналу незалежно від середовища оболонки. Далі тоді запустимо Python з оболонки і переконаємось, що sys.stdout.encoding встановлено для кодування середовища оболонки (для мене UTF-8):

$ python

>>> import sys

>>> print sys.stdout.encoding
UTF-8

>>> print '\xe9' # (1)
é
>>> print u'\xe9' # (2)
é
>>> print u'\xe9'.encode('latin-1') # (3)
é
>>>

(1) python видає двійкову рядок як є, термінал отримує її і намагається співставити її значення з карткою символів латин-1. У латинській-1, 0xe9 або 233 видається символ "é", і ось так відображається термінал.

(2) python намагається неявно кодувати рядок Unicode з будь-якою схемою, яка в даний час встановлена ​​в sys.stdout.encoding, у цьому випадку це "UTF-8". Після кодування UTF-8 отриманий двійковий рядок є "\ xc3 \ xa9" (див. Пізніше пояснення). Термінал отримує потік як такий і намагається розшифрувати 0xc3a9, використовуючи latin-1, але Latin-1 переходить від 0 до 255 і так, декодує лише потоки 1 байт за один раз. 0xc3a9 - 2 байти, латино-1 декодер інтерпретує це як 0xc3 (195) та 0xa9 (169), і це дає 2 символи: Ã і ©.

(3) python кодує код коду унікоду u '\ xe9' (233) зі схемою lat-1. Виходить діапазон кодових точок латин-1 - 0-255 і вказує на такий самий символ, що і Unicode в цьому діапазоні. Отже, кодові точки Unicode у цьому діапазоні дадуть те саме значення, що кодується латиною-1. Тож u '\ xe9' (233), закодований в латинській 1, також дає двійковий рядок '\ xe9'. Термінал отримує це значення і намагається його зіставити на карті символів латиниці-1. Як і у випадку (1), він дає "é" і ось що відображається.

Давайте тепер змінимо параметри кодування терміналу на UTF-8 з випадаючого меню (як би ви змінили налаштування кодування веб-браузера). Не потрібно зупиняти Python або перезавантажувати оболонку. Кодування терміналу тепер відповідає програмі Python. Спробуємо роздрукувати ще раз:

>>> print '\xe9' # (4)

>>> print u'\xe9' # (5)
é
>>> print u'\xe9'.encode('latin-1') # (6)

>>>

(4) python видає двійковий рядок як є. Термінал намагається декодувати цей потік за допомогою UTF-8. Але UTF-8 не розуміє значення 0xe9 (див. Пізніше пояснення) і тому не в змозі перетворити його в код коду unicode. Не знайдено точку коду, не надруковано жодного символу.

(5) python намагається неявно кодувати рядок Unicode тим, що є в sys.stdout.encoding. Ще "UTF-8". Отриманий двійковий рядок дорівнює '\ xc3 \ xa9'. Термінал приймає потік і намагається декодувати 0xc3a9 також за допомогою UTF-8. Він отримує зворотне значення коду 0xe9 (233), яке на карті символів Unicode вказує на символ "é". На терміналі відображається "é".

(6) python кодує рядок unicode з latin-1, він отримує двійкову рядок з тим же значенням \ \ xe9 '. Знову ж таки, для терміналу це майже те саме, що і у випадку (4).

Висновки: - Python видає рядки без унікоду як вихідні дані, не враховуючи кодування за замовчуванням. Термінал просто відбувається, щоб відобразити їх, якщо його поточне кодування відповідає даним. - Python виводить рядки Unicode після їх кодування за схемою, визначеною в sys.stdout.encoding. - Python отримує цей параметр із середовища оболонки. - термінал відображає вихід відповідно до власних налаштувань кодування. - кодування терміналу не залежить від оболонки.


Детальніше про unicode, UTF-8 та latin-1:

Unicode - це в основному таблиця символів, де умовно призначені деякі клавіші (кодові точки), які вказують на деякі символи. наприклад, за домовленістю було вирішено, що ключ 0xe9 (233) - це значення, що вказує на символ 'é'. ASCII і Unicode використовують однакові кодові точки від 0 до 127, як і латині-1 і Unicode від 0 до 255. Тобто 0x41 вказує на "A" в ASCII, латині-1 і Unicode, 0xc8 вказує на "Ü" в латинська-1 і Unicode, 0xe9 вказує на "é" в латинській-1 і Unicode.

Під час роботи з електронними пристроями кодові точки Unicode потребують ефективного представлення в електронному вигляді. Ось в чому полягають схеми кодування. Існують різні схеми кодування Unicode (utf7, UTF-8, UTF-16, UTF-32). Найінтуїтивнішим і прямолінійним підходом кодування було б просто використовувати значення кодової точки на карті Unicode як її значення для своєї електронної форми, але в даний час Unicode має понад мільйон кодових точок, що означає, що для деяких з них потрібно 3 байти виражений. Для ефективної роботи з текстом відображення від 1 до 1 було б досить непрактичним, оскільки вимагало б збереження всіх точок коду в точно однаковій кількості простору, принаймні 3 байти на символ, незалежно від їх реальної потреби.

Більшість схем кодування мають недоліки щодо простору, більшість економічних не охоплюють усіх кодів коду unicode, наприклад, ascii охоплює лише перші 128, тоді як латинська-1 охоплює перші 256. Інші, які намагаються бути більш всебічними, також закінчуються будучи марними, оскільки їм потрібно більше байтів, ніж потрібно, навіть для звичайних "дешевих" символів. Наприклад, UTF-16 використовує як мінімум 2 байти на символ, у тому числі ті, що знаходяться в діапазоні ascii ("B", який дорівнює 65, все ще потребує 2 байти зберігання в UTF-16). UTF-32 ще більш марнотратний, оскільки він зберігає всі символи в 4 байти.

UTF-8 спритно вирішив дилему, зі схемою, здатною зберігати кодові точки із змінною кількістю байтових пробілів. Як частина стратегії кодування, UTF-8 зашнуровує кодові точки з бітами прапора, які вказують (імовірно на декодери) їхні вимоги до простору та їх межі.

Кодування UTF-8 унікодових точок коду в діапазоні ascii (0-127):

0xxx xxxx  (in binary)
  • x 'показує фактичний простір, відведений для "зберігання" кодової точки під час кодування
  • Провідний 0 - це прапор, який вказує на декодер UTF-8, що для цього кодового пункту буде потрібно лише 1 байт.
  • при кодуванні UTF-8 не змінює значення точок коду в цьому конкретному діапазоні (тобто 65, закодованих в UTF-8, також 65). Враховуючи, що Unicode та ASCII також сумісні в одному діапазоні, це, до речі, робить UTF-8 та ASCII також сумісними в цьому діапазоні.

наприклад, кодова точка Unicode для 'B' - це "0x42" або 0100 0010 у двійковій формі (як ми вже говорили, вона однакова в ASCII). Після кодування в UTF-8 він стає:

0xxx xxxx  <-- UTF-8 encoding for Unicode code points 0 to 127
*100 0010  <-- Unicode code point 0x42
0100 0010  <-- UTF-8 encoded (exactly the same)

Кодування UTF-8 кодових точок Unicode вище 127 (non-ascii):

110x xxxx 10xx xxxx            <-- (from 128 to 2047)
1110 xxxx 10xx xxxx 10xx xxxx  <-- (from 2048 to 65535)
  • провідні біти '110' вказують на декодер UTF-8 початок кодової точки, закодованої в 2 байти, тоді як '1110' позначає 3 байти, 11110 позначає 4 байти тощо.
  • внутрішні біти прапора "10" використовуються для сигналізації початку внутрішнього байта.
  • знову ж таки, х позначає простір, де зберігається значення кодової точки Unicode після кодування.

наприклад 'é' Кодова точка Unicode - 0xe9 (233).

1110 1001    <-- 0xe9

Коли UTF-8 кодує це значення, воно визначає, що значення більше 127 і менше 2048, тому слід кодувати в 2 байти:

110x xxxx 10xx xxxx   <-- UTF-8 encoding for Unicode 128-2047
***0 0011 **10 1001   <-- 0xe9
1100 0011 1010 1001   <-- 'é' after UTF-8 encoding
C    3    A    9

Кодові точки 0xe9 Unicode після кодування UTF-8 стають 0xc3a9. Саме так термінал отримує його. Якщо ваш термінал встановлений для декодування рядків, використовуючи latin-1 (одне з некодичних застарілих кодувань), ви побачите Ã ©, оскільки так буває, що 0xc3 в латинській-1 вказує на Ã і 0xa9 на ©.


6
Відмінне пояснення. Тепер я розумію UTF-8!
Doctor Coder

2
Гаразд, я прочитав всю вашу публікацію приблизно за 10 секунд. У ньому було сказано: "Python відсмоктує, коли мова йде про кодування".
Андрій

Чудове пояснення. Не могли б ви вирішити це питання?
Maggyero

26

Коли символи Unicode друкуються до stdout, sys.stdout.encodingвикористовується. Передбачається, що символ не Unicode входить sys.stdout.encodingі просто надсилається до терміналу. У моїй системі (Python 2):

>>> import unicodedata as ud
>>> import sys
>>> sys.stdout.encoding
'cp437'
>>> ud.name(u'\xe9') # U+00E9 Unicode codepoint
'LATIN SMALL LETTER E WITH ACUTE'
>>> ud.name('\xe9'.decode('cp437')) 
'GREEK CAPITAL LETTER THETA'
>>> '\xe9'.decode('cp437') # byte E9 decoded using code page 437 is U+0398.
u'\u0398'
>>> ud.name(u'\u0398')
'GREEK CAPITAL LETTER THETA'
>>> print u'\xe9' # Unicode is encoded to CP437 correctly
é
>>> print '\xe9'  # Byte is just sent to terminal and assumed to be CP437.
Θ

sys.getdefaultencoding() використовується лише тоді, коли в Python немає іншого варіанту.

Зауважте, що Python 3.6 або пізніша версія ігнорує кодування в Windows і використовує API Unicode для запису Unicode в термінал. Немає попереджень UnicodeEncodeError і правильний символ не відображається, якщо шрифт підтримує це. Навіть якщо шрифт не підтримує його, символи все одно можуть бути вирізані з терміналу до програми із підтримуючим шрифтом, і це буде правильно. Оновіть!


8

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

>>> print sys.stdout.encoding
UTF-8

3
просто з цікавості, як би я змінив sys.stdout.encoding на ascii?
Майкл Екока

2
@TankorSmash Я виходжу TypeError: readonly attribute2.7.2
Кос

4

Ви вже вказали кодування, ввівши явно рядок Unicode. Порівняйте результати невикористання uпрефікса.

>>> import sys
>>> sys.getdefaultencoding()
'ascii'
>>> '\xe9'
'\xe9'
>>> u'\xe9'
u'\xe9'
>>> print u'\xe9'
é
>>> print '\xe9'

>>> 

У разі \xe9тоді Python передбачає кодування за замовчуванням (Ascii), таким чином друкуючи ... щось порожнє.


1
тож якщо я добре розумію, коли я роздруковую рядки Unicode (кодові точки), python передбачає, що я хочу, щоб результат був закодований у utf-8, а не просто намагався дати мені те, що це могло бути в ascii?
Майкл Екока

1
@mike: AFAIK те, що ви сказали, є правильним. Якщо він зробив роздрукувати символи Unicode , але закодований як ASCII, все вийде спотворений і , ймовірно , всі новачки будуть питати, «Чому я не можу надрукувати текст Unicode?»
Марк Рушакофф

2
Дякую. Я насправді є одним із тих початківців, але приходжу з боку людей, які мають певне розуміння унікоду, саме тому така поведінка мене трохи відкидає.
Майкл Екока

3
Р., не правильно, оскільки "\ xe9" не в наборі символів ascii. Рядки Unicode друкуються за допомогою sys.stdout.encoding, рядки Unicode кодуються до sys.stdout.encoding перед друком.
Марк Толонен

0

Це працює для мене:

import sys
stdin, stdout = sys.stdin, sys.stdout
reload(sys)
sys.stdin, sys.stdout = stdin, stdout
sys.setdefaultencoding('utf-8')

1
Дешевий брудний злом, який неминуче зламає щось інше. Це не важко зробити це правильно!
Кріс Джонсон

0

Відповідно до кодового / неявного рядкового кодування та конверсій Python :

  • Коли printін unicode, це буде encodeс <file>.encoding.
    • коли значення encodingне встановлено, то unicodeімпліцитно перетворюється на str(оскільки кодек для цього є sys.getdefaultencoding(), тобто asciiбудь-який національний символ може викликати a UnicodeEncodeError)
    • для стандартних потоків, encodingвисновок визначається з навколишнього середовища. Зазвичай це встановлені ttyпотоки fot (з локальних налаштувань терміналу), але, швидше за все, вони не встановлені для труб
      • тож a print u'\xe9', швидше за все, вдасться, коли вихід буде до терміналу, і не вдасться, якщо він перенаправлений. Рішення полягає encode()в рядку з потрібним кодуванням перед printing.
  • Коли printІНГ strбайти передаються в потік , як є. Який гліф відображатиме термінал, залежатиме від його локальних налаштувань.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.