Это сайт — моя персональная записная книжка. Интересна мне, по большей части, история, своя жизнь и немного программирование.

I � Unicode

Давайте я вам про Unicode ещё раз расскажу?

Как известно, в памяти компьютера числа представлены битами, которые группируются в байты. Один байт может хранить одно из 256 значений (поскольку состоит из восьми бит, каждый из которых может хранить одно из двух). Следовательно, числа, значения которых > 255 хранятся в больше, чем одном байте.

Например, числа до 65535 можно уместить уже в двух байтах: в так называемом старшем записывается сколько раз полных 256 содержится в числе, а в младшем — остаток: старший × 256 + младший.

В зависимости от типа процессора, порядок, в котором записаны в памяти старшие и младшие байты, различается. Собственно, мне хорошо известны только две системы: младший записывается первым (как в процессорах Intel) и старший записывается первым (в процессорах ARM, которые стоят в смартфонах). Есть ещё смешанная, но с ней я не сталкивался. Системы эти носят имена: little-endian и big-endian (системы со смешанным порядком называются middle-endian и термин не указывает на то как именно «мешается» этот порядок). Краткая запись названий — LE и BE.

Есть ещё системы, которые умеют переключать порядок (те же ARM) и называются bi-endian.

Термины little-endian и big-endian пришли к нам из «Приключений Гулливера» и на русский переводятся как «тупоконечный» и «остроконечный». Те, кто читали, те помнят (война по поводу того с какой стороны разбивать яйца). Информатика тут какбэ намекает. Хотя у каждой системы есть свои достоинства и (не удержался) мне ближе LE.

Сюрприз для непрограммистов: буквы в памяти компьютера тоже представлены числом. Это просто номер по порядку в компьютерном алфавите. Так девочки в нашем классе «кодировали» записки: вместо букв ставили номер позиции в алфавите. В чём-то они были правы, но только не в том, что это шифр.

Когда-то компьютеры победивших сейчас систем использовали всего 256 символов и всем было хорошо — туда умещались все символы, которые присутствовали в том мире, где эти компьютеры создавались. Экспансия привела к тому, что 256 значений для символов перестало хватать.

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

Система кодирования Unicode, где выделяются два байта, называется UTF-16 (16 бит на символ), там где четыре байта — UTF-32 (32 бита). Название UCS-4 (четыре байта) является синонимом UTF-32, а UCS-2 (два байта) подмножеством UTF-16. UCS-2 отличается от UTF-16 отсутствием так называемых «суррогатных пар» (которые появились только в Unicode 2.0, вы не хотите знать что это) и является устаревшим стандартом, можете про него забыть.

Так как способов хранения чисел, не умещающихся в памяти у нас несколько, то системы подразделяются на UTF-16BE, UTF-16LE, UTF-32BE и UTF-32LE. Отсюда видно, что UCS-4LE это тоже, что и UTF-32LE. Если порядок байт не указан, то принято считать, что используется big-endian.

Первого апреля 2005-го года были предложены шуточные «стандарты» UTF-9 и UTF-18, отношения к рассматриваемой проблеме они имеют. Для телеграфа и прочего слоновьего гуано, разрабатывались UTF-5 и UTF-6, но о их судьбе мне ничего не известно. Так же есть UTF-7, который в стандарт не вошёл, но реально применяется (в модифицированном виде) внутри почтового протокола IMAP4, про него я рассказывать не буду, мне он стал известен из-за оригинального способа его использования для XSS-атак в IE (в частности, решением этой проблемы я занимался в PEAR PHP классе HTML_Safe). Можно упомянуть ещё UTF-1, но с ней я не сталкивался в работе.

BOM. BOM расшифровывается как «byte order mark» (признак порядка байт) и ставится внутри файлов упомянутых двух- и четырёхбайтных кодировок. Если BOM внутри файла не встретился, принимается порядок big-endian. У BOM есть значание. В UCS-2 это 65279 (для программистов — FEFF), для UCS-4/UTF-32 — это 4278124544 (FEFF0000). Число выбрано так, чтобы старшие и младшие байты у них не совпадали и по их порядку можно было бы определить какой порядок байт используется. К сожалению, BOM не даёт возможности определить использутеся двух- или четырёхбайтная кодировка.

