Perl долгое время ничего не знал про кодировки. Строка была просто последовательностью байтов, каждый держал там все что хотел, и лишь изредка приходилось задумываться о том, какая же все-таки кодировка у этих данных. Времена изменились, появился UTF; поддержать его пришлось и перлистам. Как это обычно бывает, in a perl way. Я надеюсь, что эта статья сбережет немного здоровья тем, кто до сих пор пребывает в неведении относительно реализации UTF-8 в Perl.

Собственно, реализации UTF-8 в Perl было две. Первая появилась в Perl 5.6, но была достаточно сырой и неудобной. Начиная с Perl 5.8 механизм работы с уникодом был радикально пересмотрен, и модули на CPAN запестрили забавными проверками на версию интерпретатора. Все, что написано ниже, относится именно к этой, второй реализации.

За и против


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

Вам наверняка понадобится UTF-8, если Вы не знаете наперед, в каком виде придет приложению очередная порция данных, или разрабатываете международный проект. Ведь даже если Ваш сайт на английском языке, на нем вполне может зарегистрироваться какой-нибудь немец с умляутами в ФИО, или даже житель поднебесной. Простейший способ не задумываться о том, что окажется после этого в БД (ну и о том, как вы будете показывать имя китайца в любимой latin-1) — работать в кодировке, поддерживающей множество языков.

И еще один случай, когда без знакомства с Perl UTF не обойтись — интеграция с работающими в этом формате сторонними компонентами. Например, библиотека XML::LibXML возвращает результаты разбора XML-файлов именно в этом формате.

The Perl Way


Вероятно, майнтейнеры рассуждали примерно так: мы хранили в переменных цепочки байт, теперь нам надо научиться хранить там символы. Длина символа в UTF-8 непостоянна и может быть больше одного байта. Если регулярки и функции для работы со строками (типа length, substr) начнут себя вести по-другому, нам спасибо не скажут. Значит, нужно сделать строки двух типов — для работы по-старой схеме, с байтами, и для работы по новой схеме, с символами. Как это сделать? А давайте введем для скаляров скрытый флаг. Если флаг установлен, строка воспринимается как состоящая из логических символов (назовем это Perl Internal Format), если нет — из байтов.

Если взять две одинаковые unicode-переменные и у одной из них просто опустить флаг, переменные будут обрабатываться перлом по-разному (например, у них скорее всего будет разная длина). Однако, сами данные при этом не изменяются — это можно увидеть, например, если обе переменных вывести в файл, либо на экран.

Стоит упомянуть, что символы UTF-8 в терминологии Perl часто называются wide characters. Если у вас попадаются варнинги с этими словами, значит дело касается уникодных строк.

Вариантов для работы с уникодными данными в Perl несколько. Основные из них это:
  1. принудительное указание уникодных символов в строке — через конструкцию вида \x{0100};
  2. ручная перекодировка строки при помощи модуля Encode, либо функций из пакета utf8;
  3. включение прагмы use utf8 — флаг поднимается у всех констант, которые встретились в коде;
  4. чтение из дескриптора ввода-вывода с указанием IO-Layers :encoding или :utf8 — все данные автоматически перекодируются во внутренний формат.
С пунктом №1, я надеюсь, все понятно и вопросов он не вызывает. На всякий случай упомяну, что фигурные скобки являются обязательными. Остальные варианты рассмотрим подробнее.

Модуль Encode

Модуль входит в поставку Perl 5.8, так что использовать его имеет смысл не только для уникода, но и для любых других преобразований кодировки. Работа с модулем не слишком сложна. Единственная проблема — научиться не путать функцию encode с функцией decode :-). Интерфейс у них одинаковый, а логика наименования не настолько очевидна, как хотелось бы. Поскольку формат строк с unicode-флагом считается внутренним форматом, в него нужно декодировать данные из произвольной кодировки (в том числе и UTF-8 без флага), и наоборот, при желании перевести данные в некую внешнюю кодировку, их нужно из внутреннего формата закодировать в нее. Выглядит это примерно так:

$bytes = encode('cp1251', $string); # перекодировали строку из внутреннего представления в cp1251
$string = decode('cp1251', $bytes); # и обратно


Поскольку не все символы можно без потерь гонять из одной кодировки в другую, есть еще и третий параметр, который определяет, как себя вести в случае проблем. О нем можно прочитать в документации на модуль Encode, там этому посвящен целый раздел.

Если Вы точно уверены, что в вашей переменной находятся байты в UTF-8, можно просто поднять флаг у переменной, не производя перекодировку и проверку — при помощи _utf8_on. Определить наличие флага у строки (и при желании проверить валидность лежащих там данных) поможет функция is_utf8. Ну а сбрасывается флаг, как можно догадаться, через _utf8_off. Единственное «но» — эти функции помечены как INTERNAL, и рассчитывать на их неизменность не стоит.

Начиная с Perl 5.8.1 часть функций модуля Encode стала доступна в неймспейсе utf8:: — это функции is_utf8, encode, decode. Последние две отличаются от синонимов из модуля Encode тем, что изменяют значение переданной переменной вместо возвращения результата, и не требуют указания кодировки (подразумевается, что работа происходит с данными UTF-8 без поднятого флага). Все эти функции встроены в интерпретатор, и писать use utf8 для доступа к ним не нужно — более того, это может привести к дополнительным эффектам (о них чуть позже).

use utf8;

Прагма use utf8 сообщает интерпретатору, что все константы и регулярные выражения, записанные в зоне ее действия и имеющие не-ASCII символы, должны трактоваться как уникодные и автоматически приводиться ко внутреннему формату. Для отмены действия прагмы, как обычно, используется конструкция no utf8.

Cуществует и противоположная по смыслу прагма use bytes, в зоне действия которой даже данные с флагом UTF-8 трактуются, как состоящие из байтов.

PerlIO

Тема Perl IO Layers в принципе заслуживает отдельной статьи. Идея в том, что с некоторых пор старая добрая функция open обзавелась трехаргументным синтаксисом:

open $fh, $mode, $filename

Кроме стандартных значений типа '>' и '<' в $mode можно указывать также кодировку файла. При этом загружаемые данные автоматически конвертируются во внутренний формат Perl:

open $fh, "<:encoding(cp1251)", $filename

Если речь идет о файле, содержащем данные в UTF-8, код можно слегка упростить:

open $fh, "<:utf8", $filename

Безусловно, использовать данные модификаторы можно и для модификации файлов — эффект будет обратным.

Кстати, в Perl есть возможность сделать потоки ввода-вывода уникодными раз и навсегда при помощи ключа командной строки -C. Подробности можно посмотреть, как всегда, в perldoc.

Грабли


Конечно же, они есть. Вообще иногда возникает ощуще��ие, что на каждом витке развития Perl разбрасывает вокруг себя множество разнообразных граблей, которые программисты затем старательно собирают (иногда по два раза, если первые грабли были экспериментальными).

Во-первых, некоторые функции по определению работают именно с байтами, а не с символами, и строки во внутреннем представлении встают им поперек горла. К числу таких функций относятся часто используемые функции из модуля Digest::MD5. Так, приведенный пример отвалится с ошибкой Wide character in subroutine entry at test.pl line 3.:

use Digest::MD5 'md5_hex';
print md5_hex("\x{400}");


Во-вторых, данные далеко не всегда приходят в том виде, в котором их ожидает увидеть программа. Наивно ожидать, например, что в обработчик HTML-формы всегда будет приходить валидный UTF-8. Результаты излишнего доверия к источникам могут быть довольно разнообразными, начиная с порчи данных, и заканчивая фатальными ошибками при попытке их перекодировать в другую кодировку (например, при формировании email'а).

И наконец, самая частая и интересная проблема возникает при попытке конкатенации двух строк, только одна из которых хранится во внутреннем перловом формате. Допустим, у нас есть такой файлик (записанный в UTF-8):

use Encode;
$a = decode('utf8', "Мне нравится "); # строка во внутреннем формате
$b = "на Хабре"; # последовательность из 15 байт
$c = $a.$b;


В последней строке Perl пытается привести строки к общему знаменателю формату. Поскольку $b он воспринимает как цепочку байт, каждый байт этой строки перекодируется в UTF-8. В результате получится примерно такая каша (с поднятым, кстати, флагом):

$c = "Мне нравится на Хабре"

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

Заключение


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

UPD: хабраюзер codesign прислал ссылки на свои наработки по этой же теме, рекомендую: