Пользователь
0,0
рейтинг
17 января 2012 в 18:51

Разработка → Юникод для чайников

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


Зачем?


Главный вопрос новичка, который встречается с впечатляющим количеством кодировок и на первый взгляд запутанными механизмами работы с ними (например, в 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
баба-яга

Пример не совсем простой, но тут есть всё (ну или почти всё). Что здесь происходит:
  1. Что имеем на входе? Байты, которые IDLE передает интерпретатору. Что нужно на выходе? Юникод, то есть символы. Осталось байты превратить в символы — но ведь надо кодировку, правда? Какая кодировка будет использована? Смотрим дальше.
  2. Здесь важной момент:
    >>> 'баба-яга'
    '\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
    
  3. Только вот проблема — 224-ый символ в cp1251 (кодировка, которая используется интерпретатором) совсем не тот, что 224 в Юникоде. Именно из-за этого получаем кракозябры при попытке напечатать нашу юникод-строку.
  4. Как помочь бабе? Оказывается, что первые 256 символов Юникода те же, что и в кодировке ISO-8859-1\latin1, соответственно, если используем её для кодировки юникод-строки, получим те байты, которые вводили сами (кому интересно — Objects/unicodeobject.c, ищем определение функции «unicode_encode_ucs1»):
    >>> parser_result.encode('latin1')
    '\xe1\xe0\xe1\xe0-\xff\xe3\xe0'
    
  5. Как же получить бабу в юникоде? Надо указать, какую кодировку использовать:
    >>> parser_result.encode('latin1').decode('cp1251')
    u'\u0431\u0430\u0431\u0430-\u044f\u0433\u0430'
    
  6. Способ с пункта #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.

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

P.S. Подкинули линк на перевод Спольски — Абсолютный Минимум, который Каждый Разработчик Программного Обеспечения Обязательно Должен Знать о Unicode и Наборах Символов.
Роман @leron
карма
75,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (53)

  • +7
    Годная, полезная статья.
    Я бы еще раскрыл тему как читать/писать юникодные файлы: codecs.open вместо file() и про PEP 0263.
    • –1
      по поводу файлов

      #! /usr/bin/env python
      # -*- coding: utf-8 -*-

      операции с файлами становятся уникодными=)

      ну еще со строками магия проходит вроде
      регулярки работают
      m=re.match('^(第)([0-9]+)(届|期)',s)

      Как практика показывает, входные данные лучше отгонять в UTF-8
      тогда проблемы вроде того что одни и теже данные python, c++ и так далее не сильно актуальные…
      Отдельно надо выделить если происходят операции за Базой данных тут кодировки тоже важно…

      З.Ы каждый программист должен начинать новый язык с UTF-8 / Unicode :) решать вопрос чтобы устойчивая программа была к любым символам
      • +2
        Не операции становятся уникодными, а исходный текст скрипта :).
        Файлы [данных] все же придется читать и декодировать руками.

        > входные данные лучше отгонять в UTF-8

        Ни в коем случае! UTF-8 — это внешнее представление. Входные данные надо отгонять в юникод, со всеми строками работать только в юникоде, а сохранять снова в UTF-8. Кстати, в C++ c UTF-8 вообще можно повеситься (ну или забыть работу со строками).
        • 0
          Насчет входных файлов соглашусь…

          У меня просто все данные в UTF-8, входные из файлов поэтому работает 2.6/2.7
          Внешение представление в UTF-8 спасает…

          UTF-8 для символов a-z 1byte если идет кирилица А_Я и так далее 2 byte.

          С точки зрения C/C++ когда strlen(ansi) = 10 с точки зрения strlen(utf-8) 10 длина не понятна(может символы 1 +2 +2 +2 +1+1 ).

          Если код не ориентирован на чёткий поиск, а склейка строк то strcpy/strcat можно пользовать…

          з.ы. пора тему Unicode / UTF-8/ UTF-16 как курс или пособие, обобщение для разных языков…
  • –44
    Чё за бред? Хабр — УГ.
    • +2
      Толсто.
    • +3
      Терпите. Скоро вас запикает НЛО.
    • 0
      Что полезного сделали вы для хабрсообщества, чтоб его критиковать?
    • +3
      Хабрасэппуку?
    • 0
      Это вы зря. Мне недавно пришла идея автоматического пополнения словаря с использованием нейронной сети. Идея не нова, просто самообучался. Столкнулся с проблемой кодировки, перерыл полинтернета. Нашел ответы на вопросы, но все очень разрозненно, плюс умные люди помогли, теория все равно осталась мне не понятна. А это фактически единственная статья в рунете, которая все раскладывает по полочкам.
  • 0
    Непонятно, чем «символы» отличаются от байтов. В конечном счете они все равно хранятся как любая другая информация в памяти — как байты. Поясните разницу.
    • +5
      Символ — это, например, «CYRILLIC SMALL LETTER A», а байт — это нолики и еденицы, например '\xe0'. Для того, что б байт стал символом, необходима кодировка. И в зависимоти от кодировки, этот байт (или байты) может представлять разные символы.
    • 0
      Определение одного символа не обязательно умещается в один байт.
      • 0
        Я и сказал «чем символы отличаются от байтов?» а не «чем символ отличается от байта?».
        Я понимаю, что символ может быть больше байта, я не понимаю вот это «не обязательно».
        То есть в юникоде разные символы имеют разный размер? Вообще что можно почитать по этому поводу. поменьше и попроще, чем стандарт?
        • +3
          В Unicode определяются code points. Но code point — это число + название, он не задаёт байты. Для того, чтобы преобразовать последовательность code points в байты, вам нужно выбрать некоторый Unicode Transfotmation Format (UTF) и закодировать в соответствии с его правилами.
          • 0
            Да, но число — разве оно и не есть эти самые байты? А названия — разве не однозначно соответствуют числам?
            • +1
              Нет, число — это просто целое число. Оно может быть закодировано в виде байт (причём правила различны в зависимости от UTF, и ещё и не всеми битами этих байтов).
  • 0
    Один символ может занимать несколько байт.
  • 0
    Похоже что косяки машинного перевода немного накладывает отпечаток…
    ...«Хорошо, я понял чем есть строка.»…

    А так по логике вещей показалось странным, что:
    байты в Unicode — это кодирование
    а Unicode в батый — это де-кодирование

    интуитивно хочется наоборот… хотя смысл, понятен, и зависит от того что-во-что кодировать и следовательно де-кодировать
    • +1
      А мне именно такая логика кажется естественной.
      У символа есть «код», мы получаем код из символа декодированием оного.
    • +1
      Нет, это просто стиль такой выбран. А вобще автор совсем не русско-говорящий :)

      >> интуитивно хочется наоборот

      дык на самом деле и есть наоборот — байты в Unicode — это декодирование.
    • +1
      "string".decode('ascii') # декодируем в  юникод
      u"string".encode('ascii') # переводим юникод в кодировку ascii


      Все как раз наоборот
  • +1
    >>
    На данный момент в Юникод-стандарте есть немного более 100 тысяч символов, тогда как UTF-16 позволяет поддерживать более одного миллиона (UTF-8 — и того больше).


    На сколько я понимаю, UTF-16 может кодировать максимум 65 535 символов. Возможно, вы ошиблись?
    • +3
      Тогда UTF-8 кодирует 255 символов?

      Нет. «16» в UTF-16 означает размер используемого слова: каждый code point в UTF-16 кодируется одним или более 16-битными словами.
      • 0
        Да, вы правы. Принцип работы UTF-16 аналогичен UTF-8 с разницей лишь в размере слова.

        До этого момента я свято верил в то, что wchar_t в Windows — это UTF-16. Оказывается, это UCS-2 — строго 16-битная кодировка без возможности расширения до 32 бит.
    • 0
      16 в названии совсем не значит, что используются 16 бит\2 байта\65 535 доступных комбинаций. Деталей кодировки не знаю, число взял сами знаете откуда. Посмотреть детали думаю можно там же (пока не закрыли в знак протеста против SOPA:) )
    • +2
      Согласно Вики и в UTF-8 и в UTF-16 равное число возможных символов — 1,112,064
      • +3
        Совершенно верно. Но можно расширить по аналогии правила построения UTF-8 на более длинные последовательности и получить так называемые overlong sequences. Хотя в стандарте прямо написано, что они ошибочны, к сожалению некоторые «хакеры» считают, что они выше стандарта и говорят, что в UTF-8 больше кодов, чем написано в стандарте, да ещё и реализуют их поддержку в своих программах (например, в eglibc bugs.debian.org/cgi-bin/bugreport.cgi?bug=555922 ).

        И да, не называйте юникодные code points символами. Не каждый code point можно нарисовать в виде символа и даже есть такие code points, которые называются noncharacters. См. также en.wikipedia.org/wiki/Mapping_of_Unicode_characters
        • +1
          ммм… Code point. Пасиб, проще с такой терминологией работать.
          Какой-то русский аналог у этого названия есть?
          • 0
            Не слышал, но возможно «кодовая позиция Unicode»?
        • +1
          прочитал ссылку на баг в eglibc. мдааа…
          Ещё один кейс на проверку в программах (я тестировщик). Правда не очень понятно кого винить потом проблеме — своих разработчиков, или разработчиков билиотеки которую они использовали.
          Особенно «порадовал» комментарий — «Nobody has ever shown any evidence why this is a bad idea.»
          Стандарты видимо люди тоже просто так придумывали…
  • +7
    > в тоже время в кодировке ISO-8859-1 это греческая "ß".
    В ISO-8859-1 нет греческих букв. Это немецкая Eszett. (на правах занудства)
  • 0
    Спасибо за занудство:)
  • 0
    Хорошая статья, советую еще раскрыть тему локалей, без этого материал неполный.

    Жаль что не смотря на теорию проблема кодировок это сверхбольное место питона, которое каждый раз съедает у меня массу сил и времени. Заткнулся какой ни будь встроенный модуль для работы с gzip на русскоязычных именах файлах и рабочий день коту под хвост.

    В python 3000 демонстрировать особо нечего, для самого питона проблема решена на корню как в java, все строки — юникод, на практике ад с портированием библиотек и, имхо, эта ветка сдохнет и на ее место доэволюционирует 2.x, х/з как при этом решится проблема со строками.
    • +1
      Помер как раз 2.х
      3.3 выкатят к осени.
      Трудности с портированием есть, но «адом» я их не назову.
  • +2
    Мне очень понравилась тема Unicode в Dive into Python 3
    diveintopython3.ep.io/strings.html
  • –1
    Честно говоря, до этой статьи я понимал что такое кодировки и как с ними работать.
    Очень тяжелая статья для новичков, которая лишь внесет кашу в головы.
  • 0
    Увидел сначала сколько букв — испугался.
    Когда осилил, понял, что не так уж и много. Читается нормально.
    Полезная статья!
  • +1
    В вводной забыли про EBCDIC, они с ASCII практически ровесники.
  • 0
    Нужная статья, основная полезность направление decode / encode.
    Некоторая мнемоника:
    1. Для себя сделал ассоциации типа decode более тяжелое по звучанию соответственно создает тяжелый utf,
    encode легкое создает легковесный байт.
    2. Байты это непонятный код, который чтобы увидеть надо декодировать в символы (utf), а чтобы символы превратить в код (т.е. байты), их соответственно нужно закодировать (encode).
  • 0
    На виндах у петона вечные проблемы с юникодом, что у третьего, что у второго. На линуксе с этим проще.
    • 0
      Да и не только с юникодом.
      Да, я по это причине все пытаюсь переползти на макось но пока не складывается.
    • +2
      А чем проблемность юникода в Windows вызвана?
      • –2
        видимо тем, что родная для винды CP1251
    • 0
      не волнуйтесь, в линуксе тоже есть вечные проблемы с вводом/выводом изнутри библиотек.
      например, stderr направленный в файл — работает, а на консоль — падает, а на appengine — генерит servererror.
  • 0
    Правильно ли я понимаю, что если везде и всюду в константах и на входе программы будет только юникод (вместо байтстрок) — это избавит от проблем вида «UnicodeDecodeError» в крайне неожиданных местах?
    (например, от лютых кабздецов, когда appengine-приложение падает в «server error» из-за того, что логгер не может нарисовать лог.)
  • 0
    А в каком формате сам python внутри хранит строки в 2.х и 3.х?
    • 0
      Строки (str)? Естественно, байты char[]
      Unicode — платформозависимо, обычно UCS-2(4)
      Хотя что понимать под «хранит внутри» — я о памяти запущенной программы.
  • 0
    Забавно, вот что по ссылке на перевод статьи Спольски :)
  • 0
    картинка для предыдущего комментария img.skitch.com/20120320-mb9qxs4tukfin98ria2wb7rjt3.png
  • 0
    Меня спасает
    # -*- coding: utf-8 -*-
    from __future__ import unicode_literals
    


    Таким образом весь текст становится unicode, если не укажете что он b'текст'

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.