Некоторые сведения о Perl 5
UTF-8
Юникод — это стандард кодирования символов, который включает в себя символы всех значимых алфавитов мира. Так как Perl использовался и используется для написания сетевых сервисов, само собой разумеющееся что в нём должна быть поддержка мультиязычности, а значит должны существовать способы выводить слова на разных языках.
Юникод состоит из двух частей: это большой справочник символов (UCS), в котором каждому символу делается однозначное сопоставление c числовым кодом (т.н. кодовая точка, англ. codepoint), записываемому обычно в виде положительного шестнадцатеричного числа с префиксом U+, например, U+041F или U+1F680, и стандартов кодирования этих символов (UTF).
В этом разделе мы рассмотрим стандарт кодирования UTF-8, который де-факто является самым распространенным в Интернете и который стал поддерживаться Perl из коробки, начиная с версии 5.6.0. Также будут рассмотрены возможности по кодированию и декодированию текстовых потоков в кодировке UTF-8.
Что такое UTF-8
[править]UTF-8 — это способ кодирования символов Юникода в виде одного или нескольких (до 4-х) идущих друг за другом байтов (о чем и говорит число 8 в названии). Преимуществом такого кодирования является то, что оно полностью совместимо с однобайтной кодировкой US-ASCII. Так, при кодировании от 0 до 127 кода (в десятеричной системе) используется всего один байт, а таблица кодовых точек Юникод полностью совпадает с US-ASCII. Далее от 128 до 2047 — двумя байтами; от 2048 до 65 535 — тремя байтами и, наконец, от 65 536 до 1 114 111 — четырьмя байтами. Теоретически данный способ кодирования позволяет использовать плюсом еще 2 байта, но того количества кодовых позиций, кодируемых 4-мя байтами, уже и так достаточно.
При однобайтном кодировании, UTF-8 полностью совместима с ISO-8859-x (может использоваться в Windows) и ASCII кодировками, что позволяет не волноваться о правильности выводимого текста, если в некоторой системе одна из этих кодировок используется по умолчанию. Тем не менее, при двухбайтном кодировании, UTF-8 и ISO-8859-x уже не взаимозаменяемы, так как символы в каждой из этих кодировок кодируются двумя байтами по-разному.
Рассмотрим, как это работает. Так как количество байтов при кодировании в этой системе переменно, нужны методы сообщать декодеру сколько байтов нужно отсчитать для одного символа в потоке. Число затрачиваемых байтов на символ сообщают старшие биты самого первого:
0xxxxxxx— если для кодирования потребуется один байт;110xxxxx— если для кодирования потребуется два байта;1110xxxx— если для кодирования потребуется три байта;11110xxx— если для кодирования потребуется четыре байта.
Последующие байты при многобайтном кодировании всегда имеют первые два бита 10.
Биты, помеченные x, являются значащими и участвуют в декодировании кодовой точки. Например, символ U+24 ($) кодируется одним байтом так (значащие биты подсвечены жёлтым)
00100100а символ U+10348 (𐍈)
11110000 10010000 10001101 10001000Если бы в Perl не было встроенной поддержки UTF-8, возможно пришлось бы писать собственный декодер. Давайте посмотрим, как декодер выглядел бы на языке Perl.
use strict;
use warnings;
sub encode_utf8 {
my $cp = shift;
#
# При кодировании кодовой точки, её значащие биты должны занять позиции
# после сигнальных битов в каждом октете.
#
# Определяем сколько байтов нужно для хранения символа:
# - один
if ($cp <= 0x7F) {
return pack("C", $cp);
}
# - два
elsif ($cp <= 0x7FF) {
return pack("C2",
(0xC0 | ($cp >> 6)), # Cдвигаем на 6, потому что эти
# 6 бит займут следующий байт
# и накладываем маску 1100 0000.
(0x80 | ($cp & 0x3F))); # Оставляем значащие биты маской 0011 1111
# и накладываем маску 1000 0000.
#
# Для большего числа байт мы поступаем по аналогии с учетом сигнальных битов.
#
}
# - три
elsif ($cp <= 0xFFFF) {
return pack("C3",
(0xE0 | ($cp >> 12)),
(0x80 | (($cp >> 6) & 0x3F)),
(0x80 | ($cp & 0x3F)));
}
# - четыре
elsif ($cp <= 0x10FFFF) {
return pack("C4",
(0xF0 | ($cp >> 18)),
(0x80 | (($cp >> 12) & 0x3F)),
(0x80 | (($cp >> 6) & 0x3F)),
(0x80 | ($cp & 0x3F)));
}
}
sub decode_utf8 {
# Формируем массив октетами - беззнаковый char.
my @b = unpack("C*", shift);
#
# При декодировании нужно сделать все в обратном порядке:
# удалить сигнальные биты и в старших октетах подвинуть значащие биты
# налево, на позиции сигнальных битов.
#
# Если первый октет меньше 128 это однобайтовый UTF-8.
if ($b[0] < 0x80) {
return $b[0];
}
# Если в первом октете старшие биты 110, то двухбайтный UTF-8
elsif (($b[0] & 0xE0) == 0xC0) {
return (($b[0] & 0x1F) << 6) | ($b[1] & 0x3F);
}
# Если в первом октете старшие биты 1110, то трехбайтный UTF-8
elsif (($b[0] & 0xF0) == 0xE0) {
return (($b[0] & 0x0F) << 12) | (($b[1] & 0x3F) << 6) | ($b[2] & 0x3F);
}
# Если в первом октете старшие биты 11110, то четырехбайтный UTF-8
elsif (($b[0] & 0xF8) == 0xF0) {
return (($b[0] & 0x07) << 18) |
(($b[1] & 0x3F) << 12) |
(($b[2] & 0x3F) << 6) |
($b[3] & 0x3F);
}
}
# Следующая кодовая позиция соответствует ракете 🚀 ...
my $rocket_cp = 0x1F680;
# ... которая кодируется 4-мя байтами.
my $bytes = encode_utf8($rocket_cp);
printf "Bytes (hex): %v02X\n", $bytes;
# Декодирование в кодовую позицию.
my $decoded = decode_utf8($bytes);
printf "Code Point: U+%X\n", $decoded;
use Unicode::UCD 'charinfo';
use Data::Dumper;
print Dumper(charinfo($rocket_cp));
Bytes (hex): F0.9F.9A.80
Code Point: U+1F680
$VAR1 = {
'combining' => 0,
'name' => 'ROCKET
'bidi' => 'ON',
'lower' => '',
'category' => 'So',
'unicode10' => '',
'numeric' => '',
'script' => 'Common',
'digit' => '',
'comment' => '',
'decimal' => ''
};
В этом примере мы используем пару сложных функций pack() и unpack(), которые позволяют нам соответственно записать и прочитать последовательность битов по некоторому шаблону (первый аргумент). В данном случае мы используем шаблон C, который означает unsigned char (байт, октет), а число указывает сколько октетов должно быть. При чтении битов, шаблон C* означает читать их октетами, сколько бы их не было.
Функция encode_utf8() преобразует символ в двоичную последовательность для передачи её куда либо дальше. Для этого программа, кодирующая в UTF-8, обычно просматривает таблицу UCS в поисках кодовой позиции для символа исходной кодировки и преобразует её в двоичное представление, как это делает encode_utf8(). Программа-приёмник некоторым образом узнаёт, что получает поток UTF-8 и декодирует входящие байты в кодовые позиции, что демонстрируется функцией decode_utf8(). Затем программе нужно просто найти отображение для кодовой позиции, если она работает в UTF-8, либо она может дальше перекодировать символ в свою родную кодировку.
В конце программы мы пользуется встроенным модулем Unicode::UCD, с помощью которого мы можем узнать, что кодирует переданная кодовая позиция.
Преимущества и недостатки
[править]Давайте начнем с преимуществ:
- К преимуществу стоит отнести то, что ваше приложение сможет выводить текст на родном для пользователя языке. В частности, в UTF-8 можно закодировать более 140 тысяч символов.
- UTF-8 обладает преемственностью к некоторым старым однобайтным и двухбайтным кодировкам, что улучшает портируемость приложений.
- Кодировка компактна (особенно для текстов, основанных на латинском алфавите) за счет того, что в ней тратится ровно столько байтов, сколько нужно. При передаче по сети это важно, так как это уменьшает количество передаваемого трафика. Также при хранении файлов в этой кодировке, они занимают меньше места, чем, например, в UTF-16 или UTF-32.
- Если при передаче теряется один или несколько байтов, то страдает один или несколько символов текста, но не весь текст.
- В UTF-8 порядок байтов всегда один и тот же, т.е. метка BOM для этой кодировки не нужна.
Из недостатков:
- Некоторые функции, которые работают с UTF-8 кодировкой, работают медленнее, так как им нужно проверять ошибочные ситуации кодирования.
- Некоторые символы могут формироваться из нескольких кодовых точек, что усложняет некоторые алгоритмы работы с текстом. Например, чтобы узнать длину строки (т.е. сколько знаков фактически напечатано), нужно пройти все байты от начала до конца, чтобы узнать границы символов.
- Обычно принимающая программа должна заранее знать, что работает с UTF-8, так как никаких автоматических признаков в ней не предусмотрено.
- Для некоторых алфавитов UTF-8 является тяжелой кодировкой. Например, русские буквы в UTF-8 занимают 2 байта, в то время как в старых кодировках (Windows-1251, KOI8-R) они занимали байт. Другими словами, памяти для текстов на кириллице нужно тратить больше, чем на латинице.
Perl и UTF-8
[править]Формально поддержка UTF-8 в Perl была начата с версии 5.6.0, однако, первые реализации работали недостаточно хорошо. Более чистая и плавная реализация началась с версии 5.14.
Изначально Perl хранит все строковые данные в однобайтовой нативной кодировке. Эта кодировка не совсем ASCII, так как все биты в ней значимые, т.е. она может закодировать 256 символов. Эта кодировка также используется для всего входящего/исходящего текста программы. При декодировании нативной строки специализированными функциями, если не указано иное, Perl использует кодировку ISO-8859-1, а не кодировку платформы.
Если вы укажете директиву
use utf8;
то тем самым скажете Perl, что в текущей лексической области следует интерпретировать все символы исходного кода в кодировке UTF-8. Например,
use utf8;
my $utf8_char = "🚀";
print "$utf8_char\n";
Эту директиву следует включать только в случаях, когда в исходном коде очень много текста в UTF-8 кодировке. В остальных случаях её не следует включать, а функции из пакета utf8 следует вызывать через полное квалифицированное имя.
По возможности Perl всегда старается пользоваться нативной кодировкой, но если символ использует для хранения больше одного байта, Perl будет пытаться применить UTF-8 декодер. Так, в следующем примере строки в первых трех переменных будут интерпретироваться в нативной кодировке только потому, что занимают один байт, а в переменной $utf8_string уже хранится два байта, поэтому внутренне будет применяться UTF-8 декодер.
$native_string = "\xf1"; print "$native_string\n";
$native_string = "\x{00f1}"; print "$native_string\n";
$native_string = chr(0xf1); print "$native_string\n";
$utf8_string = "\x{0100}"; print "$utf8_string\n";
При попытке напечатать последнюю строку, Perl будет выдавать предупреждение о печати многобайтной строки (Wide character in print), потому что не знает, в какую кодировку её трансформировать на лету, поэтому напечатает её в сыром виде. Далее мы рассмотрим, как такие ситуации разрешать правильно.
Если вы намеренно желаете однобайтные символы вывести из нативной кодировки в UTF-8, то следует использовать встроенную функцию utf8::upgrade()
$my_string = "\xf1";
print utf8::upgrade($my_string), "\n";
Принципы написания кода с поддержкой UTF-8
[править]- Следует помнить, что программа не должна сама делать предположений о кодировке входящих октетов. Эту информацию должен предоставить клиентский код.
- После того, как программа получит информацию о входящей кодировке, например UTF-8, она обычно либо продолжает работать с этой кодировкой, либо декодирует её в более удобную для себя. Затем полученный текст обрабатывается и кодируется в исходящую кодировку.
- Если нужно открыть файл для чтения из него в программу с автоматическим декодированием из UTF-8, то дескриптор нужно открыть с указанием кодировки в режиме открытия:
open (FILE, "<:encoding(UTF-8)", $filename) || die "Cannot open file: $!";
- Если программа получает данные в UTF-8 кодировке через стандартный поток ввода, то следует использовать операцию
binmodeна дескрипторе (и на любом другом дескрипторе) следующим образомbinmode STDIN, ":encoding(UTF-8)";
- Если вам нужно, чтобы все новые дескрипторы открывались в режиме
:encoding(UTF-8), вы можете использовать директивуopen, напримерЧтобы учитывать стандартные дескрипторы STDIN, STDOUT и STDERR, можно добавить флаг# К новым дескрипторам слой :encoding(UTF-8) будет применяться автоматически use open qw(:encoding(UTF-8)); open FILE, "./utf8.txt"; # Дескриптор файла открыт на чтение следующим образом "< :encoding(UTF-8)". open OUT, ">", "test.txt"; # Дескриптор файла открыт на запись следующим образом "> :encoding(UTF-8)". binmode STDOUT, ":utf8"; # Так как мы печатаем на терминал широкие символы. while (<FILE>) { my $s = $_; print "$s"; print OUT $s; }
:std:Следует помнить, что директиваuse open qw(:std :encoding(UTF-8)); # Добавляем флаг :std open FILE, "./utf8.txt"; open OUT, ">", "test.txt"; # Обратите внимание, что binmode больше не нужна. while (<FILE>) { my $s = $_; print "$s"; print OUT $s; }
openработает только в текущем лексическом пространстве. - Еще один способ принимать данные из файлов — это использовать функцию
decode()из пакетаEncode. Он позволяет работать как с голыми строками, так и с файловыми дескрипторами при этом не ограничиваясь входящей кодировкой.Пакетuse Encode qw(decode); my $utf8_text = decode('UTF-8', readline STDIN); # Преобразует входящую UTF-8 кодировку во внутреннее представление. my $iso8859_text = decode('ISO-8859-1', readline STDIN);
utf8также предоставляет функциюdecode, которая позволяет пометить на месте, что некоторая переменная хранит UTF-8 строку, но обычно лучше пользоваться пакетомEncode:utf8::decode($string)
- После того как все входящие строки будут преобразованы во внутреннее представление, вы можете работать с ними как с обычными строками. Регулярные выражения также будут работать с ними, начиная с версии Perl 5.8, однако до версии 5.14 наблюдались некоторые проблемы, поэтому рекомендуется не использовать интерпретатор ниже Perl 5.14.
- Если в исходном коде вы объявляете строки, в которых символы кодируются несколькими байтами (код больше 127 в десятеричной системе), следует использовать функцию
utf8::upgrade()my $text = "\xE0"; utf8::upgrade($text); my $unicode_char = "\x{00f1}"; utf8::upgrade($unicode_char);
- Если вы печатаете текст в стандартные потоки вывода STDOUT и STDERR функциями Perl, не закодировав их, он будет отдавать их в так называемом сыром виде (raw), который может представлять мешанину из внутренней кодировки Perl и UTF-8. Если устройство вывода не ожидает этого, результат будет непредсказуемым. Обычно Perl предупреждает такие ситуации, когда вы пытаетесь печатать многобайтные символы без нужных переключений:По этой причине, если ваше приложение ориентировано на работу с многобайтными кодировками, любой вывод должен кодироваться явно специализированными функциями.
# В командной оболочке Bash: $ perl -e 'print "\x{0100}\n"' Wide character in print at -e line 1. Ā # В этом примере устройство вывода декодировало символ правильно, потому что оно работает # в локали LANG=ru_RU.UTF-8. Тем не менее, Perl предупредил вас, что байты выводятся сырыми, и # в других условиях вывод был бы непредсказуем.
- Если вы уверены, что любая строка, которая выводится в STDOUT (STDERR) уже закодирована в UTF-8, то в начале программы можно переключить режим вывода для дескриптора такТак, предупреждение пропадет, если вызов будет таким
binmode STDOUT, ":encoding(utf8)";
$ perl -e 'binmode STDOUT, ":encoding(utf8)"; print "\x{0100}\n"' Ā
- Вывод в стандартный дескриптор UTF-8 строк во многом зависит от того, как они были сформированы. Ниже показан комплексный пример, в котором показано, как UTF-8 строки могут попадать на печать правильным способом.Если ваш терминал работает с кодировкой UTF-8, результатом будет вывод одной и той же строки 5 раз подряд:
# Файл: utf8.pl use Encode qw { encode }; $variant_0 = "\x{041f}\x{0440}\x{0438}\x{0432}\x{0435}\x{0442}\x{0020}\x{043c}\x{0438}\x{0440}\x{0021}"; # Сырое представление. use utf8; $variant_1 = "Привет мир!"; # Внутреннее UTF-8 представление. no utf8; $variant_2 = "\x{041f}\x{0440}\x{0438}\x{0432}\x{0435}\x{0442}\x{0020}\x{043c}\x{0438}\x{0440}\x{0021}"; # Сырое представление. $variant_3 = encode("UTF-8", $variant_2); # Кодирование в UTF-8 со всеми строгими проверками. $variant_4 = "\x{041f}\x{0440}\x{0438}\x{0432}\x{0435}\x{0442}\x{0020}\x{043c}\x{0438}\x{0440}\x{0021}"; # Сырое представление. utf8::upgrade($variant_0) if utf8::is_utf8($variant_0); # Функция переводит строку во внутреннее UTF-8 представление. # Perl понимает это по внутреннему флагу. utf8::encode($variant_2) if utf8::is_utf8($variant_2); # Низкоуровневое кодирование без проверок соответствия таблице Юникод. binmode STDOUT, ":encoding(utf8)"; # Переключаем дескриптор в автоматическое кодирование в UTF-8 при выводе. print "$variant_0\n"; # МОЖНО, потому что строка внутренне представлена в UTF-8 кодировке. print "$variant_1\n"; # МОЖНО, потому что строка внутренне представлена в UTF-8 кодировке. print "$variant_4\n"; # МОЖНО, потому что Perl сам перекодирует строку, так как он работает в этом режиме. binmode STDOUT, ":raw"; # Переключаем дескриптор в сырое кодирование. Программа сама отвечает за выводимые символы. # Если терминал ожидает кодировку UTF-8 вывод будет правильным без предупреждений, потому что # строки уже закодированы в эту кодировку специализированными функциями. print "$variant_2\n"; print "$variant_3\n";
Строкаperl ./utf8.pl Привет мир! Привет мир! Привет мир! Привет мир! Привет мир!
$variant_0была преобразована во внутреннее представление UTF-8 функциейutf8::upgrade($variant_0). Данная функция является низкоуровневой и не делает проверок соответствия Юникоду. Тем не менее, в нашем примере мы переводим правильно закодированную строку. Строка$variant_1находится в пределах директивыutf8и кодируется в UTF-8 во время компиляции. Строка$variant_2кодируется низкоуровневойutf8::encode(). Такое кодирование правильно только с технической стороны, но результат не обязательно должен быть правильным с точки зрения Юникода. Строка$variant_3кодируется функцией модуляEncode. В отличие от низкоуровневых функций модуляutf8, все строки строго проверяются на соответствие заказываемой кодировке (в нашем случае Юникоду). Строка$variant_4вообще никак не кодируется: это сделано намеренно, чтобы показать, как это делается на уровне дескриптора. Строки$variant_0,$variant_1и$variant_4выводятся в STDOUT, который был заранее переключен в режим:encoding(utf8). Если строка уже представляет UTF-8, внутренний декодер пропускает её как есть, иначе он закодирует её в UTF-8 со всеми проверками, как это делается со строкой$variant_4. Так как интерпретатор заранее знает, что ему делать с сырыми байтами, никаких предупреждений выводиться не будет. Строки$variant_2и$variant_3выводятся в режиме:raw, т.е. мы говорим интерпретатору, что будем сами отвечать за все байты, которые будут проходить через дескриптор. Так как наши строки закодированы в приложении правильно, а также терминал работает с кодировкой UTF-8, наши строки выводятся в последних двух случаях правильно. - При печати в файлы во многом все аналогично вышесказанному. Ниже приведен небольшой пример.
# Файл: utf8_file.pl use Encode; $file = "/tmp/utf8_text.tmp"; open OUT, ">:utf8", $file or die "Cannot open file to write: $!"; $variant_0 = "\x{041f}\x{0440}\x{0438}\x{0432}\x{0435}\x{0442}\x{0020}\x{043c}\x{0438}\x{0440}\x{0021}"; OUT->print($variant_0); OUT->close; open IN, "<:encoding(utf8)", $file or die "Cannot open file to read: $!"; binmode STDOUT, ":raw"; # Так как дескриптор, в который мы печатаем, работает в raw режиме, внутреннее # представление мы обязаны кодировать всегда явно. print Encode::encode("UTF-8", readline IN), "\n"; # Но если переключить дескриптор явно в режим кодирования, то мы # можем положиться на внутренний кодер. binmode STDOUT, ":encoding(utf8)"; seek(IN, 0, 0); print readline IN, "\n"; IN->close;
В этом примере мы сначала открыли файл на запись, при этом указав при открытии, что мы будем пользоваться кодировкой UTF-8 при печати в него символов, и записываем в него сырую строку. Затем мы закрываем этот файл и открываем его на чтение в режиме UTF-8. После этого мы показываем, как можно напечатать прочитанную строку на терминальное устройство. В первом случае дескриптор STDOUT включен в raw-режиме. Так как$ perl utf8_file.pl Привет мир! Привет мир!
readlineна дескриптореINзадействует кодер UTF-8, то он преобразует строку из файла из UTF-8 во внутреннее представление Perl. Так как внутреннее представление в многобайтной кодировке, а дескриптор STDOUT работает в сыром режиме, мы обязаны кодировать внутреннее представление в UTF-8 (в данном примере) явно (в этом примере мы пользуемся модулемEncode), иначе при печати будет выводиться предупреждение. Если мы уверены, что внутреннее представление будет корректно кодироваться в UTF-8, то мы можем просто переключить дескриптор STDOUT в режим:encoding(utf8), тогда входящий и исходящий UTF-8 кодеры в этом случае автоматически будут согласованы. Однако, помните, что внутреннее представление не обязательно может быть закодировано в UTF-8, поэтому вы должны всегда понимать, что делаете. - Флаг
:utf8, обычно используемый в операцииbinmode, говорит о том, что исходящий поток является потоком UTF-8, но не делает проверок в их кодировании для производительности. Этот флаг может быть применен и на входящем дескрипторе, но это крайне не рекомендуется делать, так как программа не должна доверять входящему источнику данных.binmode OUT, ':utf8';
В заключении хотелось бы отметить, что многие CPAN-модули, ориентированные на мультиязычность, для вашего удобства, проворачивают действия по кодированию информации где-то внутри себя, и делают это они очень похоже на то, как это было описано выше. Не забывайте читать документацию, чтобы не получать неожиданных выводов на печать.
UTF-8 и utf8
[править]Вы могли заметить, что в разных местах псевдоним кодировки UTF-8 записывается в разном стиле. Это последствия несогласованности в разработке, с которой нужно мириться. При отсутствии должного внимания, вы можете допустить ошибку в написании этого идентификатора и при этом не получите никакого предупреждения от интерпретатора. Программа при этом скорее всего запуститься, но будет работать неправильно.
Следует запомнить следующие правила:
- Для модуля
EncodeВСЕГДА должна использоваться строгая записьUTF-8. При этом модуль проверяет все идентификаторы и безопасно падает, если не знает их. - Для встроенных в Perl операций и функций в большинстве ситуаций не важен регистр букв в идентификаторе, а также работают формы с тире и без тире:По возможности, для единообразия, стоит отдавать предпочтение записи
# Работают одинаково open IN, "<:encoding(UTF-8)", $file; open IN, "<:encoding(utf8)", $file; open IN, "<:encoding(utf-8)", $file;
:encoding(UTF-8). - Записи
:encoding(utf8)и:utf8следует отличать друг от друга. В первом случае запускается строгий кодер, который проверяет все входящие/исходящие биты и падает, если кодировка не соответствует правилам кодирования UTF-8. Во-втором случае, это просто флаг, который делает отметку, что поток битов представляет собой поток UTF-8, но не делает проверок правильности кодирования байтов этого потока. Этот флаг обычно используется для ускорения чтения/записи, когда вы уверены в источнике данных. Обычно флаг поднимается только для исходящей записи из программы для ускорения. Для входящих данных ВСЕГДА должен использоваться строгий кодер. - Флаг
:utf8ВСЕГДА записывается без тире, в отличие от формы:encoding(utf-8). - Директива
utf8записывается без тире.
Модуль Encode или модуль utf8
[править]Так как модуль Encode и встроенный модуль utf8 очень похожи с точки зрения работы с кодировкой UTF-8, возникает логичный вопрос, в каких ситуациях пользоваться каждым их них. В пользу Encode выступает то, что он следует стандарту UTF-8 строго и выявляет все ошибки кодирования. Из-за этой строгости программа будет работать надёжнее, но за надёжность придется расплачиваться производительностью. Разумно пользоваться модулем Encode на входе и на выходе приложения, так как программа не должна полностью доверять правильности кодировки входящих данных, а также должна отдавать данные наружу правильно закодированными. Кроме того, модуль Encode позволяет работать с множеством кодировок, что всегда требуется, если программу планируется запускать и в Windows и *nix системах. Чтобы узнать, какие кодировки поддерживает модуль конкретно в вашем дистрибутиве, можно ввести такую команду:
$ perl -MEncode -le "print for Encode->encodings(':all')"
Функциями модуля utf8, напротив, стоит пользоваться внутри приложения для реализации бизнес-логики, в которой участвуют символы разных алфавитов. Некоторые функции из этого модуля позволяют работать с UTF-8 быстрее. Ниже в таблице перечислены функции из этого модуля.
| Функция | Описание |
|---|---|
$num_octets = utf8::upgrade($string)
|
Конвертирует строку из внутреннего представления Perl в строку UTF-8, сохраняя логическую последовательность символов в строке. Возвращает количество октетов, которое нужно потратить для хранения новой строки. Если строка уже находится в кодировке UTF-8, то ничего не делает. До версии Perl 5.38, если аргументом было значение undef, то конструировалась строка нулевой длины, но с версии 5.38 функция просто ничего не делает.
|
$success = utf8::downgrade($string[, $fail_ok])
|
Наоборот, преобразует UTF-8 строку в эквивалентную ей форму во внутренней кодировке Perl. Если строка уже находится во внутренней кодировке, то ничего не делает. Возвращает ИСТИНУ, если преобразование удалось. Преобразование может привести к критической ошибке, если некоторый символ не может быть преобразован во внутреннюю кодировку. В этом случае флаг $fail_ok подсказывает программе, что делать: если флаг установлен в ИСТИНУ, программа завершается через die, иначе управление возвращается основной программе с результатом ЛОЖЬ.
|
utf8::encode($string)
|
Преобразует исходную строку в многобайтную строку с правилами кодирования UTF-8 на месте. Флаг внутреннего представления UTF-8 опускается. По смыслу функция похожа на Encode::encode("UTF-8", ...), но не по строгости.
|
$success = utf8::decode($string)
|
Пытается преобразовать многобайтную строку UTF-8 в строку внутреннего представления Perl на месте. Если в результате преобразования в строке появляются символы, для хранения которых требуется больше одного байта, внутренний флаг UTF-8 поднимается. Возвращает ИСТИНУ, если такое преобразование возможно. По смыслу функция похожа на Encode::decode("UTF-8", ...), но не по строгости. В прошлом, у модуля Encode была проблема с тем, что при декодировании внутренний флаг UTF-8 был поднят даже в том случае, когда в строке не было ни одного многобайтного символа, но сейчас это уже история[1].
|
$unicode = utf8::native_to_unicode($code_point)
|
Функция принимает целое число, которое представляет порядковый номер символа (или кодовой точки) на платформе, на которой запущена программа, и возвращает его эквивалентное значение в Юникоде. На платформах с кодировкой EBCDIC она преобразует данные из EBCDIC в Юникод. Если входные данные не являются целым числом без знака, возвращает бессмысленное значение. |
$native = utf8::unicode_to_native($code_point)
|
Наоборот, выполняет преобразование кодовой точки Юникод в код кодировки целевой платформы. Если входные данные не являются беззнаковым целым числом, возвращается бессмысленное значение. |
$flag = utf8::is_utf8($string)
|
Возвращает истину, если строка закодирована в UTF-8. По смыслу похожа на Encode::is_utf8($string).
|
$flag = utf8::valid($string)
|
Внутренняя функция модуля. Возвращает ИСТИНУ, если строка находится в согласованном состоянии с точки зрения UTF-8: поднят внутренний флаг UTF-8 или строка хранится байтами. |
Юникод и регулярные выражения
[править]Регулярные выражения в Perl способны учитывать Юникод, однако, нужно помнить об особенностях этого процесса.
- Один печатаемый символ может быть записан несколькими кодовыми точками. Метасимвол точка (
.) в регулярных выражениях соответствует именно кодовой точке. Например, символàможет быть записан какU+0061 U+0300, так иU+00E0, при этом регулярное выражение/./для первой формы будет соответствовать только кодовой точкеU+0061(a), т.е. диакритический знак не будет учитываться, а для второй формы все будет работать верно, так как символ записан в нормализованной форме. Чтобы учитывался символ грависа, регулярное выражение должно быть записано как/../, когда символ записан, например, двумя кодовыми точками, но на практике так не делают. - Чтобы учитывать символы, записанные несколькими кодовыми точками, в Perl используется метасимвол
\X— супер-точка для Юникод символов. Этот метасимвол соответствует записи(?>\P{M}\p{M}*). - Чтобы учитывать в регулярных выражениях конкретную кодовую точку, нужно использовать запись
\xNNNNили\x{NNNN}, где N — шестнадцатеричная цифра. - Каждый символ Юникода имеет набор свойств (properties), по которым они могут объединяться в группы. Чтобы в регулярных выражениях искать символы по свойствам, используется синтаксис
\p{}или\P{}(Unicode Property Escape), где между фигурными скобками записывается имя свойства. Запись\p{}говорит, что вам интересны сопоставления символов по этому свойству, а\P{}наоборот — не соответствие по этому свойству. В Perl поддерживается огромное множество свойств, с которыми лучше знакомиться из документации — perluniprops. Здесь отметим, что имена свойств могут быть записаны в строгой или не строгой (сокращенной) форме. Вам следует запомнить наиболее часто встречаемые:\p{L}или\p{Letter}— буква некоторого алфавита.\p{Lu}или\p{Uppercase_Letter}— буква в верхнем регистре.\p{Lt}или\p{Titlecase_Letter}— диграф с заглавной первой буквой.\p{Cased_Letter}— буква, которая существует в строчном и прописном вариантах.\p{Lm}или\p{Modifier_Letter}— специальный символ, используемый как буква, напримерʰ.\p{Other_Letter}— буква или идеограмма, не имеющая строчных и прописных вариантов, например, китайские иероглифы.
\p{M}или\p{Mark}— символ, предназначенный для сочетания с другим символом.\p{Mn}или\p{Non_Spacing_Mark}— символ, который сочетается с другим символом, но не занимающий отдельный глиф, например, знаки ударения и умлауты.\p{Mc}или\p{Spacing_Combining_Mark}— символ, предназначенный для сочетания с другим символом, занимающий дополнительное место.\p{Me}или\p{Enclosing_Mark}— символ, который окружает символ, с которым он сочетается, например 1️⃣.
\p{Z}или\p{Separator}— любые пробельные символы или невидимые разделители.\p{Zs}или\p{Space_Separator}— символ пробела, который невидим, но занимает место.\p{Zl}или\p{Line_Separator}— разделитель строк U+2028.\p{Zp}или\p{Paragraph_Separator}— разделитель параграфов U+2029.
\p{S}или\p{Symbol}— математические символы, знаки валют, дингбаты, символы для рисования и т.д.\p{Sm}или\p{Math_Symbol}— любой математический символ.\p{Sc}или\p{Currency_Symbol}— любой символ валюты (кроме некоторых).\p{Sk}или\p{Modifier_Symbol}— объединяющий символ (знак) как самостоятельный полный символ, например, тильда~.\p{So}или\p{Other_Symbol}— различные символы, не относящиеся к прошлым подкатегориям.
\p{N}или\p{Number}— любой числовой символ любой письменности.\p{Nd}или\p{Decimal_Digit_Number}— цифра от нуля до девяти в любой письменности, кроме идеографической.\p{Nl}или\p{Letter_Number}— число, которое выглядит как буква, например, римская цифра.\p{No}или\p{Other_Number}— надстрочная или подстрочная цифра, или число, не являющееся цифрой 0-9 (исключая числа из идеографических письменностей).
\p{P}или\p{Punctuation}— любой символ пунктуации.\p{Pd}или\p{Dash_Punctuation}— любой вид дефиса или тире.\p{Ps}или\p{Open_Punctuation}— любой вид открывающей скобки\p{Pe}или\p{Close_Punctuation}— любой вид закрывающей скобки.\p{Pi}или\p{Initial_Punctuation}— любой вид открывающей кавычки\p{Pf}или\p{Final_Punctuation}— любой вид закрывающей кавычки.\p{Pc}или\p{Connector_Punctuation}— символ пунктуации, такой как подчёркивание, который соединяет слова.\p{Po}или\p{Other_Punctuation}— любой знак препинания, не относящийся к прошлым подкатегориям.
\p{C}или\p{Other}— невидимые управляющие символы и неиспользуемые коды.\p{Cc}или\p{Control}— управляющие ASCII или Latin-1 символы: 0x00–0x1F и 0x7F–0x9F.\p{Cf}или\p{Format}— невидимый индикатор форматирования.\p{Co}или\p{Private_Use}— любой код, зарезервированный для частного использования.\p{Cs}или\p{Surrogate}— одна половина суррогатной пары в кодировке UTF-16.\p{Cn}или\p{Unassigned}— любой код, которому не присвоен ни один символ.
- Кроме того, символы Юникода принадлежат одной из многих письменностей. Некоторые категории соответствуют одному языку. Другие, напротив, относятся к нескольким языкам (например, Latin). Например, чтобы указать категорию письменности Кириллица, нужно использовать запись
\p{Cyrillic}. - Юникод делит карту символов на различные блоки или диапазоны кодов. Каждый блок используется для определения символов конкретной письменности. Например кириллица относится к блоку
\p{Block: Cyrillic}. Блоки Юникода не совпадают на 100% с категориями письменностей. Также письменность отличается от блока тем, что блок это непрерывный диапазон кодов, в то время как символы одной категории могут быть раскиданы по разным частям таблицы Юникода. Также категории никогда не включают ещё неназначенные коды.
Примеры
[править]Нормализация
[править]В Юникоде некоторые символы могут быть закодированы несколькими способами. Например, русские буквы ё и й могут кодироваться двумя или одной кодовыми позициями Юникода. Соответственно алгоритмы лексикографического сравнивания перестают нормально работать, потому что изначально они сравнивают коды символов в ожидании, что на один символ тратится не больше одного кода.
Юникод для таких ситуаций предусматривает алгоритмы нормализации, в частности каноническая композиция и каноническая декомпозиция. В первом случае символ, состоящий в потоке из нескольких кодовых позиций временно может быть представлен одной кодовой позицией, а во втором — можно представить символ из одной кодовой позиции несколькими, если этот символ на них может быть разложен.
В Perl есть модуль Unicode::Normalize, который реализует алгоритмы нормализации Юникод. Рассмотрим типичный пример сортировки слов, записанных кириллицей.
# Файл: unicode_sort.pl
use strict;
use warnings;
use utf8;
use Unicode::Normalize;
binmode(STDOUT, ":utf8");
my @words = (
"яблоко",
"икра",
"е\x{0308}лка",
"йогурт",
"и\x{0306}од",
"арбуз",
"ёлка",
"ель"
);
# --- ПЛОХАЯ СОРТИРОВКА (без нормализации) ---
my @bad_sort = sort @words;
print "Обычная сортировка:\n";
foreach (@bad_sort) {
# Специально выводим длину, чтобы увидеть разницу
printf " %-10s (длина: %d)\n", $_, length($_);
}
# --- ПОЧТИ ПРАВИЛЬНАЯ СОРТИРОВКА (с нормализацией NFC) ---
# Мы нормализуем слова только ВНУТРИ блока sort для сравнения
my @good_sort = sort { NFC($a) cmp NFC($b) } @words;
print "\nСортировка с NFC:\n";
foreach (@good_sort) {
printf " %-10s (длина: %d)\n", $_, length($_);
}
В этом примере есть слова йод, йогурт и ёлка, причем буква й в слове йод записана двумя кодовыми позициями, в слове йогурт — одной, и слово ёлка также написано разными стилями два раза . Программа должна выполнять сортировку корректно, независимо от того, как представлены символы.
В программе показан первый способ сортировки (без нормализации), который выдает такой результат
... Обычная сортировка: арбуз (длина: 5) ёлка (длина: 5) ель (длина: 3) йод (длина: 4) икра (длина: 4) йогурт (длина: 6) яблоко (длина: 6) ёлка (длина: 4) ...
Очевидно, что результат правильный отчасти: йогурт и слово ёлка, в которых первый символ записан двумя кодовыми позициями оказались не на своих местах, потому что функция сортировки учитывала обе кодовых позиции для одного символа. В данном случае мы должны выполнить композицию, чтобы вторая кодовая позиция для символов ё и й исчезли. Для нормализации мы используем функцию Unicode::Normalize::NFC(). Сортировка с нормализацией дает результат получше, но все равно не идеальный:
... Сортировка с NFC: арбуз (длина: 5) ель (длина: 3) икра (длина: 4) йогурт (длина: 6) йод (длина: 4) яблоко (длина: 6) ёлка (длина: 5) ёлка (длина: 4) ...
Стало лучше, но слова «ёлка» оказались в конце списка, когда ожидалось, что они будут после буквы е. Это происходит потому, что кодовая позиция буквы ё стоит отдельно от других символов кириллицы и имеет самое большое значение.
Юникод и для таких случаев предусмотрел стандарт Unicode Technical Standard #10, который описывает правила сравнивания и сортировки символов Юникод. В Perl его реализует модуль Unicode::Collate. С использованием этого модуля программа становится намного короче:
use strict;
use warnings;
use utf8;
use Unicode::Collate;
binmode(STDOUT, ":utf8");
my @words = (
"яблоко",
"икра",
"е\x{0308}лка",
"йогурт",
"и\x{0306}од",
"арбуз",
"ёлка",
"ель"
);
my $collator = Unicode::Collate->new();
my @good_sort = $collator->sort(@words);
print "\nСортировка с UTS:\n";
foreach (@good_sort) {
printf " %-10s (длина: %d)\n", $_, length($_);
}
Сортировка с UTS: арбуз (длина: 5) ёлка (длина: 5) ёлка (длина: 4) ель (длина: 3) икра (длина: 4) йогурт (длина: 6) йод (длина: 4) яблоко (длина: 6)
Для сортировки нам достаточно построить экземпляр класса Unicode::Collate. Класс имеет встроенный метод sort(), который самостоятельно справляется с нормализацией.
Но, как видно из вывода, проблема с буквой ё так и не решилась полностью. Это происходит потому, что вес буквы е остается выше буквы ё. Чтобы повысить вес, нужно вмешиваться во внутренние алгоритмы класса Unicode::Collate. Из работающих решений автор нашел такое:
use strict;
use warnings;
use utf8;
use Unicode::Collate;
use Unicode::Normalize;
binmode(STDOUT, ":utf8");
my @words = (
"яблоко",
"икра",
"е\x{0308}лка",
"йогурт",
"и\x{0306}од",
"арбуз",
"ёлка",
"ель"
);
my $collator = Unicode::Collate->new(
preprocess => sub {
my $s = shift;
$s =~ s/ё/е\x{FFFF}/g;
return $s;
}
);
my @good_sort = $collator->sort(map { NFC($_) } @words);
print "\nСортировка с UTS:\n";
foreach (@good_sort) {
printf " %-10s (длина: %d)\n", $_, length($_);
}
Здесь слова нормализуются до сортировки, чтобы убрать лишние кодовые точки. Затем используется приём, в котором все буквы ё временно заменяются на е, к которым добавляется невидимый хвост в виде символа U+FFFF. Этот символ не имеет отображения, при этом он как бы утяжеляет букву е, заставляя алгоритм сортировки помещать её после обычной е, но раньше буквы ж. Результат работы показан ниже:
Сортировка с UTS:
арбуз (длина: 5)
ель (длина: 3)
ёлка (длина: 4)
ёлка (длина: 4)
икра (длина: 4)
йогурт (длина: 6)
йод (длина: 3)
яблоко (длина: 6)
В заключение отметим, что у класса Unicode::Collate есть потомок Unicode::Collate::Locale, который расширяет базовый класс, позволяя управлять локалью программно. Эквивалентный код можно записать и так тоже:
use strict;
use warnings;
use utf8;
use Unicode::Collate::Locale;
use Unicode::Normalize;
binmode(STDOUT, ":utf8");
my @words = (
"яблоко",
"икра",
"е\x{0308}лка",
"йогурт",
"и\x{0306}од",
"арбуз",
"ёлка",
"ель"
);
my $collator = Unicode::Collate::Locale->new(
locale => 'ru',
level => 3,
preprocess => sub {
my $s = NFC(shift);
$s =~ s/ё/е\x{FFFF}/g;
return $s;
},
normalization => 'prenormalized',
);
my @good_sort = $collator->sort(@words);
print "\nСортировка с UTS:\n";
foreach (@good_sort) {
printf " %-10s (длина: %d)\n", $_, length($_);
}
Автор также пробовал использовать Unicode::Collate::Locale версии 1.31 со следующей последовательностью параметров
my $collator = Unicode::Collate::Locale->new(
locale => 'ru',
level => 3,
normalization => 'NFC',
);
но по какой-то причине слова на букву ё остаются раньше е, даже несмотря на то, что установлен третичный уровень.
Определение входящей кодировки
[править]Давайте расширим предыдущий пример и попытаемся написать аналог консольной утилиты sort. Утилита умеет принимать строки в текущей локали из стандартного потока ввода и выводить их в сортированном виде, что мы и попытаемся реализовать. Отчасти задача сортировки нами уже решена через Unicode::Collate, осталось только настроить получение строк из стандартного потока ввода и правильно их кодировать/декодировать.
К сожалению, написание кроссплатформенного решения это непростая задача. Если утилита работает только в *nix системе, то задача облегчается тем, что все современные *nix системы обычно по умолчанию используют UTF-8. Однако, в такой системе как Windows мало того что нет понятия локали, так ещё и информация об используемой кодировке хранится в системном реестре.
В первом приближении можно написать следующую программу:
# Файл: sort.pl
use strict;
use warnings;
use utf8;
use Encode;
use Encode::Locale;
use Unicode::Collate;
binmode(STDIN, ":encoding(console_in)");
binmode(STDOUT, ":encoding(console_out)");
binmode(STDERR, ":encoding(console_out)");
my @to_sort;
while (my $line = <STDIN>) {
chomp $line;
push @to_sort, $line unless $line !~ /\S/;
}
exit 0 if @to_sort == 0;
my $collator = Unicode::Collate->new(
level => 1
);
print join "\n", $collator->sort(@to_sort);
exit 0;
Здесь мы используем CPAN модуль Encode::Locale. Данный модуль автоматически определяет кодировку консоли и вводит специальные псевдонимы console_in и console_out для передачи правильных значений для кодировок. Соответственно нам достаточно только переключить стандартные дескрипторы с помощью этих псевдонимов.
В *nix системе все работает без нареканий:
echo -e "ы\nр\nк\nп\nо\nф" | perl sort.pl
к
о
п
р
ф
ы
Но как только мы попытаемся запустить эту же программу из Powershell в Windows, мы получаем следующее:
> $multiLine = @"
>> ы
>> р
>> к
>> п
>> о
>> ф
>> "@
> $multiLine | perl sort.pl
?
?
?
?
?
?
Это происходит потому, что Powershell передает строку через конвейер немного не так, как это делается в *nix. При отправке данных через Pipe-канал используется переменная $OutputEncoding, которая обычно инициализируется значением US-ASCII. Следовательно символы превратились в вопросы ещё до того, как они попали в программу.
Если передавать данные из буфера клавиатуры, то все будет работать нормально:
> perl sort.pl
ф
ы
а
п
р
в
^Z
а
в
п
р
ф
ы
Данную проблему пользователь может решить, если выставит многобайтную кодовую страницу (например, cp65001) явно, например так:
> [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
> [Console]::InputEncoding = [System.Text.Encoding]::UTF8
> $OutputEncoding = [System.Text.Encoding]::UTF8
> $multiLine | perl sort.pl
к
о
п
р
ф
ы
К сожалению, модуль Encode::Locale требуется устанавливать, так как он не является CORE-модулем. Мы можем написать другое решение, используя только стандартные модули, например так:
# Файл: sort_pure.pl
use strict;
use warnings;
use Encode;
use Unicode::Collate;
my $console_enc;
if ($^O eq 'MSWin32') {
my $cp_info = `chcp`;
if ($cp_info =~ /(\d+)/) {
$console_enc = "cp$1";
} else {
$console_enc = "cp866";
}
} else {
$console_enc = $ENV{LC_ALL} || $ENV{LC_CTYPE} || $ENV{LANG} || 'UTF-8';
$console_enc =~ s/.*\.//;
}
binmode(STDIN, ":raw");
binmode(STDOUT, ":raw");
my @to_sort;
while (my $line = <STDIN>) {
chomp $line;
push @to_sort, Encode::decode($console_enc, $line) unless $line !~ /\S/;
}
exit 0 if @to_sort == 0;
my $collator = Unicode::Collate->new(
level => 1
);
print join "\n", map { Encode::encode($console_enc, $_) } $collator->sort(@to_sort);
exit 0;
В этом решении мы вызываем утилиту chcp для Windows, которая возвращает нам текущую кодовую таблицу. Далее мы пытаемся найти нужную нам подстроку в выводе и инициализируем ей вызовы функций модуля Encode. Для *nix систем мы пытаемся найти данные о локали через переменные окружения. Далее мы работаем с дескрипторами ввода/вывода в сыром режиме, полагаясь исключительно на функционал модуля Encode.
Решение является рабочим, но менее надежным, так как мы во многом полагаемся на формат вывода сторонней утилиты.
Еще один пример
[править]Давайте расширим предыдущий пример и попробуем реализовать сортировку параметров программы (например, сортировку имен файлов в директории) независимо от входящей кодировки. Так как мы хотим более-менее переносимое решение, мы воспользуемся модулем Encode::Locale. Преимуществом этого модуля является то, что он может определять кодировку по настроенной в системе локали.
Собирая части из предыдущих примеров, мы получаем такую программу.
# Файл: sort_args.pl
use strict;
use warnings;
use utf8;
use Encode;
use Encode::Locale qw { decode_argv };
use Unicode::Collate;
use Unicode::Normalize;
binmode(STDOUT, ":encoding(console_out)");
binmode(STDERR, ":encoding(console_out)");
decode_argv();
my $collator = Unicode::Collate->new(
preprocess => sub {
my $s = NFC(shift);
$s =~ s/ё/е\x{FFFF}/g;
return $s;
},
normalization => 'prenormalized',
);
print "$_\n" for $collator->sort(@ARGV);
В этом примере мы используем функцию decode_argv() модуля Encode::Locale, который позволяет нам декодировать входящие аргументы программы, используя данные о локали, которые модуль вычисляет во время своей загрузки. Далее мы используем уже когда-то написанные части, чтобы отсортировать массив @ARGV и вывести результат на экран. В этом примере мы не работаем ни с одной кодировкой напрямую, доверяя всю работу сторонним модулям.
Мы можем попробовать программу в Windows, в которой установлен эмулятор Linux, чтобы увидеть, что решение может работать на обеих системах. Для начала создадим каталог в домашней директории:
$ mkdir -p ~/my_dir
$ for idx in $(seq 5); do echo > ~/my_dir/"Файл_$(mktemp -u XXXXXXXXX).txt"; done
$ ls ~/my_dir/
Файл_Hs1NYeKGR.txt Файл_IR0egen8v.txt Файл_NeC9k6Rqj.txt Файл_YRL5Kf4eN.txt Файл_kiFjC5Esq.txt
Теперь запустим программу сначала в Bash
$ perl sort_args.pl ~/my_dir/*
/c/Users/John/my_dir/Файл_Hs1NYeKGR.txt
/c/Users/John/my_dir/Файл_IR0egen8v.txt
/c/Users/John/my_dir/Файл_kiFjC5Esq.txt
/c/Users/John/my_dir/Файл_NeC9k6Rqj.txt
/c/Users/John/my_dir/Файл_YRL5Kf4eN.txt
а теперь в Powershell
> $files=Get-ChildItem ~/my_dir/*
> perl .\sort_args.pl $files
C:\Users\John\my_dir\Файл_Hs1NYeKGR.txt
C:\Users\John\my_dir\Файл_IR0egen8v.txt
C:\Users\John\my_dir\Файл_kiFjC5Esq.txt
C:\Users\John\my_dir\Файл_NeC9k6Rqj.txt
C:\Users\John\my_dir\Файл_YRL5Kf4eN.txt
Результат нас вполне удовлетворяет.