MyTetra Share
Делитесь знаниями!
Юникод для чайников
Время создания: 31.08.2017 21:01
Текстовые метки: knowledge
Раздел: Python - Кодировка
Запись: xintrea/mytetra_db_mcold/master/base/1503305940nhbfuzedjn/text.html на raw.githubusercontent.com

Юникод для чайников

  • Python


Сам я не очень люблю заголовки вроде «Покемоны в собственном соку для чайников\кастрюль\сковородок», но это кажется именно тот случай — говорить будем о базовых вещах, работа с которыми довольно часто приводить к купе набитых шишек и уйме потерянного времени вокруг вопроса — «Почему же оно не работает?». Если вы до сих пор боитесь и\или не понимаете Юникода — прошу под кат.


Зачем?


Главный вопрос новичка, который встречается с впечатляющим количеством кодировок и на первый взгляд запутанными механизмами работы с ними (например, в Python 2.x). Краткий ответ — потому что так сложилось :)

Кодировкой, кто не знает, называют способ представления в памяти компьютера (читай — в нулях-единицах\числах) цифр, буков и всех остальных знаков. Например, пробел представляется как 0b100000 (в двоичной), 32 (в десятичной) или 0x20 (в шестнадцатеричной системе счисления).

Так вот, когда-то памяти было совсем немного и всем компьютерам было достаточно 7 бит для представления всех нужных символов (цифры, строчный\прописной латинский алфавит, куча знаков и так называемые управляемые символы — все возможные 127 номеров были кому-то отданы). Кодировка в это время была одна — 
ASCII . Шло время, все были счастливы, а кто не был счастлив (читай — кому не хватало знака "©" или родной буквы «щ») — использовали оставшиеся 128 знаков на свое усмотрение, то есть создавали новые кодировки. Так появились и ISO-8859-1 , и наши (то есть кириличные) cp1251  и KOI8 . Вместе с ними появилась и проблема интерпретации байтов типа 0b1******* (то есть символов\чисел от 128 и до 255) — например, 0b11011111 в кодировке cp1251 это наша родная «Я», в тоже время в кодировке ISO-8859-1 это греческая немецкая Eszett (подсказывает Moonrise ) "ß". Ожидаемо, сетевая коммуникация и просто обмен файлами между разными компьютерами превратились в чёрт-знает-что, несмотря на то, что заголовки типа 'Content-Encoding' в HTTP протоколе, email-письмах и HTML-страницах немного спасали ситуацию.

В этот момент собрались светлые умы и предложили новый стандарт — 
Unicode . Это именно стандарт, а не кодировка — сам по себе Юникод не определяет, как символы будут сохранятся на жестком диске или передаваться по сети. Он лишь определяет связь между символом и некоторым числом, а формат, согласно с которым эти числа будут превращаться в байты, определяется Юникод-кодировками (например, UTF-8  или UTF-16 ). На данный момент в Юникод-стандарте есть немного более 100 тысяч символов, тогда как UTF-16 позволяет поддерживать более одного миллиона (UTF-8 — и того больше).

Полней и веселей по теме советую почитать у великолепного Джоеля Спольски 
The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets .

Ближе к делу!


Естественно, есть поддержка Юникода и в Пайтоне. Но, к сожалению, только в Python 3 все строки стали юникодом, и новичкам приходиться убиваться об ошибки типа:

>>> with open('1.txt') as fh:

s = fh.read()


>>> print s

кощей

>>> parser_result = u'баба-яга' # присвоение для наглядности, представим себе, что это результат работы какого-то парсера

>>> parser_result + s


Traceback (most recent call last):

File "<pyshell#43>", line 1, in <module>

parser_result + s

UnicodeDecodeError: 'ascii' codec can't decode byte 0xea in position 0: ordinal not in range(128)



или так:

>>> str(parser_result)


Traceback (most recent call last):

File "<pyshell#52>", line 1, in <module>

str(parser_result)

UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)



Давайте разберемся, но по порядку.

Зачем кто-то использует Юникод?


Почему мой любимый html-парсер возвращает Юникод? Пусть возвращает обычную строку, а я там уже с ней разберусь! Верно? Не совсем. Хотя каждый из существующих в Юникоде символов и можно (наверное) представить в некоторой однобайтовой кодировке (ISO-8859-1, cp1251 и другие называют однобайтовыми, поскольку любой символ они кодируют ровно в один байт), но что делать если в строке должны быть символы с разных кодировок? Присваивать отдельную кодировку каждому символу? Нет, конечно, надо использовать Юникод.

