Как стать автором
Обновить

И снова о Юникоде (Unicode)

Время на прочтение 8 мин
Количество просмотров 20K
Конечно, о юникоде (Unicode) в интернете море информации (см, например Юникод, некоторые фрагменты текста я взял оттуда), я ни в коей мере не претендую на полное и исчерпывающее описание. Это просто некоторая дополнительная «информация к размышлению».

В предыдущей заметке О кодировках и кодовых страницах, я писал о проблемах показа однобайтовых символов. Суть в том, что хотя для показа 26 символов латинского алфавита достаточно 52 кодов (прописные+строчные), для показа национальных символов даже 256 кодов (это байт, единица хранения и адресации) оказывается недостаточно. Для решения проблемы отображения национальных символов использовались (да и сейчас кое-где используются) кодовые страницы. Хочешь выводить национальные символы – используй соответствующую кодовую страницу. Но что, если в тексте нужны символы из многих кодовых страниц? Для набора математического текста могут понадобиться английские, русские, математические, греческие, типографские символы, причем одновременно. Кроме того, для национальных символов используется несколько конкурирующих кодировок (т.е. кодовых страниц), какую выбрать? Решить эти проблемы должен был созданный в 1991 году стандарт Unicode (по-русски Юникод или Уникод).

Юникод решительно порвал с однобайтовым прошлым и предложил стандарт UCS-2 (universal character set), где каждый символ кодируется раз и навсегда закрепленным за ним 16-ти битовым числом, состоящим из старшего байта и младшего байта (естественно, для прописных и строчных символов – разные числа). Кроме кода, за каждым символом закреплено название (на английском языке). Скажем, за русской прописной “A” закреплен код 104010, а за строчной “я” закреплен код 110310 и название «CYRILLIC SMALL LETTER YA». Здесь коды чисел приведены в 10-ричной системе счисления, но часто используют 16-ричной коды: 104010 соответствует 41016, а коду 110310 соответствует 44F16. Дабы не гадать, в какой системе счисления записан код и чтобы сразу было ясно, что речь идет не об абстрактном числе, а о коде символа юникода, используется особая запись: U+0410 для “А” и U+044F для “я”. Видим в тесте U+FEFF и сразу понимаем, что речь идет о символе юникода FEFF16, он же 6527910 (по-русски называется «неразрывный пробел нулевой ширины»). А коды U+0401 и U+0451 назначены символам Ё «CYRILLIC CAPITAL LETTER IO» и ё соотвественно (букву Ё опять «обидели», присвоив ей коды вне диапазона остальных букв русского алфавита). Все символы кириллицы (да и многие другие вместе с кодами) очень удобно смотреть на Кириллица.

Итак, Unicode перешел от однобайтовой кодировке к 2-байтовой, кардинально расширил количество описываемых символов – можно почивать на лаврах? Отнюдь, с новыми возможностями пришли новые проблемы. Проблема первая – как хранить эти 2 байта символа в памяти и в файлах? Запишем код U+0410 в виде двух байтов: 04(старший) и 10(младший). В памяти, как мы знаем, каждый байт имеет свой адрес и можно записать наш код двумя способами: либо 04 по адресу X, а 10 по адресу X+1, либо 10 по адресу X, а 04 по адресу X+1. Соответственно, в файле 10 будет записано либо после 04 либо перед ним. На первый взгляд проблема высосана из пальца: раз код юникода – это 16-битовое число, давайте и хранить его так, как компьютер хранит 16-битовые числа. А-а-а, вот тут и попались – компьютеры различной архитектуры хранят 16-ти битовые числа по-разному: одни хранят старший байт числа перед младшим, а другие — после! Персоналки хранят старший байт после младшего, а многие «большие компьютеры» хранят старший байт перед младшим. Архитектура многих смартфонов такая, что данные хранятся, «как в больших компьютерах», так что для архитектур «размер не имеет значения». Когда данные пишутся из памяти в файл или читаются из файла в память (напрямую, без участия процессора), байты передаются в порядке возрастания адресов. С другой стороны, операции сравнения и сортировки требуют работы с кодами как с числами. В общем – либо проблемы с порядком байтов в файлах, либо проблемы с алгоритмами обработки. Победили алгоритмы и 2 байта символа хранятся в памяти так же, как данная архитектура хранит целые числа (т.е. код «в смысле юникода» и код как число в памяти – это одно и то же 16-ти битовое число, независимо от архитектуры). Соответственно, в файле первым идет либо старший байт числа (стандарт UCS-2BE – “Big Endian” или «прямой порядок»), либо младший байт (стандарт UCS-2LE – “Little Endian” или «обратный порядок»).

