Пользователь
0,0
рейтинг
11 марта 2009 в 19:49

Разработка → Пара слов про 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 прислал ссылки на свои наработки по этой же теме, рекомендую:
Bambr @Bambr
карма
65,2
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (53)

  • 0
    О!) Что именно всё же значит — Wide characters in print и как это эффективно бороть, не зная заранее с какой кодировкой работаешь?=)
    • +1
      Значит, что в файловый дескриптор напечатаны данные с UTF-флагом. Вариантов куча, есть правильные, есть удобные.
      0) Идеальное решение — сделать так, чтобы данные всегда были в одном единственном формате.
      1) no warnings 'layer' — самый эффективный способ избавиться от варнингов :)
      2) при помощи open или binmode укажите адекватную кодировку для файлового дескриптора. open подойдет для открываемых Вами файлов, binmode — для уже открытых дескрипторов типа STDOUT. Тут может возникнуть конфуз, если на печать пойдут обычные байты, без флага — данные покоцает.
      3) ручками следите за тем, что печатаете, и при необходимости опускайте флажок
      • 0
        А если речь идёт не о файле, а о получаемых «извне» — в данном случае — из интернета «скалярах»?)

        Я нашёл модуль Фитцпатрика — is_utf8 — и мы экспериментировали с «шириной» символа — но эффективно побороть не удалось всё же(
        • 0
          Не совсем понятно, с чем именно Вы боролись. Что касается получаемых откуда-то данных, то у них не бывает флага юникода, но его можно поставить или снять при помощи функций из Encode — _utf8_on или _utf8_off.
          • 0
            Угу. У нас все входные данные автоматически перекодируются из кодировки сайта в utf-форму, потом стараемся работать только с уникодом.
          • 0
            хм. То есть я правильно понимаю, что получаемая wide characters — это свидетельство флага юникода?
            • 0
              Да. Любое упоминание про wide characters это свидетельство уникода, причем с utf-флагом
              • 0
                Ну тогда получается, что бывает флаг юникода у получаемых откуда-то данных? Я же могу доверять своим глазам :)

                Если конкретно — то через LWP получаемых=)
                • 0
                  Не верьте глазам своим. Внешние данные никогда не имеют флага юникода. Просто у Вас где-то неявно взводится этот флаг.
                  • 0
                    OK, буду копать=)

                    Как что откопаю — отпишу :)

                    Спасибо за консультацию=)
                    • 0
                      Там могут быть совершенно чудесные вещи. Например, у меня был такой случай:

                      Есть две строки, $str1 и $str2. Строка $str1 возвращается из какого-то модуля и имет флаг юникода. А строка $str2 была задана вручную и не имеет флага.

                      При конкатенации этих двух строк результатом становятся кракозябры. Это хоть и не очевидное, но документированное явление — нельзя конкатенировать строки с флагом и без флага.

                      Но вот чудо. Если перед конкатенацией от строки $str1 получить хеш функцией md5, то флаг с этой строки чудесным образом снимался и дальнейшая конкатенация проходит нормально.

                      $str1 = modul::func(); # возвращает строку с флагом
                      $str2 = 'привет';
                      print $str1.str2; # кракозябры
                      Digest::md5($str1);
                      print $str1.str2; # все нормально

                      Заметьте, полученный хеш никак не используется. Просто строка, которая была отправлена в функцию md5 каким-то неявням образом теряет флаг юникода.

                      Правильный способ решения — вместо md5 использовать функцию _utf_off, которая явно снимает флаг. Но не относящийся к делу md5, елки-палки, тоже работает:)

                      Это я к тому, что у Вас где-то аналогичным чудесным образом флаг неявно устанавливается.
                • 0
                  Попробуйте сразу после получения данных (совсем сразу!!) проверить их функцией is_utf8. Уверен, флага не будет. Ну и ниже проверочки по коду раскидайте, особенно если Вы к переменной с результатами LWP что-либо конкатенируете (даже цифру или пустую строку).

                  Кстати о цифрах. Когда я впервые столкнулся с уникодом, мне нужно было вклеить результаты разбора XML в письмо. Я честно опустил флаг у всего что можно было, но шаблон все равно портился из-за двойной перекодировки. Единственное что я не сделал — пропустил без изменений одну циферку. Проблема была там.
  • +4
    Расскажу интересную историю. Правда, детали я уже успел подзабыть, так что где-то могу ошибиться. Если кто-то поправит, а еще лучше, подкрепит примерами из исходников Perl, буду признателен.

    Где-то полгода назад мне пришлось разбираться, как на низком уровне работает поддержка UTF8 в Perl (включая изучение исходников и т.д.). И выяснилась одна очень интересная деталь: по сути, вся встроенная «ядровая» поддержка ограничивается этим самым флагом is_utf8 с минимальной его интерпретацией. Все остальное сделано на Perl-коде. Например, для полноценной работы с Unicode Perl так или иначе подключает encoding.pm, а также 500-килобайтные файлы из lib/Unicode (а возможно, из lib/unicore — я сейчас уже не вспомню). Причем делается это довольно интересно: в Perl-коде можно повесить недокументированный «callback» на ряд операций с unicode-данными, и этот callback будет вызываться из Си-кода ядра Perl. При этом модули типа encoding.pm подсовывают в роли данного callback-а… код на Perl, работающий с огромными файлами в lib/Unicode. Т.е. из ядра Perl вызывается Perl-код, а ядро ничего про Unicode не знает (оно знает про флаг is_utf8, но интерпретирует его только как подсказку для вызова хуков; с тем же успехом он мог называться is_blablabla). Так что с этой точки зрения в Perl по-настоящему встроенной поддержки Unicode как не было, так и нет.

    Все это я обнаружил, пытаясь минимизировать объем Perl-кода, который должен поставляться вместе с perl.exe для работы с Unicode. Я просто начал постепенно удалять файлы и смотреть, что происходит. По зависимостям потянулись описанные выше явления.

    Почему они не использовали стандартный и проверенный iconv для поддержки Unicode, а сделали обработку через хуки, для меня так и осталось загадкой.

    В итоге, как это ни печатльно, по совокупности причин пришлось отказаться от «встроенной» поддержки Unicode совсем (т.к. без файлов в lib/Unicode флаг is_utf8 бесполезен), вместо этого полагаясь на старый механизм и выполняя перекодировки вручную, где это необходимо (вопрос объема и скорости загрузки был критичен). Также я пришел к выводу, что поддержка Unicode в Perl — весьма костыльная, или уж — во всяком случае — по своей чистоте сильно проигрывает таковой в Javascript и Python. Она примерно такая же костыльная, как в PHP5, а может, даже и хуже (потому что в PHP есть какой-никакой встроенный mbstring).
    • +1
      Да, для тех, кто захочет копать. Ключевые слова: SWASH, SWASHGET, encoding.pm, utf8.pm, open.pm. (Кстати, мне никогда не нравилось называть «use utf8» и другие похожие штуки вроде «use strict» прагмой, потому что все «прагмы» на самом деле — обыкновенные модули на Perl. Как правило, они подменяют разные «плохо документированные» переменные типа $^W, которые влияют на результаты компиляции.)
    • 0
      Да, и еще: в 5.10 такая же история, как в 5.8. Ничего не изменилось.
      • 0
        Это даже хорошо, что не изменилось. Если бы они в очередной раз поменяли логику работы, как это было с 5.6, на него бы просто никто не перешел.
        • 0
          Я до сих пор не перешел, его в портах еще нету:)
    • 0
      В чём костыльность-то? Вы хотели, чтобы все функции были реализованы в одном бинарнике интерпретатора?
    • 0
      Спасибо за комментарий, настолько глубоко я не копал. Но то, что реализация оставляет желать лучшего — это факт. Даже с банальным вебом проблема — у нас на сервере вывод буферизируется в перловую переменную, неловкое движение с флагом — и кусок страницы испорчен. Да и вообще неудобно. Но наверное это лучше чем совсем ничего. Ждем perl6, там вроде обещали полноценный strict utf зашитый по самое нехочу.
    • 0
      Как на Javascript подсчитать md5/sha1 сумму от не-ASCII файла?
      • 0
        поправка: сериализовать в utf-8 и подсчитать…
  • –4
    Хорошая статья.Спасибо. Только вот на PERL я практически не программирую. Написал бы ктонибудь подобную по php ато даже mb_string не во всех случаях помогает.
  • НЛО прилетело и опубликовало эту надпись здесь
    • –4
      Спасибо большое, добрый человек. Будем пробовать…
  • 0
    Почему погода показывается только для Москвы?
    GeoIP отменили? Сделали хотя бы выбор города — ведь и того нет.
    • 0
      Упс. Не в том топике закомментил :-D
  • НЛО прилетело и опубликовало эту надпись здесь
    • +1
      Было бы неплохо, если бы хоть кто-нибудь объяснил, зачем нам PHP :)
      Этот блог посвящен именно перлу, Ваш вопрос здесь выглядит несколько странно.
      • НЛО прилетело и опубликовало эту надпись здесь
        • +1
          Я на подобные вопросы обычно привожу такое наблюдение:

          В книгах про Perl все сравнения и аналогии обычно проводятся с С.
          В книгах про PHP все сравнения и аналогии обычно проводятся с Perl.

          Выводы делайте сами:)
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            А где вы здесь вычитали, что Perl достойнее PHP? :))
        • +1
          Хм, кажется я Вас не совсем понял вначале, сорри.

          Выбор языка во многом субъективен. Одно дело, когда язык выбирается осмысленно под конкретную задачу — скажем, Perl принято хвалить за его возможности по работе со строками (которые последнее время можно найти в любом другом языке), C++ хвалят за скорость работы ну и т.д. Perl и PHP во многом похожи и под веб-задачу имхо можно пользоваться любым из них, в зависимости от предпочтений. Когда я знакомился с PHP, некоторые принятые там принципы, вроде подключения библиотек, встали мне поперек горла, и я этот язык оставил. Возможно, сейчас многое изменилось, но разбираться в PHP у меня желание пропало. Когда будет нечего делать, лучше питон поковыряю :)
          • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            Ну не совсем соглашусь. Для МАЛЕНЬКИХ веб-задач, действительно, выбор CGI Perl или PHP — вопрос предпочтения, и они похожи. А вот когда дело доходит до проектов с большим объемом кода и высокими нагрузками, то выбор встает не совсем между Perl и PHP, а скорее между mod_perl и PHP (или между FastCGI Perl и PHP). Названия похожи, а вот методики разработки и архитектура очень сильно различны.
  • 0
    Отличная статья, единственно, я бы убрал термин «wide characters» из параграфа под «The Perl Way», поскольку в мире статических-компиляторов под ним чаще всего понимаются элементы UTF-16/UCS2, а в perl-мире это только путает. Есть только байты и символы (числа от 0 до 2^32-1 на 32-битных платформах).
    • 0
      Спасибо. Совсем убирать этот текст мне бы не хотелось, т.к. некоторое количество выдаваемых Perl варнингов связано именно с этим термином. Пожалуй, придется прокомментировать.
  • 0
    Отлично написано, keep it up!
  • +1
    Очень-очень напоминает мой (Mons Anderson) доклад, особенно в части примеров кода.
    Либо мы очень сходно мыслим, либо кто-то случайно забыл указать используемые источники ;)
    • 0
      ты б дал ссылочку на презенташку свою с докладом или на текстовый его вариант, если таки написал уже +)
      • +2
        Ну презенташка — не вопрос.
        taka.xfo.cc/utf8.xul
        А вот статью так и не выложил
        т.е. есть она только в том спецвыпуске «Сетевых решений», который был сделан к By Perl
        www.nestor.minsk.by/sr/2008/09/sr80902.html
    • 0
      Статья писалась полностью из головы. Единственный действительно придуманный пример кода приведен в разделе «грабли», остальные естественным образом взяты из перлдоков, список которых приведен в конце статьи ;-)

      А где можно посмотреть на доклад? Если он на эту же тему, я просто прицеплю его в конце статьи, читатели будут только рады.
      • 0
        * прицеплю ссылку на него
  • 0
    А что с юникодом в 6-й версии планируется?
    • 0
      Планируется хорошо. В реальности — зависит от реализации.
      • 0
        Судя по perl6.ru/unicode/ даже слишком хорошо.

        • 0
          На самом деле всё ещё лучше :)

          <bacek> std: sub prefix:<Σ> (*@args) {...}; my @a; say Σ@a
          <p6eval> std 25806: OUTPUT«ok 00:03 41m␤»

  • –1
    Топорное решение, без модулей:

    %unicode = (
    "%u0430" => «а», "%u0431" => «б», "%u0432" => «в»,
    "%u0433" => «г», "%u0434" => «д», "%u0435" => «е»,

    );

    $query_value =~ s/(%u[0-9A-F]{4})/$unicode{$1}/eg;

    (добавил к паре слов...)
    • 0
      Вы — молодец
  • 0
    У меня проблема с расшифровкой строки из URL набранной в браузере кириллицей писал о ней еще давно тут.
    • 0
      У меня нет под рукой FF2, чтобы проверить гипотезу, но вообще при отправке запроса браузер отсылает хедер «Accept-Charset». Для моего FF3 она начинается с 1251, потом идет utf-8. Предполагаю, что если строка не распозналась как utf-8, мозилла использует первую кодировку, которая была в хедере.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.