Теперь непрограммистам будет трудно.

Пока всё было достаточно просто, но человечество придумало ещё одну кодировку — UTF-8, с плавающим размером. Хорошие новости заключаются в том, что порядок следования байт тут определён и никаких LE и BE рядом с UTF-8 не ставится. Соотвественно и BOM тут не нужен. Он может использоваться только для того, чтобы указать программе, что это именно UTF-8 и имеет номер 15711167 (EF BB BF). Откуда можно сделать вывод (дорогие писатели редакторов), что использование в UTF-8 BOM от UTF-16 — ошибка.

Трудность в том, что UTF-8, по сути это ещё один способ записи многобайтовых чисел (а каждая буква в стандарте Unicode — многобайтовое число). У системы есть целых два плюса (ирония!): старая однобайтовая кодировка совместима с UTF-8, а значит буржуинам не нужно переделывать свои программы, если они не используют в них буквы и других языков (например, на любом старом англоязычном сайте как бы уже используется кодировка UTF-8), второй плюс — латиница записывается компактнее (в один символ). Минусы — чисто программисткие: работа с кодировкой требует больше ресурсов из-за плавающего размера.

Итак. Каждый символ в кодировке занимает от 1 до 4-х байт. Вообще, формат устроен так, что можно было бы взять и более длинные цепочки, но в Unicode нет столько символов, чтобы записывать их более длинными последовательностями.

Тут надо вспомнить что такое биты. Бит — единица информации, мельче не бывает, у него всего два значения — 0 или 1. Байт состоит из 8 битов, биты очень удобно записывать в позиционной двоичной системе: 00001011. «Позиционная» тут означает, что значение числа зависит от его позиции. Кстати, это привычная нам система. В числе «22» две двойки, но у первой значение в десять раз больше, чем у второй. Это десятичная позиционная система. В двоичной, каждая более левая однёрка будет больше в два раза своей соседки.

Таким образом число 1011 расшифровывается из двоичной как 1 × 23 + 0 × 22 + 1 × 21 + 1 × 20 = 1 × 8 + 1 × 2 + 1 × 1 = 8 + 2 + 1 = 11 в десятичной системе.

UTF-8 устроен следующим образом. Пусть, мы двигаемся по строке, содержащей два байта: 208 и 159. В битах это 11010000 и 10011111. (Немного осталось, потерпите).

В первом символе нужно посчитать количество бит со значением «1» до первого нуля. Это общее количество байт, которым записан данный символ. Если количество байт — один (это вроде как специальный признак), то вы нашли не первый байт символа.

У нас в примере количество бит до первого нуля — два. Значит, буква записана двумя символами — первый это тот, на которым мы находимся и второй — который следует за ним. Каждый байт в UTF-8 разбит на две части — до первого нулевого бита. Первая часть — общая длина байт последовательности, а оставшаяся — значение. Биты из значения записывают последовательно (у нас это 10000 011111) и смотрят какое число получилось (у нас это — 1055, это номер буквы «П» в Unicode).

Могу рассказать про UTF-7 и UTF-1, если интересно. Или про суррогатные пары.

40 комментариев
bealex.livejournal.com 2009

Да, интересно. Именно это и интересно.

creagenics.com 2009

Осторожно! Интеллектуальный юмор в заголовке!

Евгений Степанищев (bolknote.ru) 2009

Комментарий для creagenics.com:

Для тех кто понимает, ага ;)

m-ivanov.livejournal.com 2009

Собственно, мне хорошо известны только две системы: младший записывается первым (как в процессорах Intel) и старший записывается первым (в процессорах ARM, которые стоят в смартфонах).

Первый — это слева или справа?

Michael Yakovis (yakovis.com) 2009

спасибо!

baka.name 2009

Первый — это слева или справа?

Вы ещё спросите, «куда растут адреса памяти?». ^_^

gaius-julius.livejournal.com 2009

«вы не хотите знать что это» — это пародия на «you don’t want to know»?

astur (astur.net.ru) 2009

После слов «Немного осталось, потерпите» текст резко теряет понятность и доступность, хотя там самый цимес остаётся.
...а про суррогатные пары — мы хотим знать, что это, да :)

isagalaev (softwaremaniacs.org/about/) 2009

Минусы — чисто программисткие: работа с кодировкой требует больше ресурсов из-за плавающего размера.

Есть мнение, что это довольно умозрительный минус. Просто потому, что операции типа «взять произвольный n-ный символ строки» на самом деле редки. Чаще строка просто сканируется целиком в любом случае (копирование, вывод, поиск). А длина строки в развитых системах давно уже таскается вместе со строкой отдельным числом, поэтому для подсчета длины в символах тоже не надо ничего декодировать и искать.

uemoe.livejournal.com 2009

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

Евгений Степанищев (bolknote.ru) 2009

Комментарий для m-ivanov.livejournal.com:

Ну, я на семитских языках не пишу и считаю слева направо )

Евгений Степанищев (bolknote.ru) 2009

Комментарий для gaius-julius.livejournal.com:

Не знаю. Такой английской фразы я не встречал.

Евгений Степанищев (bolknote.ru) 2009

Комментарий для astur.net.ru:

Там сложновато, да. Я пытался сделать на примере, чтобы было понятно. Жаль, что не удалось.

Евгений Степанищев (bolknote.ru) 2009

Комментарий для softwaremaniacs.org/about/:

Ну, в C это не так (про длину), правда ведь? Да и операции самые разные бывают. При работе с текстом каких только операций не встречается.

Но в скриптовых языках особой проблемы нет, да. И, если я ничего не путаю, внутреннее представление Unicode-cтрок у «пайтона» вовсе не UTF-8.

Евгений Степанищев (bolknote.ru) 2009

Комментарий для uemoe.livejournal.com:

Далеко не все сталкиваются с двоичной системой. Многие мои знакомые программисты не знают (и им прекрасно живётся без этого знания) что такое биты.

david-m.livejournal.com 2009

UTF-8 не является единственной кодировкой с плавающим размером символа. В UTF-16 он тоже плавает. Другое дело, что символы с кодами больше 0x10000 обычно нафиг никому не нужны, а для символов с меньшими кодами UTF-16 совпадает с UCS-2.

Евгений Степанищев (bolknote.ru) 2009

Комментарий для david-m.livejournal.com:

Подробнее, пожалуйста, про плавающий размер в UTF-16. Потому что на сайте Unicode считают иначе ( http://www.unicode.org/faq/basic_q.html#14 ):

UCS-2 is what a Unicode implementation was up to Unicode 1.1, before surrogate code points and UTF-16 were added as concepts to Version 2.0 of the standard. This term should be now be avoided […] UCS-2 and UTF-16 are identical formats. Both are 16-bit, and have exactly the same code unit representation.

Dmitriy Dzema (dmitriy.dzema.name) 2009

Комментарий для Евгения Степанищева:

Что вы имеете ввиду фразой «В C это не так (про длину)»? Никто не мешает нам завести структуру { int length; char* str; }. И будет строчка с длиной. C же язык-конструктор в котором почти всегда нужно сначала инструменты из кубиков собрать, а потом уже дом строить.

Евгений Степанищев (bolknote.ru) 2009

Комментарий для dmitriy.dzema.name:

Я не спорю, что так можно делать, но я не вижу, что повсеместно так делают. Та же libmapi, которую я ковыряю уже несколько месяцев, преспокойно хранит строки в char*, хотя там много мест, где используется именно UTF-8.

david-m.livejournal.com 2009

Комментарий для Евгения Степанищева:

http://www.faqs.org/rfcs/rfc2781.html​, “2.1 Encoding UTF-16”

Евгений Степанищев (bolknote.ru) 2009

Комментарий для david-m.livejournal.com:

Спасибо, нужный кусок:

  • Characters with values between 0x10000 and 0x10FFFF represented by a 16-bit integer with a value between 0xD800 0xDBFF (within the so-called high-half zone or high area) followed by a 16-bit integer with a value between 0xDC00 0xDFFF (within the so-called low-half zone or low surrogate area).

Исправлю.

astur (astur.net.ru) 2009

Комментарий для Евгения Степанищева:

Ну вот, например:
11010000 и 10011111 преобразуется в 1000011111. А 11010000 и 10000001 во что преобразуется? В 100001 (33), или...? А 11101000, 10000001 и 10000011, соответственно в 1000111 (71)?
Понимаю, что номера символов в примерах не требующие юникодирования. Это просто для ясности.

Евгений Степанищев (bolknote.ru) 2009

Комментарий для astur.net.ru:

Я же написал:

Каждый байт в UTF-8 разбит на две части — до первого нулевого бита. Первая часть — общая длина байт последовательности, а оставшаяся — значение.

Т. е. из двух этих числе получится 1000 и 000001 — 1000000001. Это вот этот символ: http://www.fileformat.info/info/unicode/char/0201/index.htm

astur (astur.net.ru) 2009

Комментарий для Евгения Степанищева:

Тогда получается: 110|10000 и 10|011111 => 10000 и 011111 => 10000011111.
а у тебя: 110|10000 и 100|11111 => 10000 и 11111 => 1000011111.

...или я опять неправильно понял?...

Евгений Степанищев (bolknote.ru) 2009

Комментарий для astur.net.ru:

Ой, да. Я тут накосячил, спасибо, что заметил. Поправлено.

Алик Кириллович (www.alik.su) 2009

латиница записывается компактнее (один символ против одного)

Не понял. Может быть, имелось в виду: «один символ против ДВУХ»?

Евгений Степанищев (bolknote.ru) 2009

Комментарий для www.alik.su:

Тьфу, спасибо :) Вот что значит торопился :) Поправил.