Зачем нам новый тип «unicode»?


Вот мы и добрались до самого интересного. Что такое строка в Python 2.x? Это просто 
байты. Просто бинарные данные, которые могут быть чем-угодно. На самом деле, когда мы пишем что-нибудь вроде:

>>> x = 'abcd'

>>> x

'abcd'


интерпретатор не создает переменную, которая содержит первые четыре буквы латинского алфавита, но только последовательность

('a', 'b', 'c', 'd')

с четырёх байт, и латинские буквы здесь используются исключительно для обозначения именно этого значения байта. То есть 'a' здесь просто синоним для написания '\x61', и ни чуточку больше. Например:

>>> '\x61'

'a'

>>> struct.unpack('>4b', x) # 'x' - это просто четыре signed/unsigned char-а

(97, 98, 99, 100)

>>> struct.unpack('>2h', x) # или два short-а

(24930, 25444)

>>> struct.unpack('>l', x) # или один long

(1633837924,)

>>> struct.unpack('>f', x) # или float

(2.6100787562286154e+20,)

>>> struct.unpack('>d', x * 2) # ну или половинка double-а

(1.2926117739473244e+161,)



И всё!

И ответ на вопрос — зачем нам «unicode» уже более очевиден —
нужен тип, который будет представятся символами, а не байтами.

Хорошо, я понял чем есть строка. Тогда что такое Юникод в Пайтоне?


«type unicode» — это прежде всего абстракция, которая реализует идею Юникода (набор символов и связанных с ними чисел). Объект типа «unicode» — это уже не последовательность байт, но последовательность собственно символов без какого либо представления о том, как эти символы эффективно сохранить в памяти компьютера. Если хотите — это более высокой уровень абстракции, чем байтовый строки (именно так в Python 3 называют обычные строки, которые используются в Python 2.6).

Как пользоваться Юникодом?


Юникод-строку в Python 2.6 можно создать тремя (как минимум, естественно) способами:

u"" литерал:

>>> u'abc'

u'abc'



Метод «decode» для байтовой строки:

>>> 'abc'.decode('ascii')

u'abc'



Функция «unicode»:

>>> unicode('abc', 'ascii')

u'abc'



ascii в последних двух примерах указывается в качестве кодировки, что будет использоваться для превращения байтов в символы. Этапы этого превращения выглядят примерно так:

'\x61' -> кодировка ascii -> строчная латинская "a" -> u'\u0061' (unicode-point для этой буквы)


или


'\xe0' -> кодировка c1251 -> строчная кириличная "a" -> u'\u0430'




Как из юникод-строки получить обычную? Закодировать её:

>>> u'abc'.encode('ascii')

'abc'




Алгоритм кодирования естественно обратный приведенному выше. 

Запоминаем и не путаем —
юникод == символы, строка == байты, и байты -> что-то значащее (символы) — это де-кодирование (decode), а символы -> байты — кодирование (encode).

Не кодируется :(


Разберем примеры с начала статьи. Как работает конкатенация строки и юникод-строки? Простая строка должна быть превращена в юникод-строку, и
поскольку интерпретатор не знает кодировки, от использует кодировку по умолчанию — ascii. Если этой кодировке не удастся декодировать строку, получим некрасивую ошибку. В таком случае нам нужно самим привести строку к юникод-строке, используя правильную кодировку:

>>> print type(parser_result), parser_result

<type 'unicode'> баба-яга

>>> s = 'кощей'

>>> parser_result + s


Traceback (most recent call last):

File "<pyshell#67>", line 1, in <module>

parser_result + s

UnicodeDecodeError: 'ascii' codec can't decode byte 0xea in position 0: ordinal not in range(128)


>>> parser_result + s.decode('cp1251')

u'\xe1\xe0\xe1\xe0-\xff\xe3\xe0\u043a\u043e\u0449\u0435\u0439'

>>> print parser_result + s.decode('cp1251')

баба-ягакощей

>>> print '&'.join((parser_result, s.decode('cp1251')))

баба-яга&кощей # Так лучше :)




«UnicodeDecodeError» обычно есть свидетельством того, что нужно декодировать строку в юникод, используя правильную кодировку.

Теперь
использование «str» и юникод-строк. Не используйте «str» и юникод строки :) В «str» нет возможности указать кодировку, соответственно кодировка по умолчанию будет использоваться всегда и любые символы > 128 будут приводить к ошибке. Используйте метод «encode»:

>>> print type(s), s

<type 'unicode'> кощей

