Юникод — международный стандарт кодирования символов, охватывающий почти все языки мира, также включающий математические символы, эмодзи и пр. То же: unicode.
В юникоде каждый символ имеет свой уникальный номер. Например, у латинской “d” (строчной) это 100 (в десятичной системе), у латинской же “K” — 75, у русской “К” — 1050, у скрипичного ключа 𝄞 — 119070.
Код конкретного символа легко узнать в оболочке. Для этого надо перед ним поставить знак доллара:
1> $d.
100
2> $K.
75
3> $К.
1050
4> $𝄞.
119070
В Эрланге нет специального типа данных для строк. Литерал, заключённый в кавычки, превращается в список целых чисел, где каждое число отражает уникальный номер символа в юникоде.
5> "dKК𝄞".
[100,75,1050,119070]
Корректная работа с юникодом (в частности кириллицей) сводится к трём основным принципам:
[1050,1086,1083,1086,1073,1086,1082]
.[1050,1086,1083,1086,1073,1086,1082]
на всём протяжении работы программы, пока она не замыслит отдать эти строковые данные куда-то наружу.Для работы с юникодом в регулярных выражениях надо использовать опцию 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).
Как я уже написал ранее, кодировки 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.