Pull to refresh

Пара слов про UTF-8

Perl *
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 прислал ссылки на свои наработки по этой же теме, рекомендую:
Tags:
Hubs:
Total votes 52: ↑48 and ↓4 +44
Views 44K
Comments Comments 53