>>> str(s)


Traceback (most recent call last):

File "<pyshell#90>", line 1, in <module>

str(s)

UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-4: ordinal not in range(128)

>>> s = s.encode('cp1251')

>>> print type(s), s

<type 'str'> кощей



«UnicodeEncodeError» — знак того, что нам нужно указать правильную кодировку во время превращения юникод-строки в обычную (или использовать второй параметр 'ignore'\'replace'\'xmlcharrefreplace' в методе «encode»).

Хочу ещё!


Хорошо, используем бабу-ягу из примера выше ещё раз:

>>> parser_result = u'баба-яга' #1

>>> parser_result

u'\xe1\xe0\xe1\xe0-\xff\xe3\xe0' #2

>>> print parser_result

áàáà-ÿãà #3

>>> print parser_result.encode('latin1') #4

баба-яга

>>> print parser_result.encode('latin1').decode('cp1251') #5

баба-яга

>>> print unicode('баба-яга', 'cp1251') #6

баба-яга



Пример не совсем простой, но тут есть всё (ну или почти всё). Что здесь происходит:

Что имеем на входе? Байты, которые IDLE передает интерпретатору. Что нужно на выходе? Юникод, то есть символы. Осталось байты превратить в символы — но ведь надо кодировку, правда? Какая кодировка будет использована? Смотрим дальше.

Здесь важной момент:

>>> 'баба-яга'

'\xe1\xe0\xe1\xe0-\xff\xe3\xe0'

>>> u'\u00e1\u00e0\u00e1\u00e0-\u00ff\u00e3\u00e0' == u'\xe1\xe0\xe1\xe0-\xff\xe3\xe0'

True


как видим, Пайтон не заморачивается с выбором кодировки — байты просто превращаются в юникод-поинты:

>>> ord('а')

224

>>> ord(u'а')

224


Только вот проблема — 224-ый символ в cp1251 (кодировка, которая используется интерпретатором) совсем не тот, что 224 в Юникоде. Именно из-за этого получаем кракозябры при попытке напечатать нашу юникод-строку.

Как помочь бабе? Оказывается, что первые 256 символов Юникода те же, что и в кодировке ISO-8859-1\latin1, соответственно, если используем её для кодировки юникод-строки, получим те байты, которые вводили сами (кому интересно — Objects/unicodeobject.c, ищем определение функции «unicode_encode_ucs1»):

>>> parser_result.encode('latin1')

'\xe1\xe0\xe1\xe0-\xff\xe3\xe0'


Как же получить бабу в юникоде? Надо указать, какую кодировку использовать:

>>> parser_result.encode('latin1').decode('cp1251')

u'\u0431\u0430\u0431\u0430-\u044f\u0433\u0430'


Способ с пункта #5 конечно не ахти, намного удобней использовать использовать built-in unicode.

На самом деле не всё так плохо с «u''» литералами, поскольку проблема возникает только в консоле. Ведь в случае использования non-ascii символов в исходном файле Пайтон будет настаивать на использовании заголовка типа "# -*- coding: -*-" (PEP 0263), и юникод-строки будут использовать правильную кодировку.

Есть ещё способ использования «u''» для представления, например, кириллицы, и при этом не указывать кодировку или нечитабельные юникод-поинты (то есть «u'\u1234'»). Способ не совсем удобный, но интересный — использовать unicode entity codes:

>>> s = u'\N{CYRILLIC SMALL LETTER KA}\N{CYRILLIC SMALL LETTER O}\N{CYRILLIC SMALL LETTER SHCHA}\N{CYRILLIC SMALL LETTER IE}\N{CYRILLIC SMALL LETTER SHORT I}'

>>> print s

кощей




Ну и вроде всё. Основные советы — не путать «encode»\«decode» и понимать различия между байтами и символами.

Python 3


Здесь без кода, ибо опыта нет. Свидетели утверждают, что там всё значительно проще и веселее. Кто возьмется на кошках продемонстрировать различия между здесь (Python 2.x) и там (Python 3.x) — респект и уважуха.

Полезно


Раз уж мы о кодировках, порекомендую ресурс, который время-от-времени помогает побороть кракозябры — 
http://2cyr.com/decode/?lang=ru .

Ещё раз линк на статью Спольски — 
The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets .

Unicode HOWTO  — официальный документ о том где, как и зачем Юникод в Python 2.x.

Спасибо за внимание. Буду благодарен за замечания в приват.

 
MyTetra Share v.0.58
Яндекс индекс цитирования