Поддержка Unicode

Начиная с Perl версии 5.6 в интерпретаторе была реализована поддержка Unicode на уровне языка. В данном тексте пойдет речь о том, зачем это нужно, как это можно использовать, и с какими проблемами вам придется столкнуться...

Оглавление

Вступление

Что такое Unicode?

Ответ читаем здесь.

Что такое UTF?

UTF — Unicode Transformation Format — формат преобразования Unicode. UTF — это алгоритмичное преобразование любого Unicode символа (code point) в уникальную последовательность байтов. Стандарт ISO/IEC 10646 использует термин "UCS transformation format" вместо UTF. Оба термина почти синонимы.

Каждый UTF формат обратим, то есть поддерживает преобразование из последовательности Unicode-символов и обратно без потерь, то есть: в результате преобразования любой последовательности Unicode-символов S в последовательность байтов и обратно мы получим опять S. Для достижения возможности преобразования без потерь в обе стороны, UTF формат должен определять преобразование для всех символов Unicode, даже невалидных.

Вольный сокращенный перевод What is a UTF?

Perl и Unicode

Unicode в Perl 5.6.x и 5.8.x

Поддержка Unicode в Perl 5.6 привязана к операциям, а не к данным, что требует использования прагмы "utf8" и только тогда, когда программист уверен, что все данные в кодировке UTF-8. Также, Perl 5.6 не поддерживает Unicode в регулярных выражениях, что очень сильно ограничивает круг возможностей.

Я знаю слишком мало о применении Unicode в 5.6, так что и писать об этом не могу. Все примеры и текст ниже касаются Perl 5.8.x. Eсли кто хочет, то может осветить особенности 5.6. Закиров Руслан[досье]

Как реализована поддержка в Perl

Любая структура данных (скаляр, массив, хеш...) в Perl имеет набор флагов, которые отражают те или иные свойства объектов. Для реализации полноценной поддержки Unicode был выбран формат UTF-8 и введен одноименный флаг. Все функции, которые так или иначе работают со строками, учитывают флаг и интерпретируют данные по разному в зависимости от его значения. К таким функциям относятся: length, chr, ord, substr, join, регулярные выражения и прочие. Следующий пример демонстрирует различия в поведении (используемый модуль Encode включен в дистрибутив Perl начиная с Perl 5.8):

use Encode qw(encode decode is_utf8);

my $octets = 'тест';
print "not " if is_utf8($octets);
print "ok - \$octets has no UTF-8 flag\n";

my $string = decode('cp1251', $octets);
# Encode::_utf8_off($string);
print "not " unless is_utf8($string);
print "ok - \$string has UTF-8 flag\n";

for(my $i = 0; $i < length($string); $i++) {
        printf "octet: %#x\t", ord(substr($octets, $i, 1));
        printf "char: %#x\n", ord(substr($string, $i, 1));
}

вывод:

ok - $octets has no UTF-8 flag
ok - $string has UTF-8 flag
octet: 0xf2       char: 0x442
octet: 0xe5       char: 0x435
octet: 0xf1       char: 0x441
octet: 0xf2       char: 0x442

Одна и та же информация в двух форматах: последовательность байтов (октетов) в кодировке cp1251 с отключеным флагом и последовательность символов в кодировке UTF-8 с установленным флагом. Функция length в обоих случаях возвращает одно и то же значение, потому что в cp1251 для хранения одного символа используется один байт, а для строк без флага возвращается именно количество байт. Различия в поведении функций ord и substr видны явно. В целях эксперимента попробуйте отключить флаг UTF-8, для этого раскоментируйте строку Encode::_utf8_off($string);.

Какие кодировки поддерживаются в Perl

Как уже было сказанно выше, Perl преобразует из "старых" кодировок в UTF-8, для всех преобразований используется модуль Encode, то есть от этого модуля зависит сможете вы использовать преимущества автоматического преобразования данных.

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

Различные кодировки сгруппированы в модули, например поддержка кодировок UTF-* реализованна в модуле Encode::Unicode. Нет необходимости подгружать эти модули самостоятельно, но возможно для редко-используемых кодировок придется установить дополнительные модули со CPAN.

Посмотреть список всех канонических имен всех кодировок можно с помощью

> perl -MEncode -e'print join ", ", Encode->encodings(":all");'

или посмотреть канонические имена поддерживаемые отдельным модулем

> perl -MEncode -e'print join ", ", Encode->encodings("Encode::Unicode");'

Если вы не можете найти в списке необходимую кодировку, то возможно это синоним какой-либо другой кодировки. Можно использовать следующий небольшой скрипт для определения поддерживается ли та или иная кодировка, и является название каноническим или же это синоним:

#!/usr/bin/perl

use strict;
use warnings;
use Encode qw/resolve_alias/;

my $enc = shift;
usage() unless $enc;
my $canon = resolve_alias( $enc );
unless( $canon ) {
        print "Encoding '$enc' is not supported\n";
        exit 1;
}

if( $canon eq $enc ) {
        print "Encoding '$enc' is supported(canonical name)\n";
} else {
        print "Encoding '$enc' is supported(alias for '$canon')\n";
}
exit 0;

sub usage { print "usage: $0 <encoding>\n"; exit 1 }

пример использования

> ./is_enc_supp.pl
usage: ./is_enc_supp.pl <encoding>
> ./is_enc_supp.pl koi8-r
Encoding 'koi8-r' is supported(canonical name)
> ./is_enc_supp.pl windows-1251
Encoding 'windows-1251' is supported(alias for 'cp1251')
> ./is_enc_supp.pl unknown
Encoding 'unknown' is not supported

Подробно о том какие кодировки поддерживаются смотрите perldoc Encode::Supported.

Perl и стандарты

Существует стандарт описывающий UTF-8. Стандарт достаточно строгий в отношении того, что считается UTF-8, а что нет. Например UTF-8 - это последовательность чисел в диапазоне от 0 до 0x10FFFF, но и в этом диапазоне есть запрещенные значения.

До perl 5.8.7 и Encode 2.10 в perl была реализована ограниченная поддержка стандарта, а именно не было полного контроля допустимых значений. С выходом perl 5.8.7 была представлена новая кодировка utf-8-strict, которая полностью соответствует стандарту UTF-8 и UTF-8 рекомендуемый синоним для данной кодировки. Для желающих использовать старую реализацию осталась utf8

Воспользуемся скриптом из Какие кодировки поддерживаются в Perl:

> perl ./is_enc_supp.pl UTF-8
Encoding 'UTF-8' is supported(alias for 'utf-8-strict')
> perl ./is_enc_supp.pl utf-8
Encoding 'utf-8' is supported(alias for 'utf-8-strict')
> perl ./is_enc_supp.pl 'utf 8'
Encoding 'utf 8' is supported(alias for 'utf-8-strict')

но

> perl ./is_enc_supp.pl UTF8
Encoding 'UTF8' is supported(alias for 'utf8')
> perl ./is_enc_supp.pl 'utf8'
Encoding 'utf8' is supported(canonical name).

Когда нельзя использовать Unicode

Нельзя использовать Unicode в следующих ситуациях (актуально для 5.8.x):

  1. названия пакетов/классов
  2. названия функций

Модули, прагмы, переменные и опции

use utf8;

Прагма позволяет указать, что код программы написан в кодировке UTF-8. Строковые константы, константные шаблоны в регулярных выражениях и имена переменных будут рассматриваться как строки в UTF-8 кодировке.

Прагма utf8 не влияет на работу потоков ввода/вывода (см. прагмы encoding и open).
Прагма так же не оказывает влияния на интерпретацию строк со сброшенным флагом UTF-8 (см. Конкатенация строк с флагом и без флага).

use encoding;

Многофункциональная прагма. Позволяет явно указать кодировку в которой написан ваш скрипт, например cp1251 или любую другую, которую поддерживает модуль Encode. Также вы можете управлять поведением стандартных потоков ввода/вывода (STD{IN|OUT}). И последнее, в случаях, когда интерпретатору perl необходимо преобразовать строку без UTF-8 флага в строку с флагом, то указанная кодировка будет использована для преобразования. Рассмотрим каждый из трех пунктов по отдельности.

  1. Автоматическая конвертация констант из кодировки указанной в UTF-8 c выставлением флага. Действие в данном случае очень похоже на действие прагмы utf8, за исключением того, что кодировку можно указать явно.
  2. Управление стандартными потоками ввода/вывода. Вы можете указать в какой кодировке интерпретатор должен выводить в стандартный поток вывода и/или из какой кодировки преобразовывать данные из стандартного потока ввода. То есть perl оперирует Unicode данными, а при выводе или вводе со стандартных потоков производит автоматическую конвертацию данных.
  3. Последний пункт более подробно рассмотрен в описании проблемы ниже. Вкратце, это единственная возможность указать perl'у как интерпретировать строки без флага, когда интерпретатору необходимо конвертировать строку в UTF-8 (по умолчанию используется latin-1).
#!/usr/bin/perl -w

use strict;
use encoding 'cp1251', STDOUT => 'koi8-r';

# cp1251 text
my $str = "тест";
printf "%#x\n", ord($1) while $str =~ /(\w)/g;

print "$str\n";

Вывод:

0x442
0x435
0x441
0x442
ФЕУФ

Вывели коды букв и получили UTF-8 (первые четыре строки). В последней строке получили слово 'тест' в кодировке 'koi8-r', выведенное на 'cp1251' терминал.

use open; (прагма)

use open IN  => ":crlf", OUT => ":bytes";
use open OUT => ':utf8';
use open IO  => ":encoding(koi8-r)";

use open IO  => ':locale';

use open ':utf8';
use open ':locale';
use open ':encoding(cp1251)';

use open ':std';

Прагма open - это вспогательный интерфейс для определения "уровеней" по умолчанию для всех потоков ввода/вывода. Вместо термина "уровень (layer)" так же используется термин "дисциплина (discipline)". В любых двух-аргументных вызовах open(), readpipe() (он же qx//) и в подобных операторах, находящихся в пределах лексической области видимости этой прагмы, будут использованы указанные значения по умолчанию. Трех-аргументные вызовы не подвержены действию этой прагмы и вы должны указывать уровни самостоятельно.

С помощью подпрагмы IN можно указать из какой кодировки преобразовывать данные из стандартного потока ввода.

С помощью подпрагмы OUT можно указать в какой кодировке интерпретатор должен выводить данные в стандартный поток вывода.

Если вы хотите задать кодировку явно, то используйте :encoding(...)

Если вы хотите чтобы кодировки были выбраны автоматически в соответствии с установленной в системе локалью, то используйте :locale, пример:

$ENV{LANG} = 'ru_RU.KOI8-R';
use open OUT => ':locale';
open(O, ">koi8");
print O chr(0x430); # Unicode CYRILLIC SMALL LETTER A = KOI8-R 0xc1
close O;
open(I, "<koi8");
printf "%#x\n", ord(<I>), "\n"; # this should print 0xc1
close I;

Это аналогично этому

use open ':utf8';
use open IO => ':utf8';

или этому

use open ':locale';
use open IO => ':locale';

Если в вызове open() указан явный список уровней, то он добавляется к списку, который объявлен с помощью прагмы open.

Подпрагма :std сама по себе ни на что не влияет, но в сочетании с подпрагмой :utf8 или :encoding она назначает выбранную кодировку для стандартных файловых манипуляторов (STDIN, STDOUT, STDERR).
Например, если объявить прагму :utf8 для потоков ввода и вывода, то :std также установит :utf8 для файловых манипуляторов STDIN, STDOUT, STDERR.
В другом случае, если объявить прагму :encoding(koi8r) для потока вывода, то :std установит koi8r только для файловых манипуляторов STDOUT и STDERR.
Подпрагма :locale неявно включает подпрагму :std

Логика подпрагмы :locale

Если платформа поддерживает интерфейс langinfo(CODESET), то возвращаемое им значение используется как базовая кодировка прагмы open.
Если платформа подкачала, но объявлена прагма locale, то для установки базовой кодировки прагмы open используется часть (после .) значения переменных окружения LC_ALL и LANG (в этом по порядке) подходящая для названия кодировки (если она найдена).
Если первые два пункта безуспешны, но в значениях переменных окружения LC_ALL и LANG (в этом по порядке) найдено что-нибудь похожее на UTF-8, то базовой кодировкой прагмы open устанавливается :utf8. Если в значениях переменных окружения (LC_ALL, LC_CTYPE, LANG) найдена строка 'UTF-8' или 'UTF8' (не важно в каком регистре), то базовой кодировкой для стандартных файловых манипуляторов STDIN, STDOUT и STDERR, а также любых файловых манипуляторов открытых позднее устанавливается :utf8

Управление поведением по умолчанию при запуске интерпретатора

Начиная с версии 5.8.1 была добавлена опция интерпретатора -C, которая позволяет задать режимы работы с потоками ввода/вывода и массивом @ARGV по умолчанию.

I1данные поступающие с STDIN будут интерпретироваться как UTF-8
O2вывод данных в STDOUT будет производиться в кодировке UTF-8
E4вывод данных в STDERR будет производиться в кодировке UTF-8
S7I + O + E
i8UTF-8 включен по умолчанию для открываемых потоков ввода
o16UTF-8 включен по умолчанию для открываемых потоков вывода
D24i + o
A32предполагается, что элементы @ARGV закодированы в UTF-8
L64все остальные указанные опции будут активированы, только если пользователь использует UTF-8 локаль, например ru_RU.UTF-8

Первая группа IOES влияет на стандартные потоки ввода/вывода, смотрите описание прагмы encoding.

Вторая группа ioD влияет на поведение создаваемых потоков ввода/вывода. Для достижения подобного эффекта можно использовать трех-аргументный вызов open(), двух-аргументный вызов binmode() и прагму open.

Если опция указана без дополнительных параметров, просто, -C, то используется -CSDL.

В perl 5.8.0 такое поведение включено по умолчанию, что может стать причиной некоторых проблем. В perl 5.8.1 и выше по умолчанию используется значение 0.

Переменная окружения PERL_UNICODE является синонимом опции , но имеет более низкий приоритет и будет проигнорирована, если указана опция.

Во время выполнения кода, вам доступна для чтения "магическая" переменная ${^UNICODE}, которая отражает состояние описанных выше настроек.

> perl -e 'print "${^UNICODE}\n"'
0

> perl -CIOo -e 'print "${^UNICODE}\n"'
> PERL_UNICODE="IOo" perl -e 'print "${^UNICODE}\n"'
19

> PERL_UNICODE=0 perl -CIOo -e 'print "${^UNICODE}\n"'
19

> perl -C -e 'print "${^UNICODE}\n"'
> perl -CSDL -e 'print "${^UNICODE}\n"'
> PERL_UNICODE="" perl -e 'print "${^UNICODE}\n"'
95

use bytes;

Прагма bytes позволяет переключить Perl в режим обработки строк как последовательность байтов, а не символов, даже если для строки установлен флаг UTF-8. Прагма отключает действие Unicode флага в пределах данной лексической области видимости.

Следующий пример демонстрирует действие прагмы:

my $str = "\x{442}\x{435}\x{441}\x{442}";
printf "Characters semantic: length is %d\n", length($str);
{
        use bytes;
        printf "Bytes semantic: length is %d\n", length($str);
}

вывод:

Characters semantic: length is 4
Bytes semantic: length is 8

Perl версии ниже 5.6 поставляется без прагмы bytes и если вы хотите сохранить совместимость кода с этими версиями, то можно использовать следующий блок кода:

BEGIN { $INC{'bytes.pm'} = 'foo' if $] < 5.6 }

блок необходимо разместить до первого применения прагмы.

Влияние на функции, регулярные выражения и остальное

Функция chr

Конвертирует число в символ, где число - это код символа. В версиях 5.8, с приходом unicode, действие функции немного расширилось, а точнее, если аргумнт число больше 255, то функция считает число code point'ом unicode и возвращает соответствующий символ в скаляре с установленным флагом UTF-8.

Пример: конвертирование HTML entities.

my $str = "&#x0442;&#x0435;&#1089;&#1090;";
$str =~ s/&#x([a-fA-F0-9]+);/"&#". hex($1) .";"/ge;
$str =~ s/&#([0-9]+);/chr($1)/ge;
print "$str\n";

Функция ord

Конвертирует символ в число. Действие обратное функции chr, для символов с флагом возвращает значение code point'а Unicode.

Функция binmode

У функции появился второй аргумент LAYER, который позволяет указать слои (дисциплины) потока ввода/вывода. Более подробно доступные возможности описываются в другой статье, а в нашем случае наиболее интересны :utf8, :bytes и :encoding(...).

Пример: Указываем кодировку файла.

my $str = "\x{442}\x{435}\x{441}\x{442}";
open my $fh, ">myfile.txt";
binmode $fh, ":encoding(cp1251)";
print $fh $str;
close $fh;

Шаблон \w

Как уже говорилось unicode меняет поведение регулярных выражений, вернее не меняет, а использует дополнительную мета-информацию о симфолах, которую предоставляет Unicode. Согласно perldoc perlre шаблон \w совпадает с одним символом алфавита, десятичной цифрой или символом '_', существует достаточно распространенное заблуждение, что это равнозначно шаблону [a-zA-Z0-9_], но это конечно же неверно:

perl -e 'use open ":locale"; print "\x{442} - match \\w\n" if "\x{442}" =~ /\w/'

Примеры

iconv.pl

Простой скрипт конвертации текста из одной кодировки в другую:

#!/usr/bin/perl
my ($from, $to);

use Getopt::Long;
GetOptions(
    "from|f=s" => \$from,
    "to|t=s"   => \$to,
);
$_ ||= 'UTF-8' for $from, $to;
binmode( STDIN,  ":encoding($from)" );
binmode( STDOUT, ":encoding($to)" );
print while <>;

Использование:

shell> iconv.pl -f cp1251 file_name.txt
shell> cat file_name.txt | iconv.pl -f cp1251
shell> find dir_name/ -type f -name '*.txt' | xargs perl -i.bakup iconv.pl -f cp1251

Плагин для HTML::Mason

Веб-сервер, получая запрос от клиента, не имеет информации о кодировке сообщения. Декодирование и интерпретация строковых данных остается на совести програмиста. В HTML::Mason существует достаточно гибкая система плагинов, чтобы написать плагин, который сконвретирует все строки из последовательностей байтов в perl строки.

package MasonX::Plugin::UTF8;
         
use 5.8.0;
use strict;
use warnings;
our $VERSION = '0.01';

use base qw(HTML::Mason::Plugin);
use Encode ();

sub start_request_hook {
    my ($self, $context) = @_;
    return if $context->request->is_subrequest;
    my $args = $context->args;
    $_ = Encode::decode( utf8 => $_, Encode::FB_PERLQQ ) foreach @$args;
}   
1;

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

Получение страничек с другого сервера (LWP)

use LWP::UserAgent;
use LWP::Charset qw(getCharset);
use Encode qw(decode);

my $ua = LWP::UserAgent->new;
$ua->agent("MyApp/0.1");

my $url = some_url_goes_here;
my $req = HTTP::Request->new(GET => $url);
my $res = $ua->request($req);
die "problem" unless $res->is_success ;

my $content = decode(getCharset($res), $res->content);
print $content;

Проблемы

Закладка на то, что ord() возвращает значения в диапазоне 0..255

Функция ord() также учитывает состояние флага UTF-8 и может вернуть число больше 255.

Возможные источники проблемы

Стандартным примером может стать код, реализующий "escaping URI":

> perl -e '$str = "\x{442}\x{435}\x{441}\x{442}"; $str =~ s/(.)/sprintf("%"."%"."%02x", ord($1))/eg; print "$str\n"'
%442%435%441%442

Решение

Информировать разработчиков о проблеме с целью документирования и/или исправления поведения. Если же необходимо со строкой работать, как с последовательностью байтов, то можно использовать прагму bytes.

Пример

> perl -e 'print ord("\x{442}")."\n"'
1090

Конкатенация строк с флагом и без флага

Конкатенация строки, имеющей флаг UTF-8, со строкой, у которой этот флаг снят, приводит к автоматическому преобразованию строки без флага. При этом Perl исходит из того, что строка в кодировке "latin-1" или той кодировке, которая установлена с помощью прагмы "encoding".

Решение

Постоянное использование прагмы "encoding" или же тестирование своего и внешнего кода на наличие этой проблемы. Так как первый вариант — это обходной путь и не является в полном смысле решением, то второй вариант более предпочтителен.

Возможные источники проблемы

Пример

use Encode qw();
                                                                                                                                                                      
#use encoding "UTF-8";
#use utf8;
                                                                                                                                                                      
my $str = "\x{442}\x{435}\x{441}\x{442}";
print "not " unless Encode::is_utf8 $str;
print "ok - строка с установленным UTF-8 флагом\n";
                                                                                                                                                                      
my $nstr = some_bad_function($str);
print "not " unless Encode::is_utf8 $nstr;
print "ok - новая строка тоже вообщем-то должна быть с флагом\n";
                                                                                                                                                                      
my $cstr = $str.$nstr;
print "not " unless Encode::is_utf8 $cstr;
print "ok - конкатенация новой и старой строки имеет флаг\n";
print "not " unless $cstr eq $str.$str;
print "ok - так как новая строка это копия старой, то и конкатенация должна равняться удвоенной старой строке \n";
                                                                                                                                                                      
exit;
                                                                                                                                                                      
# функция возвращает копию аргумента, но без флага UTF-8
sub some_bad_function
{
        my $str = shift;
        Encode::_utf8_off($str);
        return $str;
}

Обратите внимание, что использование прагмы "utf8" не решает проблемы.

join в perl < 5.8.3

Один и тот же вызов функции join возвращает неправильные результаты, если был последовательно выполнен для строк с флагом UTF-8, а затем без.

Решение

Обновить perl до версии 5.8.3 и выше.

Использование памяти функцией uc() в perl < 5.8.7

Символ в нижнем и верхнем регистре может занимать разное количество байт, поэтому при вызове функции uc для Unicode-строки часто нужно выделить больше памяти, чем используется для строки сейчас. Так как перевыделение (realloc) памяти достаточно трудоемкая операция, то perl пытается выделить необходимый блок сразу, но использует очень пессимистичный подсчет размеров необходимого блока.

Решение

Обновить perl до 5.8.7.

Пример

> perl -Ilib -MDevel::Peek -e'$a=("a"x 50)."\x{100}"; Dump($a); Dump uc $a'

SV = PV(0x804d56c) at 0x80648d4
  REFCNT = 1
  FLAGS = (POK,pPOK,UTF8)
  PV = 0x8077588 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\304\200"\0 [UTF8 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x{100}"]
  CUR = 52
  LEN = 53
SV = PV(0x804d4a0) at 0x804d2dc
  REFCNT = 1
  FLAGS = (PADTMP,POK,pPOK,UTF8)
  PV = 0x8086e48 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\304\200"\0 [UTF8 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x{100}"]
  CUR = 52
  LEN = 1990

ucfirst и lcfirst в perl < 5.8.8

ucfirst и lcfirst могут испортить данные в unicode строке

Решение

Обновится до версии 5.8.8 или использовать patch.

Пример

пример и описание.

Литература

Источник статьи