Конечно, о юникоде (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 и персоналки).

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


  1. mwizard
    30.03.2015 18:11
    +11

    Ваша статья вводит в заблуждение относительно UCS и UTF. UCS2 и UCS4 не имеют «байтового» представления, как не имеет его, например, unsigned long long или int, поэтому говорить UCS-2BE — некорректно и не имеет смысла.

    Юникодные строки состоят из codepoint-ов. Codepoint — это просто число в открытом диапазоне [0..0x110000), и у этого числа не определено бинарное представление. UCS4 позволяет представлять codepoint-ы как, как минимум, 32-битные беззнаковые целые — но только для использования в памяти, т.к. бинарное представление в UCS4 не определено.

    Как вариант, UCS2 представляет codepoint-ы, как 16-ти битные беззнаковые целые, но при этом обрезает кодовое пространство до открытого диапазона [0..0x10000) (т.н. базовая плоскость), и в UCS2 выразить символы из дополнительной плоскости невозможно. UCS2, очевидно, является подмножеством UCS4, и как и UCS4, не имеет бинарного предствления.

    Т.е. UCS2 и UCS4 — это способы исключительно внутреннего представления юникодных строк программами, которые работают с юникодными строками.

    Поэтому и нужны «форматы трансформации Юникода» (Unicode transformation formats, UTF) — UTF-8, UTF-16 и UTF-32. Они определяют правила для преобразования каждого codepoint-а в один или больше unit-ов, которые и есть базовая единица бинарного представления.

    UTF-32 транслирует каждый codepoint в один unit размером в 32 бита. В зависимости от порядка байт, его можно записать как UTF-32BE или UTF-32LE.

    UTF-16 ставит в соответствие каждому codepoint-у один или два unit-а, каждый 16 бит размером. Два unit-а UTF-16 образуют суррогатную пару, которая декодируется в один code point. Суррогатные пары используются, чтобы выразить символы из дополнительных плоскостей — т.е. те, которые не может выразить UCS2.

    UTF-8 преобразовывает каждый codepoint в от одного до четырех (или до шести в более старом стандарте Unicode) code unit-ов, и каждый занимает в точности 8 бит.

    Надеюсь, мой комментарий немного прояснил ситуацию.


  1. zelserg Автор
    30.03.2015 18:59

    Спасибо за комментарий.
    Вопрос не вполне однозначный, давайте попробуем разобраться. Я предложил простую схему:
    UCS-2 для старого unicode, когда использовалась только одна плоскость;
    UTF-16 для нового, когда их стало 17 и потребовались суррогатные пары;
    Почему-то UTF-16 в применении к UCS-2 мне не попадалась.

    Может, мое описание не совсем точно, зато просто и понятно и при описании языков программирования (Java, например) UCS-2 чаще трактуется как урезанный UTF-16 (без суррогатных пар), а не как абстактная точка кодовая пространства.
    Вы предлагаете UCS-2 как идею точки в кодовам пространстве. Хорошо, тогда ответьте на вопрос: как хранились символы Unicode в файлах, когда была только одна плоскость? Был ли это фиксированный порядок или он мог быть разным? Как назывался этот разный порядок? Наберите в Google UCS-2 Little Endian (хотя это некорректно) и получите десятки тысяч ссылок
    Впрочем, не хочется уподобляться остро и тупо-конечникам. Чем больше значений (даже не вполне точных/корректных) мы знаем — тем лучше (если, конечно, хотим понимать друг-друга, а не бороться за чистоту языка).


    1. mwizard
      30.03.2015 19:41
      +3

      Я предложил простую схему:
      UCS-2 для старого unicode, когда использовалась только одна плоскость;
      UTF-16 для нового, когда их стало 17 и потребовались суррогатные пары;
      Имеет смысл смотреть на восьмибитные кодовые страницы, потому что это прошлое, которое все еще где-то используется в настоящем. Но зачем думать о «старом Unicode», который нигде, абсолютнейше нигде не используется? Это неудачные версии, которые были рождены в поисках. Сейчас у Unicode множество плоскостей, и следует исходить именно из этого. Более того, использовать где-либо UCS2 вместо UCS4 — плохая идея, китайские пользователи и любители emoji скажут огромное «спасибо» за невозможность использовать привычные символы.

      Почему-то UTF-16 в применении к UCS-2 мне не попадалась.
      Вы никогда не можете знать, попадалась или нет. Если вы видите что-то, что вы думаете, что является UCS-2 — это UTF-16, и неважно, есть ли в нем суррогатные пары или нет. То, что их нет в этом отдельно взятом тексте, не означает, что их там не может быть.

      Может, мое описание не совсем точно, зато просто и понятно и при описании языков программирования (Java, например) UCS-2 чаще трактуется как урезанный UTF-16 (без суррогатных пар), а не как абстактная точка кодовая пространства.
      А вот за это стоит пороть патчкордом на конюшне. Это настолько дремучее наплевательство на стандарты — просто выкинуть все, кроме BMP — что у меня нет приличных слов, чтобы описать мое отношение к подобным затеям.

      Вы предлагаете UCS-2 как идею точки в кодовам пространстве.
      Нет, не совсем. UCS-2 и UCS-4 — это внутренние представления точек в кодовом пространстве. UCS-2 — урезанное, которое более не стоит использовать, и UCS-4 — включающее весь современный Unicode. И это не я предлагаю, а консорциум Unicode. Естественно, архитектор может выбрать использовать в качестве внутреннего представления строк в программе не UCS, а, например, UTF8. Это имеет смысл, когда нужно сохранить совместимость с char * — но на самом деле, когда будет осуществляться работа с этими строками — придется все же выбрать, какие codepoint-ы использовать для декодированных представлений — 16 или 32-битные — UCS-2 или UCS-4.

      Хорошо, тогда ответьте на вопрос: как хранились символы Unicode в файлах, когда была только одна плоскость?
      Отвечу запросом на запрос — а как хранились символы Unicode в файлах, когда Unicode не было? Изначальная идея с «64к символов хватит всем» на практике оказалась неудачной, поэтому нет больше смысла потрясать ISO 10646, в котором UCS-2 имел бинарное представление и в каких-то аспектах был похож на современный UTF-16.

      Был ли это фиксированный порядок или он мог быть разным? Как назывался этот разный порядок? Наберите в Google UCS-2 Little Endian (хотя это некорректно) и получите десятки тысяч ссылок
      Эти десятки тысяч ссылок либо относятся к ошибкам использования терминологии, когда хотят сказать UTF-16, но говорят UCS-2; либо к тем временам, когда UTF еще не было, и только UCS-2 носился над водой; либо к тем кастрированным, наплевательским реализациям, которые считают, что в UTF-16 нет суррогатных пар. Последним — стыд и позор.

      Впрочем, не хочется уподобляться остро и тупо-конечникам. Чем больше значений (даже не вполне точных/корректных) мы знаем — тем лучше (если, конечно, хотим понимать друг-друга, а не бороться за чистоту языка).
      Ради облегчения понимания я и оставил свой комментарий, так как когда выполнял работу по перестройке громаднейшего C-проекта с исключительно cемибитных ASCII-строк на полную поддержку Unicode, уже успел собрать все грабли, включая терминологические, архитектурные и практические.


  1. zelserg Автор
    30.03.2015 19:19

    Это статья (в паре со статьей о кодировках) написана, чтобы программистам было понятнее, что такое UTF-16BE/LE, UTF-8, что такое и зачем нужен BOM, как символы хранятся в памяти и в файлах. Чтобы им было легче понять, почему у них в программе читаются/пишутся «кракозябры».

    Извиняюсь, если вместо понимания я невольно ввел в заблуждение. Но ведь по Unicodee есть масса других статей, где-то более правильных и интересных. Я написал такую, какую сам бы хотел прочитать лет 5 назад… Кстати, первый вариант опубликован пару лет назад, но ЖЖ не дает поисковикам индексировавать начинающих авторов. А если статьи нет в поисковике — ее, считай, нет в интернете.


  1. kostyl
    30.03.2015 22:11

    http://unicode-table.com/ru/


  1. amarao
    30.03.2015 22:49
    +1

    Чего? Если формат не известен, то это utf8. Или я что-то не понимаю? java и windows застряли на 16 битах и хаках вокруг них, нормальный юникс сел на utf8 и с тех пор с ним всё хорошо настолько мало плохо, насколько может быть.


  1. subzey
    31.03.2015 02:10

    Кстати, раз уж в одной статье упомянуты и суррогатные пары, и UTF-8, возможно вам будет интересно почитать про CESU-8 и WTF-8.

    Tl;dr: суррогаты в utf-8 нельзя, но если очень хочется и никто не узнает, то можно.



  1. Sava
    31.03.2015 16:18

    Лирическое отступление: несколько лет назад телепузики прислали расшифровку звонков, одолел ее только Штирлиц — они прислали страницу в КОИ-5!!!


  1. zelserg Автор
    31.03.2015 17:02

    Телетайп с 5-дорожечной перфолентой помню, а вот как называлась используемая им кодировка — забыл (но кажется, не КОИ-5...)
    Кстати, хотя телетайп печатал медленней электрической машинки «Консул», зато он в отличии от нее никогда не сбивался. А у «Консула» рычажки заклинивались и печать прерывалась.


    1. Sava
      31.03.2015 23:44

      Так этот код назвал Штирлиц, сам я сталкивался с перфолентой только когда с парней на кафедре потребовали считать архив. Электронику они сделали, но юстировать механику и оптику фотосчитки от «Наири-К» позвали меня, выдали пару баллонов пива и метровый обрывок ленты. Через полчаса на экране ХТ появился осмысленный обрывок программы.