Юникод для чайников
Сам я не очень люблю заголовки вроде «Покемоны в собственном соку для чайников\кастрюль\сковородок», но это кажется именно тот случай — говорить будем о базовых вещах, работа с которыми довольно часто приводить к купе набитых шишек и уйме потерянного времени вокруг вопроса — «Почему же оно не работает?». Если вы до сих пор боитесь и\или не понимаете Юникода — прошу под кат.
Зачем?
Главный вопрос новичка, который встречается с впечатляющим количеством кодировок и на первый взгляд запутанными механизмами работы с ними (например, в 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.
Спасибо за внимание. Буду благодарен за замечания в приват.