UTF Perl Practice / как использовать UTF-8 в перле

27 января 2021 Perl

Лучшая статья по юникоду в перле, которую встречал, и которая, к сожалению, осталась только на веб-архиве: http://www.nestor.minsk.by/sr/2008/09/sr80902.html.

Переложил её сюда, чтобы с одной стороны, оно «искалось» в интернете, а с другой — чтобы насобирать откликов, так как проблема до сих пор актуальна, и все снова и снова с ней сталкиваются.

В перл-чатике пишут:

она кстати уже устарела как минимум в нескольких местах

Вступление

Для многих не секрет, что на данный момент восьмибитовые кодировки в значительной мере устарели. Основная тому причина — невозможность вместить в одну кодировку достаточное количество символов. Когда необходимо поддерживать ограниченное количество групп символов (к примеру, кириллицу и латиницу), мы можем воспользоваться koi8-r, cp1251 или iso-8859-5. Но если возникает потребность использовать несколько языков или специальные символы, то одной емкости одной кодировки становится недостаточно. Вот тут и может помочь использование юникода.

Для начала определим неясности с терминологией. Многие так или иначе сталкивались с понятиями Unicode, UTF-8, UTF-16, UTF-32, UCS-2, UCS-4, и все они так обозначали юникод. Что же значит каждый из них?

Unicode — Character Encoding Standart

Стандарт для цифрового представления символов использующихся во всех языках. Поддерживается и развивается Консорциумом юникода (unicode.org).

UCS — Universal Character Set

Международный стандарт ISO/IEC 10646, который идентичен стандарту Unicode.

UTF — Unicode (or UCS) Transformation Format

Способ представления символов Unicode в виде последовательности целых положительных чисел. - UTF-8, UTF-16, UTF-32 — различные UTF-трансформации, которые оперируют числами, занимающими соответственно 8, 16 и 32 бита. В UTF-8 минимальный размер символа — один октет (байт), максимальный — шесть. В UTF-16 минимальный размер — два октета, максимальный — четыре. В UTF-32 любой символ представляется в виде четырех октетов.

UCS-2, UCS-4

Способы кодирования по ISO/IEC 10646. Универсальный набор символов, закодированный двумя или четырьмя октетами (байтами) соответственно. UCS-2 полностью входит в UTF-16, но в UTF-16 есть составные символы (из четырех октетов), которые не входят в UCS-2. UCS-4 тождественна UTF-32.

Итог: Unicode — набор символов, определенным образом упорядоченных: каждому символу поставлена в соответствие кодовая позиция. Любая из кодировок UTF — это представление символов Unicode в виде последовательности чисел. Поэтому, говоря, например, о переводе проекта на юникод, в большинстве случаев мы подразумеваем поддержку какой-либо из трансформаций. При необходимости за уточнениями можно обратиться к словарю терминов www.unicode.org/glossary и документации на сайте unicode.org.

С точки зрения программиста наиболее комфортной для работы выглядит UTF-32. В этой кодировке мы имеем постоянный размер символа. Но с практической точки зрения, в простейшем случае, при использовании только латиницы, мы получаем затраты по объему в четыре раза (по сравнению с обычной восьмибитовой кодировкой). Вторая проблема при переходе на UTF-32 — необходимость полной замены исходного кода и всех текстов. Поэтому в качестве переходной альтернативы была разработана кодировка UTF-8. Ее особенность состоит в том, что часть символов, попадающая в ACSII, сохраняет свои коды и представления. В результате если у нас был исходный код, написанный в latin-1, то при переходе на UTF-8 ничего не нужно менять. На сегодня наибольшую популярность получила именно кодировка UTF-8, как наиболее комфортная для плавного перехода. Эту кодировку поддерживает подавляющее большинство программного обеспечения и средств разработки.

Аргументацией против перехода на юникод достаточно часто выступает ложное мнение о том, что перл его не поддерживает, или поддерживает недостаточно хорошо. Чаще всего такое мнение складывается из-за неправильного использования имеющихся средств. Миф о неумении перла работать с юникодом я и постараюсь развеять. Также аргументом может выступать излишний объем по сравнению с восьмибитовой кодировкой. Но если оценить, то получается что количество текста, который действительно увеличивается в объеме, по сравнению с общим объемом кода проекта, крайне незначителен. Под задачей перехода на юникод (а конкретно — на кодировку UTF-8) я буду понимать следующие требования: исходный код в UTF-8, корректная работа встроенных функций и регулярных выражений с использованием возможностей юникода, а также корректное взаимодействие с окружением.

Корень зла или суть проблемы

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

Возьмем любой текстовый редактор с поддержкой UTF-8, напишем в нем простой кириллический символ А и сохраним в файл. Посмотрим шестнадцатеричное представление этого файла. В нем будет два байта: 0xD090. Это и есть представление символа CYRILLIC CAPITAL LETTER A, закодированное при помощи UTF-8. Но считывая в программе на Perl UTF-данные из различных источников, мы получаем совершенно различные их представления. Если взять Data::Dumper и сделать дамп таких строк, то возможны следующие варианты:

"А"
"\x{410}"
"\x{d0}\x{90}"

В первом варианте мы имеем строку, про которую перл не знает, что это строка. Для него это набор байт. Во втором случае мы имеем юникодный символ с кодом 0410. Если обратиться к таблице символов Unicode, то мы узнаем, что это и есть CYRILLIC CAPITAL LETTER A. Третий случай — это два символа Unicode с кодами 00d0 и 0090. Первая строка — набор октетов без флага. Вторая строка — юникодный символ, на этой строке флаг включен. Третий случай — это с нашей точки зрения «поломанные» данные. На строке октетов был принудительно включен UTF-флаг, при этом каждый октет стал отдельным символом. В большинстве задач мы будем стремиться ко второму варианту.

Для начала нужно определиться, как мы можем преобразовать данные из одного представления в другой. Для данной задачи есть как минимум два хороших способа, каждый со своими плюсами и минусами — utf8::* и Encode::*.

utf8::* хорошо использовать, когда исходные данные пришли в нашу программу уже в кодировке UTF-8.

utf8::downgrade снимает флаг со строки:

utf8::downgrade("\x{d0}\x{90}") :'А'

utf8::upgrade выставляет флаг на строку:

utf8::upgrade('А') : "\x{d0}\x{90}"

utf8::encode преобразует символы в октеты, снимает флаг:

utf8::encode("\x{410}") : "А"

utf8::decode преобразует октеты в символы, выставляет флаг (флаг выставляется только в том случае, если строка содержит символы с кодами, большими 255; см. perunicode).

utf8::decode("А") : "\x{410}"

utf8::is_utf8 проверяет состояние флага. возвращает 1 в том случае, если на строке установлен utf-флаг:

utf8::is_utf8("\x{410}") = 1
utf8::is_utf8("\x{d0}\x{90}") = 1
utf8::is_utf8('А') = undef

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

Encode::* хорошо использовать, когда исходные данные имеются в разных кодировках. Также данный модуль хорош для различных преобразований между кодировками. Некоторые функции аналогичны utf8::*.

_utf8_off снимает флаг со строки. _utf8_on выставляет флаг на строку. encode_utf8 преобразует символы в октеты, снимает флаг. decode_utf8 преобразует преобразует октеты в символы, выставляет флаг (см. комментарий по поводу utf8::decode). encode преобразует символы в октеты указанной кодировки, снимает флаг:

encode("cp1251","\x{410}") = chr(0xC0)

decode преобразует октеты указанной кодировки в символы, выставляет флаг (см. упомянутый комментарий).

decode("cp1251",chr(0xC0)) = "\x{410}"
decode("MIME-Header", "=?iso-8859-1?Q?Belgi=eb?=")
= "Belgi\x{451}" (Belgiё)

Теперь нам известно, как преобразовывать данные. Попробуем воспользоваться этим знанием на практике.

Исходный код

Напишем простую программу в кодировке UTF-8, запустим ее и посмотрим на вывод.

$_ = "А";

print Dumper $_; # "А"
print lc; # А
print /(\w)/; # nothing
print /(а)/i; # nothing

Как мы видим, строка оказалась без флага, встроенные функции (lc) не работают, регулярные выражения не работают. Воспользуемся уже известной нам функцией utf8::decode:

$_ = "А";
utf8::decode($_);
print Dumper $_; # "\x{410}"
print lc; # а
print /(\w)/; # А
print /(а)/i; # nothing ?

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

print /(\x{430})/i;

или

use charnames ':full';
print /(\N{CYRILLIC SMALL LETTER A})/i;

или даже

$a = ''.qr/(а)/i;
utf8::decode($a);
print /$a/;

Но есть более удобный способ. Директива use utf8 «выполняет» utf8::decode( <SRC> ).

use utf8;
$_ = "А";
print Dumper $_; # "\x{410}"
print lc; # а
print /(\w)/; # А
print /(а)/i; # А

Все работает, никакой черной магии.

Также отмечу существование похожей директивы use encoding 'utf8'. Она делает почти то же самое, но use encoding, во-первых, не является лексической директивой (ее действие не ограничивается блоком, и при выходе из блока сохранится), во-вторых, обладает «магическим» поведением, сходным с source filters. В общем случае использование use encoding для utf-8 не рекомендуется.

Ввод и вывод

Итак, все работает, но при этом мы получаем странное предупреждение, которого раньше не было:

Wide character in print at...

Проблема заключается в том, что перл не знает, поддерживается ли utf-8 данным дескриптором. Мы можем ему об этом сообщить:

binmode(STDOUT,':utf8'); # binmode используется
# с уже открытым дескриптором

Точно так же возможно указать, что некоторый, открываемый нами файл — в кодировке UTF-8 через так называемые PerlIO Layers:

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

Также мы можем снять флаг со строки перед выводом (utf8::encode) и отдать дескриптору поток байтов. Но есть простая директива use open, которая поможет решить данные вопросы:

use open ':utf8'; # только для файлов
use open qw(:std :utf8); # файлы и STD*
# подробнее - perldoc open

Еще мы можем при помощи PerlIO указать поддерживаемую кодировку, если, например, хотим писать некоторый лог-файл в cp1251.

binmode($log, ':encoding(cp1251)');

Строки с флагом при работе с этим дескриптором будут автоматически переведены в указанную кодировку средствами PerlIO.
В результате вышеизложенного мы можем делать даже так:

use strict; use utf8; use open qw(:std :utf8);
my $все = "тест";
sub печатать (@) { print @_ }
печатать $все;

Для простоты можно сделать очень простой «прагматический» модуль, который будет для нас выполнять сразу все три действия, чтоб не писать трижды use:

package unistrict;
use strict(); use utf8(); use open();
sub import {
$^H |= $utf8::hint_bits;
$^H |= $strict::bitmask{$_} for qw(refs subs vars);
@_ = qw(open :std :utf8);
goto &open;::import;
}

И в дальнейшем:

use unistrict;

Что касается самого перла — это все, что нужно знать, для того, чтобы успешно использовать utf-8. Но еще мы рассмотрим на примерах, как скорректировать работу того или иного модуля, если он не соответствует нашим требованиям.

Окружение

Под окружением понимаются различные модули (как входящие в стандартную поставку, так и с CPAN), с которыми взаимодействует наше приложение. То есть, например, модуль бизнес-логики считается частью приложения и подразумевается, что он работает в подготовленной среде с корректными строками (с флагом), а вот модули, отвечающие за ввод и вывод — это окружение.

DBI.pm

По умолчанию большинство DBD возвращают данные без флага.

my $dbh = DBI->connect('DBI:mysql:test');
($a) = $dbh->selectrow_array('select "А"');
print '$a = ',Dumper $a; # 'А'

Но опять же, для большинства DBD уже сделана поддержка utf-8.

DBD::mysql : mysql_enable_utf8 (требует DBD::mysql >= 4.004)
DBD::Pg : pg_enable_utf8 (требует DBD::Pg >= 1.31)
DBI:SQLite : unicode (требует DBD::SQLite >= 1.10)

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

my $dbh = DBI->connect('DBI:Pg:dbname=test');
$dbh->{pg_enable_utf8} = 1;
($a) = $dbh->selectrow_array('select "А"');
print '$a = ',Dumper $a; # "\x{410}"

Template Toolkit

В TT заявлена поддержка UTF-8, но при этом имеются некоторые особенности. Чтобы файл шаблона был воспринят и перекодирован в строки с флагом, в начале каждого файла должен быть так называемый BOM-header. BOM расшифровывается как Byte Order Mark (порядок следования байт). Но получается, что BOM имеет значение только для UTF-16 и UTF-32, у которых минимальная единица — два или четыре октета (байта). Для UTF-8 BOM по спецификациям признается опциональным. А если учесть, что в шелл-скриптах наличие BOM перед шебангом (например, #!/usr/bin/perl) «ломает» скрипт, то его использование зачастую вообще сомнительно. Для UTF-8 BOM — это три байта 0xEFBBBF или \x{feff}. Соответственно, если вы хотите, чтобы TT читал файлы без BOM, но при этом все корректно работало, предлагаю воспользоваться одним из двух вариантов решения проблемы:

package Template::Provider::UTF8;
use base 'Template::Provider';
use bytes;
our $bom = "\x{feff}"; our $len = length($bom);
sub _decode_unicode {
my ($self,$s) = @_;
# if we have bom, strip it
$s = substr($s, $len) if substr($s, 0, $len) eq $bom;
# then decode the string to chars representation
utf8::decode($s);
return $s;
}
package main;
my $context = Template::Context->new({
LOAD_TEMPLATES => [ Template::Provider::UTF8->new(), ] });
my $tt = Template->new( 'file',{ CONTEXT => $context }, ... );

или

package Template::Utf8Fix;
BEGIN {
use Template::Provider;
use bytes; no warnings 'redefine';
my $bom = "\x{feff}"; my $len = length($bom);
*Template::Provider::_decode_unicode = sub {
my ($self,$s) = @_;
# if we have bom, strip it
$s = substr($s, $len) if substr($s, 0, $len) eq $bom;
# then decode the string to chars representation
utf8::decode($s);
return $s;
}
}

package main;
use Template::Utf8Fix; # 1 раз в любом месте проекта
my $tt = Template->new( 'file', ... );

CGI.pm

Наиболее часто используемым модулем при разработке CGI-приложений начального уровня является CGI.pm. У него есть много недостатков (подробнее можно ознакомиться в докладе Анатолия Шарифулина c YAPC::Russia 2008: http://event.perlrussia.ru/yr2008/media/video.html), но тем не менее, модуль крайне популярен. Рассмотрим, что нужно сделать, чтобы получать от него переданные аргументы в виде строк с флагом.

Для версии ниже 3.21 рабочим способом может быть только переопределение метода param (аналогично примерам про TT). Начиная с версии 3.21 до 3.31 нужно указать кодировку раньше, чем будет обращение к методу param():

# Запрос: test.cgi?utf=%d0%90
use CGI 3.21;

$cgi->charset('utf-8');
$a = $cgi->param('utf');
print $cgi->header();
print Dumper $a; # "\x{410}"

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

# Запрос: test.cgi?utf=%d0%90
use CGI 3.31 qw(:utf8);

$a = $cgi->param('utf');
print $cgi->header();
print Dumper $a; # "\x{410}"

Замечания

Следует также обратить внимание на термины, касающиеся UTF-8. Официальное название кодировки — UTF-8. На вебе имя часто встречается в нижнем регистре — utf-8. В перле кодировка называется utf8. Различия между ними следующие:

* utf8 — unrestricted UTF-8 encoding. Нестрогая UTF-8. Это может быть любая последовательность чисел в диапазоне 0..FFFFFFFF.

* utf-8 — strict UTF-8 encoding. Строгая UTF-8. Это может быть только некоторая последовательность чисел из диапазона 0..10FFFF, которая регламентирована стандартом Unicode (см. unicode.org/versions/Unicode5.0.0).

Таким образом:
- utf-8 является подмножеством utf8;
- перл поддерживает любые, в том числе так называемые ill-formed последовательности.

Также хочу обратить внимание, что в регулярных выражениях метасимвол \w работает по-разному в зависимости от контекста. Так, при использовании qr/[\w]/, метасимвол будет воспринят в байтовой семантике (так как в случае перечисления всех символов класса \w из таблицы Unicode данный паттерн был бы крайне объемным и в результате — медленным).

Проблемы

В режиме utf-8 не используйте локали (см. perldoc perlunicode). Их использование может привести к неочевидным результатам.

Встроенные функции работают значительно медленнее на строках с флагом.

Также встречаются крайне странные и неприятные ошибки:

use strict;use utf8;
my $str = 'тест'; my $dbs = 'это тестовая строка';
for ($str,$dbs) {
sprintf "%-8s : %-8s\n", $_, uc;
print ++$a;
}
for ($dbs,$str) {
sprintf "%-8s : %-8s\n", $_, uc;
print ++$a;
}

Результат:

123panic: memory wrap at test.pl line 12.
или
use strict;
my $str = "\x{442}";
my $dbs = "\x{43e} \x{442}\x{435}\x{441}".
"\x{442} \x{43e}\x{432}\x{430}".
"\x{44f} \x{441}\x{442}\x{440}";
sprintf "%1s\n",lc for ($dbs,$str);

Результат:

Out of memory!

Также есть не совсем адекватное поведение:

use strict; use utf8;
print "1234567890123456780\n";
printf "%-4.4s:%-4.4s\n", 'itstest','itstest';
printf "%-4.4s:%-4.4s\n", "этотест","этотест";

Результат:

1234567890123456780
itst:itst
этот :этот

Данные проблемы связаны с ошибками в реализации встроенной функции sprintf. Так что отформатировать вывод юникодных строк при помощи %*.*s в sprintf не получится. Решение проблемы находится в стадии разработки.

Кроме того

Расскажу о паре интересных вещей, которые можно сделать, когда у нас имеются строки с UTF-флагом. Для начала упомяну достаточно интересный модуль Text::Unidecode:

use utf8;
use Text::Unidecode;
print unidecode "\x{5317}\x{4EB0}";
# That prints: Bei Jing
print unidecode "Это тест";
# That prints: Eto tiest

Данный модуль позволяет получить фонетическую транслитерацию большинства символов юникода в ASCII. Кстати, этот модуль используется на pause.perl.org при транслитерировании имен, содержащих символы, выходящие за рамки latin-1.

Еще одно достаточно интересное применение юникоду я нашел в проекте, который целиком работает на koi8-r. Нижеприведенный пример показывает, как можно пользоваться возможностями регулярных выражений с функционалом юникода, не переводя весь проект на UTF-8:

sub filter_koi ($) {
# передем koi8-r в строку
local $_ = Encode::decode('koi8-r', shift);
# заменим все html-entity
# соответствующими символами юникода
s{&#(\d+);}{chr($1)}ge;
# проведем некоторые замены
# пробельные символы пробелом
s{(?:\p{WhiteSpace}|\p{Z})}{ }g;

# все кавычки – двойными
s{\p{QuotationMark}}{"}g;

# минусы, дефисы, тире и т.п. - дефисами
s{\p{Dash}}{-}g;

# символ переноса тоже дефисом
s{\p{Hyphen}}{-}g;

# символ троеточия тремя точками
s{\x{2026}}{...}g;

# символ номера заменяем на N
s{\x{2116}}{N}g;
# Вернем строку обратно в кодировке koi8-r
return Encode::encode('koi8-r',$_);
}

Как известно, на странице, отданной, к примеру, в кодировке koi8-r, есть возможность ввести символы, не входящие в эту кодировку. Они приходят на серверную сторону в виде html-сущностей &#....; Хранить с ними данные неудобно, к тому же не всегда в качестве вывода используется HTML. Данная функция преобразует отсутствующие в кодировке символы в некоторые визуальные аналоги. Для поиска и замены используются классы символов (character class), такие как, например, QuotationMark. В него входят всевозможные кавычки из всех языков.

Ответы на многие вопросы можно найти в документации к перлу:

perldoc perluniintro
perldoc perlunicode
perldoc Encode
perldoc encoding

Автор с удовольствием ответит на любые вопросы по работе с юникодом в перле по электронной почте или в рассылке группы Moscow.pm.
Владимир Перепелица, Москва, mons@cpan.org


Ещё из перл-чатика по теме, начиная с коммента https://t.me/modernperl/178819:

Oleg Pronin, [22 Jan 2021 02:05:22]:
Я заметил, что многие путаются в utf8 сильно и получают wide character или кракозябры (двойной энкод etc). Хотя тема на самом деле дико простая.
Все что нужно знать:
1) у перла строка может представлять 2 сущности - byte stream и string. is_utf8 говорит какой из режимов включен. В режиме строки просто некоторые функции (substr, lenght И так далее) меняют свой behaviour с побайтового на посимвольный, что занимает дополнительное процессорное время естественно, потому что в пасяти все равно utf8.
2) внутренее представление не изменяется никак при переходе из одного режима в другой. decode_utf8 не делает нихрена кроме как проверяет что там нет инвалидных последовательностей utf8 и переключает режим.
3) чтобы не запутаться где какой режим, простое правило - все что приходит извне (чтение из сокета, файла, stdin, ...) - всегда байтовое. Соответсвенно все что туда отправляется должно быть тоже (иначе будет wide character, однако изза совпадения внутреннего прдставления, все будет работать).
4) еще простое правило - функция is_utf8 за крайне редкими исключениями не должна использоваться. Вы всегда должны знать где у вас байты а где символы. Для этого как правило сразу на входе декодят и в приложении используются символы (кроме случая когда данные бинарные должны быть), и энкодятся в самом конце перед записью в канал.
5) некоторые библиотеки избавляют вас от необходимости энкодить и декодить. Например json::xs::decode_json() ожидает от вас бинарный поток и порождает структуру с символами. А на encode ожидает символы и порождает байты. Обычно это интуитивно логично и ожидаемо.
В случаи работы через обьект JSON::XS->new->utf8->
Метод utf8 как раз заставляет его порождать на декод и ожидать на энкод символы. Если его не писать то в структуре которую вернет декод будут тоже байты (лучше так не делать, только если точно уверены что там только инглиш). Но на вход декод и выход энкод всегда байты, это не меняется, иначе бессмыслица.
Еще например template toolkit аналогично, считывает с диска байты, декодит в символы, ожидает от вас переменные в символах, рендерит и энкодит в байты итоговый результат.
6) многие считают что utf8 это символы. Нет. Это байты. Utf8 это способ сериализации кодов юникод, то есть бинарный режим. В перле вообще нет настоящего символьного режима. Он чисто виртуальный (выполняется в рантайме, разбирая каждый байт). Например когда строке в режиме символов говорят substr на 10й символ, перл не может херак и встать на 10й символ сразу как в бинарном режиме, ему придется линейно идти от начала строки отсчитывая символы из байтов.
Настоящий символьный режим был бы если бы перл представлял строку в памяти как utf32 - коды юникода (и памяти занимало бы в 2-4 раза больше, но работало бы быстрее). В перле в режиме символов всегда бинарный режим utf8 под капотом, и виртуальное рантайм эмуляция.
И да еще забыл
7) use utf8;
Не имеет ничего общего с рантайм перекодировками данных, это просто хелпер который автомат делает decode_utf8 на все литералы написанные в этом файле / области видимости. Таким образом все литералы становятся строками. Но вычитанные в рантайме нет!
Обычно мало захардкоженных не инглиш строк в программе, поэтому он не особо полезен.