Teolog.myopenid.com 2009

Реально копатся в представлении нафиг никому не нужно.
самостоянельно парсят строки посимвольно только последние демоны. Пусть этим библиотечный функции занимаются. А им паралельно что ASII что UTF-8. Лишь бы на входе были строки одинаковой кодировки.

Евгений Степанищев (bolknote.ru) 2009

Комментарий для teolog.myopenid.com:

Откуда такое красноглазие?

Задачи бывают очень разные. Я, например, сейчас работаю с MAPI, через библиотеку libmapi, парсить за меня различные структуры библиотеки не будут, они просто не умеют.

Да и библиотеки не на деревьях растут, их кто-то пишет.

zg.livejournal.com 2009

я не знаю, как где, а в смартфонах одной финской говнофирмы — армы в le режиме.

Евгений Степанищев (bolknote.ru) 2009

Комментарий для zg.livejournal.com:

Щито?

zg.livejournal.com 2009

little-endian. другими словами, смартфоны — плохой пример big-endian платформы.

Евгений Степанищев (bolknote.ru) 2009

Комментарий для zg.livejournal.com:

А. Ну почему же плохой? Обычный.

dramele.livejournal.com 2009

не «до первого нулевого бита», а «по первому нулевому биту».
Спс, статья гуд. Сам такую у себя фигачил какое-то время назад ))

dim-hj (dim-hj.gorodok.net) 2009

Комментарий для Евгения Степанищева:

Я не спорю, что так можно делать, но я не вижу, что повсеместно так делают.

glib вполне себе повсеместный.

zelserg.livejournal.com 2010

Эта загадочная фраза:
«это именно UTF-8 и имеет номер 15711167 (EF BB BF). Откуда можно сделать вывод (дорогие писатели редакторов), что использование в UTF-8 BOM от UTF-16 — ошибка».

На самом деле — эти три байта — просто BOM (U+FEFF), записанный в кодировке
UTF-8. А число 15711167 ввобще не при чем :-).

Евгений Степанищев (bolknote.ru) 2010

Комментарий для zelserg.livejournal.com:

Число 15711167 тут так же причём, как и EFBBBF. Это просто две записи одного числа.

ninguem 2011

У меня вордовский документ большой, я делаю копу-пасте, сохранить так, сохранить этак, чтоб потом прочитать в С++. Мне нужно сделать кой-какие преобразования, причем это большой документ на 2-х языках. В заголовок этой страницы написано ??Unicode. В моем браузере ?? палка и квадратик, у вас скорее всего по другому. Наверно эти символы называются жопа. Я ищу в инете чтото типа <string.h>+unicode. И мне становится грустно

невидимка 2012

привет. интересная статей какаа про ютф=7 у тебя можно ли узнать по более про неё?

Евгений Степанищев (bolknote.ru) 2012

Комментарий для невидимка:

Очень несложный формат: http://en.wikipedia.org/wiki/UTF-7