Ну хорошо, все же 2 варианта хранения – это не несколько десятков кодовых страниц. Подождите – еще не вечер, обсудили только первую проблему. По мере роста популярности юникода возникло масса желающих добавить в него свои символы. Ладно бы хотели добавить символы типа церковнославянских «ять», «ижица», но появилось желание добавить тысячи иероглифов, знаков древних письменностей и тп. В общем, 65536 символов не хватило (а ведь когда-то казалось «всем-всем хватит», были даже незанятые области, «про запас»). В результате в 1996 году появился второй стандарт юникода, расширяющий количество доступных символов с 65536 до 1’112’064, так что код символа стал от U+0000 до U+10FFFF. Расширили «с большим запасом» и даже спустя 16 лет, в стандарте юникода 6.2 от 2012г описано ~110000 символов (плюс ~137000 зарезервировано). Расширить расширили, но как их хранить в памяти, в файлах? Для такого количество символов нужны числа с 21 битом, т.е. 3 байта. И что делать с морем программ, файлов которые хранят и обрабатывают 2-х байтовые символы юникода стандарта USC-2?

В результате нашли компромиссное решение, снижающее сложности перехода. Поступили так: те 65536 кодов из UCS-2 – они наиболее употребительные, их трогать не будем и храним как раньше – в виде 16-ти битового числа (этот набор кодов назвали нулевой или базовой плоскостью). Остальные коды образуют плоскости с 1-й по 16-ю (в каждой по 65536 кодов). Так что символы нулевой плоскости U+0000 – U+FFFF храним как раньше – в виде 16-битового числа, а символы с кодами U+010000 — U+10FFFF (таких кодов 220) храним в виде пары 16-ти битных чисел (так называемые «суррогатные пары»), первое число пары из диапазона U+D800 — U+DBFF, а второе — из диапазона U+DС00 — U+DFFF. Легко увидеть, что в каждом диапазоне 10 бит произвольны, в паре это дает произвольное 20-ти битовое число. Но как отличить, представляет ли код U+DA15 соответствующий ему символ из UCS-2 или это первый код суррогатной пары? А не надо отличать – в UCS-2 коды из диапазона D800 — DFFF (2048 символов) были зарезервированы, поэтому им не соответствовали никакие символы. Стандарты представления символов с «суррогатными парами» называются utf-16BE и utf-16LE. Пришлось, конечно, переписывать программы и библиотеки для работы с новым стандартом, но если не было нужды в использовании символов из плоскостей 1-16, то и старые программы отлично работали. Java (которая сразу использовала стандарт UCS-2) включила поддержку суррогатных пар только в версии J2SE 5.0, а до этого как-то обходилась без них… Почему в суррогатных парах диапазоны первого и второго символа различаются? Не знаю… Если бы был один общий диапазон, то тогда бы можно было закодировать не 20 бит, а 22 бита и вместо миллиона доступных символов получить 4 миллиона. Но миллион тоже много, когда еще его «истратят», а раздельные диапазоны дают дополнительный контроль (вдруг файл не юникодовский).

Резюмирую: для символов U+0000-U+FFFF в utf-16 используются 16-битовое представление, как в UCS-2, а для символов U+10000-U+10FFFF в используются «суррогатная пара» из кодов в области U+D800-U+DFFF (перед переводом числа U+10000 и выше его уменьшают на 1000016 и полученное 20-битное число кодируют суррогатной парой). Итого, в utf-16 можно представить 220+216-2048 = 1’112’064 символов. Что дало расширение Unicode? Те, кому нужны были новые символы, получили возможность их использовать. Те, кому не нужны редко используемые иероглифы, кто не работает с устаревшей письменностью расширения возможностей [почти] не заметили. А вот программистам мороки добавилось. То ли дело в UCS-16: хочешь получить n-й символ строки, берешь n-й символ массива и все. А с суррогатными парами это не проходит. Пишешь программу, работающую с текстом – не забывай про эти «пары» (даже если тебе они тебе вроде как и не нужны, все равно используй другие объекты, другие функции).

Есть ли другие способы преставления (хранения) символов юникода, не utf-16? Есть – это utf-32, где каждый символ представлен 32-битным числом. Разумеется, старший байт такого числа всегда 0, а байт перед ним почти всегда 0, но столь неэкономное расходование памяти компенсируется удобством обработки (не нужны суррогатные пары, нет проблемы «взять n-й символ в строке). Естественно, в зависимости от архитектуры, используют вариант либо utf-32BE либо utf-32LE).

А теперь сюрприз — кроме utf-16/32 есть еще одно замечательное представление символов юникода в виде последовательности байтов (от 1 до 4). Это представление придумали в 1992 году Кен Томпсон и Роб Пайк и назвали его utf-8. В этом представлении символам ASCII (первые 128 символов 0-й плоскости) соответствует сам код символа, т.е. текст из символов с номером меньше 128 в utf-8 состоит из тех же самых байтов, поэтому любую строку в ASCII автоматически можно считать строкой в utf-8.

Символы utf-8 получаются из Unicode следующим образом (из Википедии):
0x00000000 — 0x0000007F: 0xxxxxxx (т.о. символы 0-127 не меняются)
0x00000080 — 0x000007FF: 110xxxxx 10xxxxxx
0x00000800 — 0x0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx
0x00010000 — 0x001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

Чем больше код символа, тем больше надо байтов для его преставления в utf-8. Счастливым пользователям «чисто латинского» алфавита достаточно 1 байта (так что выигрыш по сравнению с utf-16 очевиден), европейцам только иногда требуется два байта. А для кириллицы всегда надо два байта (как в utf-16), но для смешанного русско-английского текста какой-то выигрыш получается. А вот сирийским, грузинским символам надо три байта… Еще один плюс utf-8 — в компьютерах с любой архитектурой байты хранятся по возрастанию адресов и поэтому не нужны BE/LE (суррогатные пары, кстати, тоже не нужны). Наконец, такая фишка utf-8, как «самосинхронизация». Предположим, программа принимает поток символов в формате utf-8 и выводит их на экран. Пусть по каким-то причинам (сбой, например) программа неверно определила номер байта для принимаемого символа. Естественно, на экран вместо правильного символа будет выведен совсем другой символ. Но очень быстро программа исправится и начнет выводить корректные символы (это не свойство программы – это свойство самой utf-8). А если бы принимали utf-16/32 и потеряли байт (или вместо ожидаемого utf-16LE поступает utf-16BE)? Все, приехали – вместо нужной информации на экране будут «не наши» символы и для правильного показа программа должна сделать какой-то интеллектуальный анализ.

Кстати, раз уж пошла речь о сбоях и чужих форматах, как программа определит, в каком именно utf записан файл (даже если она точно знает, что имеет дело с юникодом)? Вариантов три:
1. В самое начало файла записан специальный символ с кодом U+FEFF. Позвольте, ведь это «неразрывный пробел нулевой ширины». Да, это он, но теперь как пробел он не используется и у него есть второе имя –“byte order mark” (BOM, маркер порядка байтов). В файл utf-16LE он запишется как FF FE, а в файл utf-16BE как FE FF, поэтому, прочитав пару (тройку) первых байт файла, программа может определить, в каком формате записан файл. А вдруг в файл записан символ с кодом U+FFFE и программа собьется, приняв его за BOM? Не беспокойтесь – символа с кодом U+FFFE не существует, т.е. такой код никому не назначен (и не будет). Для файлов в формате utf-8 первым в файле тоже может быть записан символ U+FEFF (он выглядит как последовательность EF BB BF), хотя здесь он маркирует не порядок байтов, а сам формат utf-8. А если программа старенькая и про новую роль BOM ничего не знает, а принимает его за «неразрывный пробел нулевой ширины»? Ничего страшного – он «пробел» и поэтому пустой, а из-за «нулевой ширины» места на экране не занимает, т.е. на экране его не видно (код специально так подобрали, чтобы не имел пары с обратным порядком байтов и не «мозолил» глаза при выводе)! А его «типографскую роль» сначала взял на себя символ “word joiner” U+2060, а потом и другие (U+200b “zero width space”, U+200d “zero width joiner”)
2. Есть соглашение, что этот файл всегда пишется в определенном формате или есть общесистемное соглашение (к примеру, в персоналках и windows – LE, в «больших» машинах и unix — BE)
3. Если нет ни BOM ни соглашения, то для межплатформенного обмена считается, что файл в стандарте utf-16BE (тут unix и большие машины «победили» windows и персоналки).
Теги:
Хабы:
+10
Комментарии 11
Комментарии Комментарии 11

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн