Юникод

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

Над такими решениями работает некоммерческая организация Unicode Consortium. Организация вырабатывает стандарт, предметом которого является система кодирования символов для поддержки всемирного обмена, обработки и отображения текстов разнообразных языков и технических приложений в современном мире. Дополнительно стандарт поддерживает классические и исторические тексты на многих естественных языках.

История стандарта начинается с 1991 года. Он получил название Юникод (Unicode). В его основе лежит универсальный набор символов — UCS (Universal Character Set), объединяющий буквы практически всех современных языков, большой набор иероглифов языков, использующих иероглифическую письменность, цифры, знаки пунктуации, огромное множество математических и технических символов.

Символы в UCS пронумерованы целыми неотрицательными числами. В сущности UCS представляет собой большую кодовую страницу. Её принципиальным отличием от рассмотренных ранее кодовых страниц является размер. Существует два варианта UCS: короткий (UCS-2) и длинный (UCS-4). Диапазон номеров в UCS-2 — 0‥65535, и для представления таких чисел требуется два байта. Для представления символов в UCS-4, как нетрудно догадаться, нужно 4 байта. Из всей огромной «номерной ёмкости» (0‥4294967295) UCS-4 по техническим причинам решено использовать очень малую часть, которая, впрочем, весьма велика: 1114112 позиций. В настоящее время этого более чем достаточно: это пространство заполнено символами лишь примерно на одну десятую. UCS-2 естественным образом встраивается в UCS-4 и в рамках большой таблицы называется базовой многоязычной плоскостью (BMP — Basic Multilanguage Plane).

Базовая многоязычная плоскость включает 99,9% того, что может потребоваться для решения большинства задач. Многие программы, поддерживающие Юникод, ограничивают такую поддержку базовой плоскостью.

Универсальный набор символов хорошо продуман. Его заполнение подчинено двум принципам:

Для обозначения позиций в UCS применяется особая нотация. Например, для символа № 9786 (смайлик — ) обозначением будет U+263A (здесь 263A — шестнадцатеричная запись числа 9786).

Таблица символов UCS несёт дополнительную нагрузку. Символы, кроме номеров, имеют дополнительные признаки, в зависимости от которых символы подразделяются на графические (имеющие изображение), управляющие, и символы форматирования. Графические символы, в свою очередь, делятся на категории:

Использование большой кодовой таблицы ставит перед нами очень неприятную проблему. Поскольку номера символов из таблицы UCS-4 требуют четырёх байт, текст, закодированный таким образом, становится в 4 раза длиннее. Это плата за большой выбор используемых символов. Это очень расточительно, но по-другому не получится, если кодировать каждый символ одним и тем же количеством байтов.

Есть и другая возможность, но она предполагает отказ от фиксированной длины символов (измеренной в байтах). Это кодировки с переменной длиной символа. Большое распространение получила кодировка UTF-8.

Эта кодировка охватывает диапазон символов UCS-4 с номерами из диапазона 0 2 31 , то есть первую половину таблицы UCS-4. Это символы с номерами, занимающими в двоичной записи не более 31 бита. Этого вполне достаточно. Один символ в кодировке UTF-8 занимает, в зависимости от его номера, от одного до шести байтов (октетов) в соответствии с таблицей:

диапазон номеров символовпоследовательность октетов
0 2 7 1 0*******
2 7 2 11 1 110***** 10******
2 11 2 16 1 1110**** 10****** 10******
2 16 2 21 1 11110*** 10****** 10****** 10******
2 21 2 26 1 111110** 10****** 10****** 10****** 10******
2 26 2 31 1 1111110* 10****** 10****** 10****** 10****** 10******

В таблице вместо звёздочек размещаются биты двоичной записи номера символа.

Заметим, что символы с номерами из диапазона 0 127 кодируются одним октетом с тем же самым, что и у символа, номером. Кодирование всех остальных пяти диапазонов устроены по одному принципу. Это последовательность октетов, каждый из которых начинается с битов служебных (показаны в таблице как нули и единицы), и заканчивается битами информационными (показаны как звёздочки). Первый (головной) октет в начале содержит столько единиц, какова длина последовательности, а за единицами следует ноль. Все последующие октеты начинаются со служебной битовой последовательности 10.

Для тренировки определим UTF-8-код смайлика . Его номер в таблице UCS-4 равен 9786, или в двоичной записи 10011000111010. Из таблицы определяем, что в кодировке UTF-8 запись символа потребует трёх октетов: 1110**** 10****** 10******. Дополним слева двоичный код двумя нулевыми битами до 16 (по количеству звёздочек), и заменим звёздочки полученными битами: 11100010 10011000 10111010.

Помимо своей простоты, кодировка UTF-8 имеет и другие достоинства:

Очевидным недостатком кодировки является переменная длина символа. Чтобы разбить последовательность октетов по символам, нужно анализировать головной октет, чтобы определить, сколько октетов приходится на очередной символ. Затем следует прочитать нужное количество добавочных октетов, извлечь из головного и оставшихся информационные биты и составить из них номер символа.

Особо отметим, что не всякая последовательность октетов является закодированной последовательностью символов. «Плохими» являются, например, последовательности 11011001 и 11100101 00010011.

Perl — один из немногих языков программирования, в котором имеется встроенная поддержка стандарта Юникод. И, хотя эта поддержка не полная, многие возможности, предоставляемые стандартом, реализованы.

Перечислим некоторые аспекты поддержки Юникода в Perl. Они касаются того, где могут встречаться символы UCS:

Гораздо важнее другое применение директивы use utf8;. С ней строки, заключённые между апострофами и двойными кавычками, будут рассматриваться как строки символьные, а не октетные.

А какая, спрашивается, разница? Обсудим этот важный вопрос подробно.

Рассмотрим строку Привет!. Согласно таблице UCS она состоит из символов U+41F U+440 U+438 U+432 U+435 U+442 U+21. Но, закодированная в UTF-8, она представлена как последовательность октетов D0 9F D1 80 D0 B8 D0 B2 D0 B5 D1 82 21 (в шестнадцатеричной записи). Здесь на каждую русскую букву приходится по два октета, и лишь восклицательный знак (он есть в таблице ASCII) занимает один октет.

Сравним две программы:

print length "Привет!";

и

use utf8;
print length "Привет!";

Первая напечатает число 13, поскольку по умолчанию один октет означает один символ с тем же номером. Вторая выведет 7. В отсутствие команды use utf8; с точки зрения Perl строка состоит из символов U+D0 U+9F U+D1 U+80 U+D0 U+B8 U+D0 U+B2 U+D0 U+B5 U+D1 U+82 U+21, то есть Привет!. Пусть читатель поверит нам на слово, здесь действительно 13 символов, но некоторые из них являются служебными, поэтому невидимы.

Директива use utf8; влияет на смысл регулярных выражений. Рассмотрим программу, которая, по нашему замыслу, печатает те названия месяцев, которые состоят из восьми букв:

for(qw/январь февраль март апрель май июнь июль август сентябрь октябрь ноябрь декабрь/)
{
	print "$_\n" if m/^.{8}$/;
}

Но вместо ожидаемого вывода

появится

Каждая русская буква в кодировке UTF-8 превращается в два октета, которые по умолчанию считаются парой символов с теми же номерами. Так что март — восьмисимвольная строка. Директива исправляет это поведение программы.

Ещё один пример влияния директивы на смысл регулярных выражений. Выведем те названия месяцев, в которые входит русская буква а (заглавная или маленькая).

for(qw/январь февраль март апрель май июнь июль август сентябрь октябрь ноябрь декабрь/)
{
	print "$_\n" if m/[Аа]/;
}

Но что же это?

Откуда буква а в слове июнь? Дело в том, что символьный класс в регулярном выражении [Аа] состоит вовсе не из двух русских букв, а из символов U+D0, U+90, U+D0 и U+B0 (два из них совпали). С октета D0 начинается код UTF-8 у многих русских букв, так что не удивительно, что он встречается в названиях всех месяцев. Опять же ситуацию исправит директива.

Каждое строковое значение в программе на Perl кодируется при помощи UTF-8. Но в разных ситуациях оно рассматривается либо как октетная строка, либо как символьная. Соответствующий признак хранится вместе с октетами, составляющими строку. Он называется флагом UTF-8. При использовании директивы use utf8; все строки и регулярные выражения из текста программы устанавливают этот флаг, в отсутствие директивы этот флаг очищен.

Помимо процедуры length, состояние флага влияет и на некоторые другие встроенные процедуры языка Perl. Например, для процедуры substr важно, из чего состоят строки — из октетов или символов. Для процедуры chop флаг тоже имеет значение.

До сих пор мы обсуждали поддержку Юникода в текстах программ. Пришло время подумать о строках, которые программа получает извне, читая из файла, или выводит вовне, записывая в файл.

Когда в разделе «Чтение» мы рассматривали побайтное или посимвольное чтение в связи с процедурой read, мы не делали различия между этими двумя видами чтения, отождествляя байты (октеты) и символы. Теперь мы знаем, чем чревато такое отождествление. Третий параметр процедуры read отвечает за размер порции данных, которую желательно прочесть из дескриптора. Этот размер обычно задаётся в октетах, но если требуется читать заданное количество символов, файл следует открывать для чтения несколько иначе:

Perl
open my $file, '<:utf8', 'input.txt';

Дополнительная опция :utf8 указывает, что файл рассматривается как последовательность символов, закодированная в UTF-8. Все порции, прочитанные процедурой read или оператором <…>, будут символьными строками (то есть с установленным флагом UTF-8). Директива use utf8; не оказывает влияния на данную ситуацию.

Открывая файл для чтения таким образом, нужно быть уверенным, что в нём содержатся правильно закодированные данные (мы уже упоминали, что не всякая последовательность октетов обладает этим свойством). Что же произойдёт, если файл не таков? Каждый раз, когда при чтении встречается неправильная с точки зрения UTF-8 последовательность октетов, происходит ошибка чтения, а вместо такой неправильной последовательности считывается так называемый подстановочный символ U+FFFD, который выглядит так: . После такой неудачи чтение можно продолжать.

Аналогичным образом открывается файл для записи или добавления, если записываться будут именно символьные строки: в качестве второго параметра в процедуре open указывается '>:utf8' или '>>:utf8'.

Описанная возможность является частью более общего механизма, который позволяет соединить файловый ввод/вывод с дополнительными манипуляциями над считываемыми или записываемыми октетами. Это так называемые слои ввода/вывода, которые появились в Perl не так давно. Мы не ставим перед собой задачу подробно углубляться в эту тему (желающие могут почитать страницу руководства perlopentut). Но упомянем ещё одну возможность, которую дают слои.

Часто большие файлы с целью уменьшения их размера сжимают компрессором. Таких компрессоров несколько, но среди них наиболее популярны gzip и xz. Первый из них уступает второму в степени сжатия, но заметно выигрывает в быстродействии. В комплекте с компрессором поставляется и декомпрессор. Особый слой ввода/вывода позволяет читать и записывать файлы, сжатые при помощи gzip, не прибегая к услугам соответствующих программ — компрессора и декомпрессора:

Perl
open my $in, '<:gzip', 'input.txt.gz'; open my $out, '>:gzip', 'output.txt.gz';

Слой :gzip становится доступным в программе, если в системе установлен модуль PerlIO::gzip, который обеспечивает поддержку слою. Этот модуль не входит в стандартный набор модулей, и его придётся устанавливать дополнительно.

Слои ввода/вывода могут комбинироваться друг с другом. Если требуется читать символьные строки из сжатого файла, открываем его так:

Perl
open my $in, '<:gzip:utf8', 'input.txt.gz';

Для чтения или записи файлов в других кодировках будет полезен слой ввода/вывода :encoding(…). В скобках указывается название кодировки, разумеется, оно должно быть среди известных. В следующем примере программа читает по строкам файл input.txt в кодировке CP1251 и записывает его содержимое уже в кодировке CP866. Это простейший код, осуществляющий перекодировку.

Perl
open my $in, '<:encoding(CP1251)', 'input.txt'; open my $out, '>:encoding(CP866)', 'output.txt'; print {$out} $_ while <$in>; close $out; close $in;

Информатика-54© А. Н. Швец