Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Bregor/d9416c4cf7e443ddef8adbcafd507363 to your computer and use it in GitHub Desktop.
Save Bregor/d9416c4cf7e443ddef8adbcafd507363 to your computer and use it in GitHub Desktop.
Композиция, нормализация и уроды

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

Байт, кодпойнт, глиф

Юникод - многобайтовый способ кодирования текста. Текст состоит из codepoints (кодовых позиций), все позиции присутствуют в каталоге символов Unicode. Кодпойнты включают базовые компоненты графем и графемы в целом. При этом:

Каждый кодпойнт можно выразить в байтовом виде как минимум 5 разными способами

Один из них - UTF-8, в котором все латинские буквы заменены на однобайтовые ASCII-эквиваленты. Другие варианты - UTF-16 и UTF-32. UTF-16 - стандартный способ хранения Unicode-строк в операционных системах. InDesign импортирует тексты именно в UTF-16 например.

Каждый кодпойнт (в нашем понимании - значимая часть буквы) может содержать как часть графемы, так и графему целиком. В документах называется Codepoint.

Каждый глиф (в нашем понимании - буква, знакоместо) может состоять как из одного кодпойнта, так и из нескольких. Глиф в документах Unicode называется кластером графем.

Причем надо очень хорошо себе представлять, когда вам нужно работать с байтами, когда с кодпойнтами, а когда с глифами! И чаще всего вы будете выяснять, что работать вам надо именно с глифами (а софт, который вы используете, работает с кодпойнтами - или, что уж совсем клинично, с байтами).

Как, спросите вы, кодпойнты "собираются" в то что мы называем "буквами"? Ответ - они компонуются.

Компоновка и нормализация

То, что в Юникоде конкретные "буквы" несколько эвфемерны - полезно показать на наглядных примерах. Дело в том, что выглядящая одинаково Unicode-строка может быть построена совершенно по-разному. Более того - UTF-16 тоже не fixed-width (по два байта на символ), потому как добавлены суррогатные пары.

Для игрищ вам понадобится Ruby на нормальном не-Windows компьютере (потому что можно ли на Windows-машине скомпилировать Unicode Gem я не знаю и знать не хочу). и Unicode-gem. Поставить его можно как gem install unicode или, если вы на Windows, взять его в бинарном виде здесь. Помимо этого мы воспользуемся String#unpack и Array#pack. String#unpack с "шалбоном" U* распаковывают строку на Unicode-кодпойнты, обозначенные целыми числами. Array#pack с тем же шаблоном пакует массив целых чисел в строку как Unicode-кодпойнты (по целочисленным идентификаторам в десятичном исчислении).

Чтобы понять, что вам выдает unpack, воспользуйтесь например Unicode Checker для MacOS X или charmap в Windows. Интересующий вас вариант - decimal. Для новичков пока буду указывать названия кодпойнтов.

>> require 'rubygems'
=> true
>> require_gem 'unicode'
=> true
>> $KCODE = 'u'
=> 'u'
>> "å".unpack("U*") # на MacOS этот символ вводится как Option+a
=> [229] ## LATIN SMALL LETTER A WITH RING ABOVE
>> Unicode.decompose("å").unpack("U*")
=> [97, 778] ## LATIN SMALL LETTER A + COMBINING RING ABOVE

Ага? То есть одна и та же буква может быть представлена двумя способами, но остается при этом одним глифом. Более того, в 90 процентах случаев вы ну никак не захотите третировать отдельно-висящий кружок от Å как отдельную букву (то есть глиф)! Например, если обрабатываемый текст находится в "раскомпонованном" виде, то при его "перевертывании" мы получим:

>> Unicode.decompose("å").unpack("U*").reverse.pack("U*")
=> "̊a" 

То есть акцент над "а" станет отдельной буковкой. А это, дорогие друзья, никуда не годится.

Более того, вы думали только французов, датчан и исландцев может интересовать Unicode composition? Выпейте лучше йаду.

>> Unicode.decompose("ё").unpack("U*")
=> [1077, 776] ## CYRILLIC SMALL LETTER E + COMBINING DIAERESIS
>> Unicode.decompose("й").unpack("U*")
=> [1080, 774]  ## CYRILLIC SMALL LETTER I + COMBINING BREVE

Понимаете, к чему я клоню? Да, к тому самому. При попытке decomposed-текст разбивать по code points мы чудесным образом можем "отрезать точечки от Ё" и "оторвать ушки й". Поэтому и создана Unicode-нормализация

>> Unicode.normalize_D("й").unpack("U*")
=> [1081]  ## CYRILLIC SMALL LETTER SHORT I

Но. с другой стороны, есть лигатуры - например ff и ffi. А в форме D они останутся одним символом. Для программы InDesign, немедленно подставляющей прекрасную ffi-лигатуру в нашу верстку, это прекрасно. А вот для наших нескромных нужд это кошмар. Более того, никто (в здравом уме) не будет искать в тексте лигатуру, а искать будут три буквы ffi, равно как никто отдельно не будет искать диакритику без буквы-носителя.

Следовательно использовать надо NFKC:

>> Unicode.normalize_KC("й").unpack("U*")
=> [1081]  ## CYRILLIC SMALL LETTER SHORT I

Из чего следует: Перед обработкой текста как "ввода данных" его крайне желательно нормализовывать, в формы KC или D. При токенизации для поиска это вообще жизненно необходимо.

Об этом надо помнить всегда - потому что string.reverse и string.characters_in_range(from_letter=3, to_letter=5), как видите, штуки совершенно нетривиальные. Кстати, если вы до сих пор тешите себя иллюзией, что "прозрачные симметричные транслиты" в принципе возможны и еще хоть кому-то нужны, снимите штаны и высеките себя больно.

И обращаться с Юникод-строками побайтово (как это можно себе позволить в windows-1251) нельзя ни в коем случае! Посмотрим, как разработчики решают эту проблему.

Уроды, дубль 1

Итак, что сделали в Python и Perl, чтобы решить ту проблему (что с мультибайтными строками надо обращаться иначе, чем с однобайтовыми?).

Правильно, как господа с (мать их так) historical reasons, они взяли да сделали ДВА вида строк (беря пример с чудовищ - то есть с char и wchar). В случае Perl строка помечается специальным "флажком" (глаясщим что "тут живет юникод"), в случае Python - вместо string создается ustring. И вроде как хорошая идея. Только результат ее - пыльные руины (а ведь их предупреждали). Почему это плохо, спросит меня наивный читатель? Ответ прост. Все разработчики плюют на дополнительное. Это значит, что драйвер MySQL для python без особой настройки (и драйвер MySQL для Perl ВООБЩЕ всегда) просто напросто наплюет на все ваши старания и вернет вам те самые строки, которые - ну вы поняли - обычные. Которые не wchar. Это значит что библиотека, которая сама читает для Вас файл наплюет на то, что надо его сначала раскодировать. Это значит что библиотека, выводящая на экран шрифты, ну отнюдь не подумает что неплохо бы ожидать от вас Unicode-строки. Неплохо да?

Это весьма практично придумано, таким образом можно дальше прятать голоу в песок тухлых historical reasons и разработчики этих библиотек (как истинные "меня ниибет"-people) отстраняются от любых претензии публики, которая юникодом не пользуется - для них ничего не меняется (и даже - mea culpa - не надо править ни строчки исходников). А вот у всех остальных (то есть у нас, бедных 5 миллиардов непонятно кого) возникает имплицитная дрель в одном месте. Потому что выясняется что (Готичную Органную музыку включить по вкусу):

Каждую Строку Обрабатываемую Вашей Программой Надо Проверять На Флаг Юникода

И наступает Боль. Если то что кроется под этой ссылкой можно назвать иначе -- не верю. Это боль. Заметьте, кстати, как Питон научил человека писать все со строчной буквы...

Причем, заметьте - это нужно делать не просто абы как, а всегда и везде. Потому что вы не можете просто напросто склеить две строки в своей библиотеке, перед этой операцией вы обязаны проверить биты у каждой из строк, участвующей в операции (сколько уходит циклов? не лучше ли было не экономить на спичках?). Если раньше мы имели один элементарный тип (String или str или char или как он там зовется), то теперь - сюрприз - у нас их два (а имплицитно - три, когда закодированная неким образом строка сидит внутри простой). Более того, в связи с фундаментальными различиями этих типов мы обязаны приводить их друг к другу явным образом, каждый goddamn fucking time когда с ними надо что-то сделать. Ура языку C - более прямолинейной трансплантации Плохого в скриптинг представить себе нельзя.

Двойка, порка и расстрел. Господа питонисты вроде как шевелятся в направлении PEP 332 но когда и как он станет реальностью - неизвестно.

Да, как показывает практика - Гидо ван Россум редиска (это чтобы матом не покрыть).

Face it. Unicode stinks (from the programmer's POV). But we'll have to live with it.

Face it. Programmers who don't like Unicode stink. And we won't have to live with it, as soon as all the programmers are Indian, Russian and Chinese.

Guido, schaam je niet?

Уроды, дубль 2

Уроды второго порядка занятные люди - с ними симпатично беседовать потому что они прячутся за занавесью UTF-8.

Как мы я написал в начале статьи, в UTF-8 латиница кодируется точно как в ASCII, то есть на одну латинскую букву выделяется один байт. Что автоматически ведет к одному занятному факту.

Это позволяет разработчикам, использующим ограниченный ASCII, удобно НЕ ЗАМЕЧАТЬ тот чудовищный багодром который они организуют "вслепую" обрабатывая строки как UTF-8 в байтовом представлении.


Например, Лукас Карлсон дает полезный совет на своем блоге. Почему он советует мне сделать require 'jcode' и тд, и не подозревает что это не работает? Ответ прост. Для его буковок это работает, хоть они и в UTF-8! И пока он не напряжется и не соблюдет ДВА условия:

  • В тексте должен быть многобайтовый Юникодный символ
  • Байты которого должны попадать как раз на границу диапазона

которые еще должны совпасть, бага он не заметит!

А теперь 20 Rails-разработчиков пойдут на его сайт и прочитают полезный тип. А потом у меня в Basecamp (или где-то еще) появится вопросительный знак на месте буквы Я и сдохнут все RSS-фиды (потому что такая строка-результат автоматически инвалидирует любой XML, и при передаче ее в веб-сервисы парсер на другой стороне провода вполне может кинуть очень глубокий и злобный exception). А потом я иду и вижу целый пласт ActiveSupport, покоящийся на полностью убитом методе String#[].

Двойка, две порки (за хитрость) и расстрел. А фикс для убитого метода отправляется прямиком в мой плагин Unicode Hacks.

Уроды, дубль 3

Уроды третьего порядка - это японцы, китайцы и корейцы. Их очень напрягает Унификация Хан, потому что они придерживаются точки зрения, что каждый иероглиф должен иметь указанный язык, к которому он относится. И их (упс!) якобы обидели и не посоветовались. Ни одна нормальная не-японско-китайская_ кодировка, правда, этого не предусматривает - но их, как истинных любителей Своего Пути (см. русский Свой Путь) этот сюжет не устраивает.


А поскольку им не нравится Унификация Хан, они продолжают использовать Shift-JIS, а каждому, говорящему о Юникоде, плюют в лицо и надменно (по японски) говорят "Ты ничего не понимаешь, идиот, выучи вон иероглифов с наше - и тогда разглагольствуй". Наглядно доказано, что этим перфекционизмом японцы способны задавить любую дискуссию в любой рассылке и на любом форуме (потому что от одного слова "иероглиф" воспитанный немецко-американский программист бежит в кусты и дрожит там со снятыми штанами).

Оппоненты Уификации Хан утверждают, что она подавляет тысячи лет культурной традиции, опускает массу тонкостей, являющихся важнейшими свойствами этих языков, и делает серьезное литературное исследование на этих языках невозможным

Конечно, блин. Куда там нам с нашими червиным желанием нормально буквы с первой по пятую показать на экране? Это же тысячи лет, аспада, тысячи!

Некоторое количество проблем описано тут

То есть в принципе Россия могла бы весьма плезирно блокировать юникод с той же точки зрения (потому что он не включает Мефодиевскую кириллицу).


И пока у них не случится (например) новый император Хирохито, который в заставит, ситуация останется плачевной - потому как информация в JIS плодится и множится - а информацию, которая уже есть, конвертить во что-то еще очень не любят.

Какие последствия эта тупость имеет в Ruby - где до сих пор вроде бы даже создатель языка не знает, как он разрешит абсолютную не-юникодность программы -- можно догадаться. Я подозреваю, что есть 90% вероятность что в Ruby 2.0 все останется так-же кошмарно как и сейчас, просто потому что всем этим господам не вдолбили.

А в чем все дело? Аааа.. ну конечно...

For example, I understand that one of the top 5 politicians in China has a name which does not appear in Unicode. This makes life hard for journalists and political scholars writing in Chinese.

Видать пока не скончается, да-с... Заметьте - треду четыре года, а Unicode-поезд в Ruby (из-за которого сыр-бор) и ныне там.

Кстати, скорее всего это происходит от невозможности китайцев и японцев договориться друг с другом (но это мои инсинуации).

Не уроды

И только одни люди поступили единственно возможным образом, отрезав уродам вышестоящих типов все пути к отступлению в туманный отказ. Они устроили в Java такой Освенцим, что остается только восхищаться и трепетать. Им, слава Б-у, стало ясно, что если всю эту радость не запихать американским программерам ректально и эксплицитно, бардак из wchar и char продолжится до 3000 года.

Да, увы, их UTF-16 включает только Basic Multilingual Plane (не хватает пары десятков тысяч Хан-кодпойнтов), но по сравнению с вышестоящим убожеством это невероятный прогресс.

Их правила гласят:

  • Любая строка - в UTF16, без исключений, допущений и позволений. Кто исключил, допустил и позволил расстреливается в саду.
  • ByteArray принять за String нельзя, потому что это две разные вещи (String не наследует от ByteArray).
  • Хотите читать другую кодировку - раскодируйте ее известным вам способом сами при получении из байтов. Не раскодировали - расстрел в саду.
  • Хотите писать в файл или сокет - кодируйте строку при записи нужным вам способом сами в поток байтов. Не закодировали - расстрел в саду.
  • Хотите получить строку от другой библиотеки, или передать ее - о кодировке никто не думает.

Большое персональное спасибо тем кто это придумал. Потому что такое положение вещей заставляет американизированных дебилов считаться с оставшимися 5 миллиардами человек, заставляет их не экономить байты на спичках и хранить юникод в нужном виде, и заставляет их видеть говно в своем терминале, когда они делают это не так (не маскриуясь за UTF-8). Потому что пока Ражалпраграмов Наджави не заставить делать все в юникоде этот бесконечный ночной кошмар с "программа не пишет по-русски" будет продолжаться вечно.

Шестерка пехепе

В PHP6 этому бардаку тоже решили положить конец.

  • Native Unicode strings
  • A clear separation between Binary, Native (Encoded) Strings and Unicode Strings
  • UTF­16 as internal encoding
  • All functions and operators work on Normalized Composed Characters (NFC)
  • All identifiers can contain Unicode characters
  • Internationalization is explicit, not implicit

Но кажется и они хотят присоединиться к оравам Уродов Типа 1.

  • string: Native string, encoded with the current script's encoding. For backwards compatibility.
  • unicode: Strings, internally encoded in UTF­16.

В переводе это значит любая библиотека на планете земля будет использовать НЕ-юникодный string. Другое дело что:

  • Runtime Encoding - Determines which encoding to attach to Native Strings

Главное чтобы ее не скрыли и не урезали и не задушили в подвале как sys.setdefaultencoding в Python.

Если вы не поняли - да, я, носитель подхода, обратного традиционному Software Development, предпочитаю когда все ломается нафиг но потом работает как положено. Потому что иначе любой бред продолжается, бизнесмены и академики вроде Джоелов Спольски и прочих Реймондов Ченов празднуют победу, а юзеры плачут в гараже. Такой-вот я opinionated.

Кстати заметьте - вворачивание Юникода происходит методом вставления И Еще Пятидесяти Функций Без Общего Префикса, что говорит много и хорошо о дизайн-стороне PHP core team.

Upd. Да, я хочу чтобы у rails-core зачесалось.


И если все это -- не работа пидоров с Бетельгейзе, сходная по масштабу с саботажем Вавилонской башни, то я не знаю что это. Погодите, они еще будут жаловаться что PHP 6 потяжелел на 8 мегабайт из-за ICU, вот увидите.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment