YA IT's blog

ЙЯитцБлог by @nordicdyno

Unicode. Ликбез

| Comments

Текст выросший из моего доклада Unicode-ликбез на Санкт-Петербугском воркшопе SaintPerl 2011.

Вступление

Хочу предупредить, что этот рассказ не о интернационализации или локализации, хотя это и очень близкие темы. Еще я не буду рассказывать об истории кодировок (почти). Или о том, как надо правильно работать с Unicode в Perl или каком-либо другом языке. Я хочу рассказать о том, что просто необходимо разработчику знать о Unicode и почему.

объяснение названия

Ликбез (ликвидация безграмотности) – в переносном смысле — обучение неподготовленной аудитории базовым понятиям какой-либо науки, процесса или явления.

В нашем случае аудитория вполне подготовленная. И, казалось бы, что такого можно рассказать о Unicode разработчикам? Все наверняка слышали про кодировки (мы не из ASCII-мира), и разве Unicode – это не просто еще одна расширенная универсальная кодировка, призванная заменить все предыдущие?

Честно говоря, я примерно так и воспринимал Unicode. Потому что это просто - так думать! Но такое упрощенное понимание неверно и может приводить к неожиданным ошибкам в коде и непониманию происходящего при обработке текстов, вне зависимости от используемых инструментов: редактор ли это текстов или язык программирования.

Во многих случаях упрощение – это неизбежность, т.к. невозможно (и не нужно) знать все. Но для программиста незнание или непонимание Unicode – серьезный изъян в профессиональных навыках.

К необходимости знания и понимания Unicode я пришел, основываясь на собственном опыте и ликбез относится ко мне не в последнюю очередь.

Я разделил свой доклад на три части.

Часть 1 «Задачка»

В том, что Unicode - это не просто кодировка, я убедился, когда попробовал решить простую на первый взгляд задачку:

найти в слове

Îñţérñåţîöñåļîžåţîöñ 

подстроку

Nation

Выглядит довольно просто. Да, в исходном слове (реально не существующем, кстати) есть какие-то крышечки, но наверняка их можно убрать и найти нужную подстроку регулярным выражением – ведь регекспы в Perl могут все? Но как это сделать, что и где искать в документации?

Для начала я задал вопрос на stackoverflow: http://stackoverflow.com/questions/7429964/how-to-match-string-with-diacritic-in-modern-perl/7440789#7440789, после чего приступил к самостоятельному исследованию.

Первый шаг был узнать, что хвостики и крышечки – это могут быть как диакритические знаки, так и знаки ударения (accent) и что-то еще. Затем, то что в регулярном выражении в Perl их можно искать/удалять с помощью \p{Marks}, предварительно выделив эти знаки (marks) из строки с помощью NFD-нормализации.

решение №1

variant 1
1
2
3
4
5
6
7
8
9
10
use utf8;
use Unicode::Normalize qw/ NFD /;
binmode STDOUT, ':encoding(UTF-8)';
my $str  = "Îñţérñåţîöñåļîžåţîöñ";
my $look = "Nation";
say "before: $str\n";
$str = NFD($str);
$str =~ s/\pM//og; # remove "marks"
say "after: $str";
say "is_match: ", $str =~ /$look/i || 0;

Довольно много новой информации и новых слов: “нормализация”, “диакритические знаки”. Я был доволен и опубликовал свое решение на stackoverflow. Но, когда, спустя некоторое время, я вернулся к своему посту (чтобы порадоваться растущему рейтингу), там меня озадачил такой комментарий:

This is the wrong way to do it. You need to use a UCA match at level 1

Итак, я узнал много новых слов при решении, но все равно сделал что-то неправильно! Хмм…

Я не имел никакого представления, что за зверь «UCA match at level 1» и в чем моя ошибка, но твердо решил, что должен с этим разобраться. Особенно, после того, как посмотрел в профиле кто такой tchrist. Это оказался, известный в Perl-коммьюнити разработчик Tom Christiansen, соавтор книг о Perl, один из разработчиков языка и (как оказалось позже) Unicode-эксперт.

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

решение №2

variant 2
1
2
3
4
5
6
7
8
9
10
use 5.014;
use utf8;
use Unicode::Collate;
my $str  = "Îñţérñåţîöñåļîžåţîöñ";
my $look = "Nation";
my $Collator = Unicode::Collate->new(
    normalization => undef, level => 1
);
my @match = $Collator->match($str, $look);
say matches:  . join(, , @match);

Кстати, вот еще одна цитата tchrist, найденная на stackoverflow:

«Code that assumes you can remove diacritics to get at base ASCII letters is evil, still, broken, brain-damaged, wrong, and justification for capital punishment»

рефлексия

В чем же была моя основная ошибка (или в чем заключалось невежество)? Основной ошибкой была попытка, не понимая сути, решить, как оказалось, нетривиальную задачу с помощью простого рецепта.

Рецепты – это неплохо. Это такая хорошая штука, которая позволят здорово экономить время. Например, готовка супа по рецепту отлично работает. И для многих случаев, с которыми приходится сталкиваться в программировании, рецепты тоже работают. Но это хорошо работает только для простых задач, а не для сложных! Например, для химических экспериментов необходимо понимание происходящих процессов и основ химии. Иначе может быть бум! (Придумайте еще примеры :) И для правильной обработки Unicode, также, просто необходимо знание основ.

Часть 2 «Ликбез»

Итак мы видим, что Unicode – это большая и сложная тема. И если мы хотим в ней разобраться, то с чего начать?

Первое, с чего я рекомендую начать изучение – это терминология. Важно начать именно с нее, так как знание терминологии помогает избежать путаницы и сохранить ясность мысли в процессе обучения. В сети очень много материалов о Unicode, и хотя в целом информация полезная и хорошая, но авторы очень часто небрежно используют термины (например, не делая различия между UCS-2 и UTF-16), в результате чего новичку (и не только!) очень легко запутаться или получить неверное представление.

Начем с того, что Unicode – это не кодировка или таблица символов. Это стандарт! В который входит, помимо таблиц символов и правил их кодирования, еще много-много чего.

Стандарт Unicode это:

  • таблицы символов
  • несколько механизмов кодирования
  • формы нормализации текста
  • правила casemapping и casefolding
  • гибкие правила collation
  • правила переносов для слов и разбиения строк
  • специальные правила для регулярных выражений
  • тысячи именованых свойств (properties)
  • численные эквивалентности (U+216B XII)
  • направление текста
  • многое другое

Важный момент: Unicode не отвечает за то, как отрисовываются символы, т.е. за их рендеринг (это делают шрифты и конечное ПО).

В Unicode можно указывать на направленность текста (но поддержка вертикальных текстов пока только в планах).

Стандарт имеет массу документов, разные версии (1.0 – October 1991, … 5.0 – July 2006, 6.0 – October 2010), собственную терминологию, в том числе множество TLA или ТБС (Three-letter acronyms или Трех Буквенных Сокращений).

Некоторые TLA (ТБС) описываются ниже.

UCS, таблицы символов

UCS (ucs-2, ucs-4) – универсальный набор символов (universal character set) задаёт однозначное соответствие символов кодам — элементам кодового пространства, представляющим неотрицательные целые числа. Определен в стандарте ISO/IEC 10646 (включен в стандарт Unicode)

UCS стандартизирует набор абстрактных символов, на данный момент примерно 100 тысяч (потенциально до 1,114,112 code points), каждый из которых имеет уникальное имя и числовое значение - кодовую точку (code point). Cейчас это часть стандарта Unicode. Стандарт включает наборы USC-2 (устаревший) и UCS-4.

Кодовое пространство (codespace) – в Unicode разбито на 17 плоскостей по 216 (65536) code points (кодовых точек), набор всех code points: 0hex to 10FFFFhex

Нулевая плоскость (BMP/Basic Multilingual Plane) – в ней расположены символы наиболее употребительных письменностей. Т.е. это code points в диапазоне 0x0 - 0xFFFF (не все используются, верхняя часть диапазона зарезервирована)

Часто встречается путаница в терминах, из-за того, что во многих системах реализованых в доюникодную эпоху (Symbyan, NT, CD_ROM, Python 2.x, Java < 7, JavaScript), используется устаревший UCS-2 для кодирования символов. И поэтому закодированный в UCS-2 текст часто называют юникодом, хотя правильнее было бы называть UCS-2 подмножеством Unicode. (программы работающие с UCS-2 не умеют полноценно обрабатывать Unicode, так как ничего не знают о стандарте)

Самый известный пример – современные реализации языка JavaScript, где строки хранятся в UCS-2 (символы там всегда шириной 2 байта и это оговорено стандартом языка). Причем сами JS-движки обычно внутри себя используют UTF-16.

Именно UCS используется при численной записи Unicode символов, которую многие видели.

u (U+0075 ʟᴀᴛɪɴ sᴍᴀʟʟ ʟᴇᴛᴛᴇʀ ᴜ) + ¨(U+0308 ᴄᴏᴍʙɪɴɪɴɢ ᴅɪᴀᴇʀᴇsɪs) => ü

эти же коды используются в современных ОС для ввода с клавиатуры

mac: unicode hex input (language & text), option + code
windows: alt + “+” + code

