Юникод

Юникод — международный стандарт кодирования символов, охватывающий почти все языки мира, также включающий математические символы, эмодзи и пр. То же: unicode.

В юникоде каждый символ имеет свой уникальный номер. Например, у латинской “d” (строчной) это 100 (в десятичной системе), у латинской же “K” — 75, у русской “К” — 1050, у скрипичного ключа 𝄞 — 119070.

Код конкретного символа легко узнать в оболочке. Для этого надо перед ним поставить знак доллара:

1> $d. 
100
2> $K.
75
3> $К. 
1050
4> $𝄞. 
119070

В Эрланге нет специального типа данных для строк. Литерал, заключённый в кавычки, превращается в список целых чисел, где каждое число отражает уникальный номер символа в юникоде.

5> "dKК𝄞". 
[100,75,1050,119070]

Корректная работа с юникодом (в частности кириллицей) сводится к трём основным принципам:

Для работы с юникодом в регулярных выражениях надо использовать опцию unicode.

У юникода есть несколько форм представления: utf-8, utf-16, utf-32. Две последние в свою очередь имеют две подформы: LE и BE (“остроконечный” и “тупоконечный” порядок байтов). Есть и другие формы, но они используются значительно реже. Формы представления влияют на то, как уникальный номер каждого символа представлен в памяти. utf-8 — самая компактная форма, зато utf-32 самая универсальная. В utf-8 и utf-16 разные символы могут быть представлен разным количеством байтов. В utf-32 на один символ приходится всегда четыре байта.

У Эрланга, конечно, есть инструменты для чтения и сохранения файлов в разных формах представления.

Юникод и строковые литералы

По умолчанию в Эрланге для файлов с кодом включена кодировка utf-8.

main() -> 
    Name = "Колобок",
    io:format("~p~n", [Name]).

Скомпилируем и запустим в оболочке:

2> proba:main().
[1050,1086,1083,1086,1073,1086,1082]

В шелле операционной системы результат аналогичный:

$ erl -noshell -s proba main -s init stop
[1050,1086,1083,1086,1073,1086,1082]

Чтение со стандартного ввода

Если ввод осуществляется в оболочке Эрланга или шелле ОС, можно использовать io:fread/2 с управляющим кодом “~ts”, который отвечает за трансляцию из юникода.

1> io:fread("Имя> ","~ts").
Имя> Колобок
{ok,[[1050,1086,1083,1086,1073,1086,1082]]}

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

main() -> 
    Name = io:fread("Имя> ","~ts"),
    io:format("~p~n", [Name]).

Дойдя до пробельного символа, io:fread перестанет воспринимать остальное. Если предполагается ввод с пробелами, в таком случае лучше использовать io:get_line.

1> A = io:get_line("Введи имя: ").
Введи имя: Круглый колобок. 
[1050,1088,1091,1075,1083,1099,1081,32,1082,1086,1083,1086,
 1073,1086,1082,46,32,10]

Как мы видим, в список был добавлен символ номер 10 (перевод строки).

Чтение юникода из файлов

Допустим, у нас есть три файла: txt.8, txt.16 и txt.32. В каждом из них записан один и тот же текст, но в разных кодировках: utf-8, utf-16 и utf-32. Создать такие файлы можно с помощью утилиты iconv:

$ echo 'Колобок' | iconv -t utf-8 > txt.8
$ echo 'Колобок' | iconv -t utf-16 > txt.16
$ echo 'Колобок' | iconv -t utf-32 > txt.32

Джо Армстронг в своей книге рекомендует при работе с файлами использовать бинарники. То есть мы читаем содержимое файла в бинарник — один в один, а потом уже с этим бинарником что-либо делаем в программе. Записываются файлы наоборот: сначала мы содержимое упаковываем в бинарник, а бинарник лёгким движением руки превращаем в файл, содержимое которого будет один в один соответстовать бинарнику.

Так мы и поступим. Превратить файл в бинарник можно одной функцией file:read_file/1, которая возвращает {ok, Binary}, ну или {error, Reason}.

1> {ok,Bin8} = file:read_file("txt.8"). 
{ok,<<208,154,208,190,208,187,208,190,208,177,208,190,
      208,186,10>>}
2> {ok,Bin16} = file:read_file("txt.16").
{ok,<<255,254,26,4,62,4,59,4,62,4,49,4,62,4,58,4,10,0>>}
3> {ok,Bin32} = file:read_file("txt.32").
{ok,<<255,254,0,0,26,4,0,0,62,4,0,0,59,4,0,0,62,4,0,0,49,
      4,0,0,62,4,0,...>>}

Таким нехитрым способом мы получили три бинарника (из трёх файлов): Bin8, Bin16 и Bin32. Оболочка нам сразу даёт обратную связь — показывает, какие бинарники она привязала к переменным. Обратите внимание, что бинарники Bin16 и Bin32 между собой больше похожи, в отличие от Bin8. Это связано с тем, что в utf-8 упаковка немного другая (чуть более сложная).

Теперь нам осталось ещё одно действие: надо бинарник превратить в нормальный список чисел, каждое из которых соответствует одной букве из слова “Колобок”. Для этого используем функцию unicode:characters_to_list. Если мы читаем бинарник с кодировкой utf-8, то можно обойтись лишь одним аргументом (бинарником). Для других кодировок придётся указать атом utf16 или utf32.

4> unicode:characters_to_list(Bin8). 
[1050,1086,1083,1086,1073,1086,1082,10]
5> unicode:characters_to_list(Bin8, utf8).
[1050,1086,1083,1086,1073,1086,1082,10]
6> unicode:characters_to_list(Bin8, unicode).
[1050,1086,1083,1086,1073,1086,1082,10]

Ура! Мы получили заветный список чисел [1050,1086,1083,1086,1073,1086,1082]. К нему, правда, добавился ещё символ 10 (“\n”). Но этот символ был добавлен при создании файла (про то, что такое возможно, тоже не следует забывать).

Данные три вызова функции идентичны: без указания кодировки, с указанием атома utf8 и с указанием атома unicode (который синоним для utf8).

Чтение юникода из файлов и BOM

Как я уже написал ранее, кодировки utf-16 и utf-32 могут быть в двух вариантах: “остроконечном” и “тупоконечном”. Это связано с порядком байтов: от младших к старшим или от старших к младшим. И тут возникает неопределённость: мы можем знать, что кодировка в файле utf-16 или utf-32, но этого знания мало.

Продолжим извлекать строку “Колобок” из файлов, созданных в предыдущем разделе.

7> unicode:characters_to_list(Bin16, utf16).
[65534,6660,15876,15108,15876,12548,15876,14852,2560]
8> unicode:characters_to_list(Bin32, utf32).
{error,[],
       <<255,254,0,0,26,4,0,0,62,4,0,0,59,4,0,0,62,4,0,0,49,4,0,
         0,62,4,...>>}

Нет, не удалось нам получить колобка. Атом utf16 по умолчанию подразумевает тупоконечный порядок байтов. Атом utf16 это синоним кортежа {utf16,big}. Аналогично utf32 это синоним кортежа {utf32,big}.

Как оказалось, файлы, созданные нами, имеют остроконечный порядок. Значит, нам вместо big надо указать little:

9> unicode:characters_to_list(Bin16, {utf16,little}).
[65279,1050,1086,1083,1086,1073,1086,1082,10]
10> unicode:characters_to_list(Bin32, {utf32,little}).
[65279,1050,1086,1083,1086,1073,1086,1082,10]

Слава богу, показался наконец наш колобок. В конце он имеет уже виденный нами символ перевода строки. Но что означает первый символ за номером 65279? А это так называемый символ BOM (byte order mark). Одно его значение показывает, что файл имеет порядок байтов little-endian, а другое — что big-endian.

Если мы читаем файлы, которые сами же и создали, нам, скорее всего, уже известна кодировка и порядок байтов, остроконечный или тупоконечный. Если это не так, мы можем определить кодировку по символу BOM, который стоит в начале файла. Для этого есть функция unicode:bom_to_encoding/1.

11> unicode:bom_to_encoding(Bin16).
{{utf16,little},2}
12> unicode:bom_to_encoding(Bin32).
{{utf32,little},4}

Как мы видим, эта функция по BOM определяет как кодировку (utf16 или utf32), так и порядок байтов (в данном случае little). Число — 2 или 4 — означает количество байтов, занимаемое символом BOM. Используя эту информацию, легко получить более точное распознание кодировки.

Следует учесть, что иногда (но далеко не всегда) BOM-символ добавляют и в файлы с utf-8. В этом нет большой необходимости, потому что там порядок байтов всегда один и тот же. Если в наших проектах мы пользуемся разными кодировками, можно везде добавлять BOM, чтобы потом автоматически раскодировать. Символ BOM для нашей кодировки можно получить с помощью функции unicode:encoding_to_bom/1.

Как отображать юникод в оболочке

Если мы выводим в оболочке текст с помощью io:format, то проблем не возникает:

1> io:format("KOT").
KOTok

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

2> A = "КОТ".
[1050,1054,1058]

"КОТ" это всего лишь синтаксический сахар для [1050,1054,1058], то есть списка целых чисел. В этом списке каждое число соответствует какому-либо символу. Виртуальная машина, исполняя программу, не знает, какой смысл мы вкладываем в тот или иной список целых чисел. Может быть, это именно список целых чисел, означающий года: когда родился Святополк II Изяславич, когда умер Ярослав Мудрый и когда Изяслав Ярославич победил голядь. А может быть, [1050,1054,1058] это всего лишь КОТ.

В Эрланге не стали вводить такой тип данных как строки, чтобы не плодить сущностей. И мы теперь поэтому имеем универсальные функции, одинаково работающие как со списками чисел, так и со списками символов. В своей голове мы можем сэкономить место и не запоминать лишнего.

Но иногда бывает неудобно при работе в оболочке. Она, как известно, старается догадаться, не кроется ли за каким-то списком целых чисел строка с буквами. Но это касается только латинских букв.

1> "cat".
"cat"
2> "КОТ".
[1050,1054,1058]

Если мы желаем, чтобы кириллические символы тоже отображались, мы можем запустить оболочку с ключом +pc unicode.

$ erl +pc unicode
Erlang/OTP 27 [erts-15.0.1] [source] [64-bit] [smp:2:2] [ds:2:2:10]
    [async-threads:1] [jit:ns]

Eshell V15.0.1 (press Ctrl+G to abort, type help(). for help)
1> "cat".
"cat"
2> "КОТ".
"КОТ"

На самом деле, это далеко не всегда удобно. Когда мы работаем с числами, нам хочется видеть именно числа, а не непонятные символы.

3> [111,222,333,444,555,666,777,888].
"oÞōƼȫʚ̉͸"

Документация

Using Unicode in Erlang — использование юникода в Эрланге.

unicodeмодуль unicode, где есть ряд полезных функций.


© Алексей Карманов, 2024.