см. также:

В вебе есть базы данных символов юникод, где можно посмотреть, что кроме числового UCS-представления у этих символов существует множество свойств и способов кодирования, определенных стандартом.

UCS4 - основа всех современных unicode-кодировок.

Character General Category – каждый code point относится к одной из основных категорий: буква, знак (см диакр. знаки), число(number), пунктуационный знак, символ, разделитель (см. http://www.unicode.org/versions/Unicode6.0.0/ch02.pdf)

UTF, кодировка символов

UTF (Unicode transformation format) – семейство кодировок, которое определяет машинное представление последовательности кодов UCS. Существует 3 кодировки: UTF-8, UTF-16, UTF-32 и 6 способов закодировать code point-ы: UTF-8 (UTF-EBCDIC), UTF-16BE, UTF-16LE, UTF-32BE, UTF-32LE

BE и LE расшифровываются как big-endian (BE), дословно «тупоконечный» – порядок байт от старшего к младшему, и little-endian (LE), дословно «остроконечный» – порядок байт от младшего к старшему.

Если нет BOM (о нем ниже), то стандартом предписывается Big Endian. (Выбор BE и LE определяется архитектурой компьютера, для большинства машин (x86-совместимых) – это LE)

Все эти кодировки, кроме UTF-32 - переменной ширины!

Еще раз: UCS - таблица(-ы) code point-ов в стандарте, UTF - способ кодирования их в поток байт, которые можно сохранить в памяти/на накопителе или передать по сети.

BOM (byte order mark) – позволет понять при чтении, какая последовательность байт используется (LE или BE).

BOM важен для UTF-16 и UTF-32. Для UTF-8 обычно не нужен, но есть в стандарте. Подразумевается, что встретив незнакомый BOM, UTF-16,32 программы cмогут понять, что в файле utf-8.

BOM не нужен для правильной обработки UTF-8, но иметь в виду его необходимо, т.к. те же MS-программы, живущие в мире нескольких Unicode-кодировок (UTF-16 и UTF-8 как минимум), соблюдают это соглашение и сохраняют файлы в UTF-8 с BOM (Notepad и др.)

UTF-16 – особенность кодировки в том, что символы не из 0-й plane задаются составными кодами 16+16=32 бита, которые называются суррогатными парами

Для суррогатных пар зарезервированны значения, не задействованные в основном codespace

Софт работающий с ucs2/utf-16 зачастую имеет ошибки в обработке суррогатов, так как редко встречаются и софт редко тестируется на совместимость с ними

UTF-16 используется в Win 2000, Vista, .NET, MacOS X Cocoa, Python (до недавних версий)

Еще немного о неотображаемых code point-ах (суррогатах). Те, что в BMP-диапазоне, называются “low surrogates”, те, что выше – “high surrogates”. И они используются только в UTF-16! (основное отличие UTF-16 от UCS-2) Пары суррогатных code point соответствуют реально существующим codepoint-ам, выходящим за base plane. По отдельности они не имеют смысла.

http://en.wikipedia.org/wiki/Mapping_of_Unicode_characters

Не надо путать составные символы и суррогатные пары

UTF32 – кодировка фиксированной ширины, всегда 4 байта.

http://en.wikipedia.org/wiki/UTF-32/UCS-4

удобна в плане простоты, но редко используется из-за необходимости выделения 4 байт для каждого символа

UTF-8 – распространена в unix/web. Переменной ширины, кодовый символ может быть от 1 до 6 байт длиной, но не встречается больше 4 (2 оставлены про запас и врядли понадобятся в обозримом будущем).

Была изобретена 2 сентября 1992 года Кеном Томпсоном и Робом Пайком. Совместима с ASCII, если не выходить за границу 128 символов.

Самая “православная” кодировка. ☺

Хитро мапит кодепоинты в байты, ипользует часть битов для спец-целей (из первого байта можно узнать длину последовательности). Отсюда следует, что коды символов не совпадают с UCS.

Perl может внутри хранить строки как в UTF-8 так и в UTF-EBCDIC

Композиция символов – cимволы задающиеся несколькими кодами (составные символы) Для некоторых символов есть как композитные так и монолитные формы записи

Ё (U+0401) и Й (U+0419)
Е +  ̈ (U+0415 U+0308)
И +  ̆ (U+0418 U+0306)

термины, которые полезно знать и различать при изучении Unicode

Character – минимальный компонент письменного языка, имеющий семантическое значение. Ссылается на абстрактное значение (знак) или на символ

Символ (symbol) (из греч. σύμβολον) — знак, изображение какой-нибудь вещи или животного для означения качества предмета; условный знак каких-либо понятий, идей, явлений.

Графема (grapheme) (от греч. γράφω — пишу и -ема) — единица письменной речи (в алфавите — буква, в неалфавитных системах письма — слоговой знак, иероглиф, идеограмма и др.). Графема однозначно отличима от любой другой единицы этой же письменности.

Глиф (glypth) (греч. γλύφειν — резное письмо) — элемент письма, конкретное графическое представление графемы, иногда нескольких связанных графем (составной глиф), или только части графемы (например, диакритический знак).
И если графема - это единица текста, то глиф - единица графики.

Буква (Letter) – отдельный символ какого-либо алфавита, графема. Чаще всего буква соответствует звуку в устной речи, но это необязательно. У буквы может существовать несколько равнозначных вариантов написания, не меняющих её произношения и смысла.

Диакритические знаки (diacritical mark) - различные надстрочные, подстрочные, реже внутристрочные знаки

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

что еще описывается стандартом Unicode

Нормализация. Поскольку одни и те же символы можно представить различными кодами, что иногда затрудняет обработку, существуют процессы нормализации, предназначенные для приведения текста к определённому стандартному виду.

Case folding – приведение символа или строки к заданному регистру (у символов может быть от одного до трех возможных регистров).

UCA (Unicode Collation Algorithm) – алгоритм сравнения двух строк, с учетом особенностей Unicode.

ICU (International Components for Unicode) – не входит в стандарт, но полезно знать. ICU – набор C-библиотек для разработки программ с поддержкой Unicode.

для Python есть pyICU, в Perl5 своя реализация Unicode, поддерживаемая perl5 porters

Вообще-то это не все термины и понятия, но уже не мало?

здесь можно сделать небольшую передышку

Часть 3. «Домашняя работа»

Итак, мы теперь знаем, что Unicode это не просто расширенная таблица символов, не плаваем в терминах и понимаем, что Unicode – это совсем не просто. Что дальше?

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

Может возникнуть вопрос “Зачем учить?”. Простой ответ заключается втом, что для квалифицированного программиста в настоящее время знание хотя бы основ Unicode и того как в используемом языке, ОС реализована поддержка его стандарта – обязательное условие.

Известная статья Джоела, на эту же тему: Абсолютный минимум, который каждый разработчик программного обеспечения обязательно должен знать о Unicode и наборах символов, которая была написана в 2001 г., то есть более 10 лет(!) назад.

Незнание или, что еще хуже, поверхностное знание, может и не помешать написать вроде как правильно работающий код, но чревато тем, что он окажется неправильным в самый неожиданный и неподходящий момент.

Никто не гарантирует, что вы не столкнетесь с неправильной реализацией Unicode в сторонних библиотеках, и в таком случае хорошее знание темы может очень помочь в понимании того, что происходит.

И еще одна причина. Когда человек чего-то не знает или недопонимает, он это может считать магическим и волшебным (примеры: молния, Unicode, сборщик мусора виртуальной машины). Цитата, иллюстрирующая пример такого отношения из рассылки Moscow.pm:

«Все-таки UTF8 в перле - это немного черная магия»

Чтобы это не казалось магией – необходимо знание!

Если вопроса “Зачем учить” не возникает, то я попробую ответить на вопрос: “Как учить?”.

Хорошая новость в том, что есть множество ресурсов с тоннами информации по Unicode. Презентации, статьи – все есть в сети. Не очень хорошая новость – информации очень и очень много. Что же делать чтобы в ней не утонуть?

Мой совет – обратиться к авторитетным источникам! (cтандарт слишком большой и читать его скучно)

Во первых, прочитайте статью Джоэла (ссылка выше). Установите Unicode-шрифты (доступны в репозитории и по ссылкам в конце).

Прочитайте обзорную статью в Википедии

Обратитесь к публикациям Tom Christiansen. Обязательно ознакомьтесь с его презентациями с OSCON 2011, если вы еще не успели это сделать.

)ригиналы находятся на странице автора http://training.perl.com/OSCON2011/index.html но она, к сожалению, чаще не работает, чем работает ☠ ☠ ☠)

Если вы Perl-разработчик, то прочитайте документацию

Обратите внимание на ссылки внизу.

Ресурсы

документация и технические подробности

Wikipedia и официальная справка

статьи, публикации в блогах

материалы конференций

разное

Сайт с большой подборкой информации о локализации и интернационализации: http://www.i18nguy.com

тест правильной работы c UTF-8 в JSON-парзере: https://metacpan.org/source/MLEHMANN/JSON-XS-2.32/t/01_utf8.t

книги

для углубленного изучения, если есть интерес (спасибо Dmitry Arsentiev за ссылки):

Comments