Шаблонизаторы — зло.
use XSLT or die.
Шаблонизаторы — зло.
use XSLT or die.
Какое-то время назад обнаружилось, что ссылки на наш калькулятор расстояний между городами, которые размещают в ЖЖ, открываются, но параметры из строки запроса при этом игнорируются.
Оказалось, что это происходит только с MSIE, и именно с теми ссылками, которые находятся в блогах на livejournal.com. В строке запроса (QUERY_STRING) в таких ссылках обычно четыре параметра — два текстовых и два числовых:
Значения параметров from и to — то, что пользователь вводил в полях ввода, а from_which и to_which уточняют запрос, выбирая из нескольких одноименных названий одно конкретное. (В общем-то, несколько избыточно, и от первых двух параметров мы уже отказались, но суть не в этом.)
После перехода по ссылке в адресной строке вместо URI-escaped-последовательности оказывались русские буквы, и первое подозрение было — MSIE не очень хорошо работает с такими адресами и передает на сервер что-то искаженное.
MSIE, однако, хорош тем, что щелкает при загрузке страницы, и вместо одного щелчка при переходе по ссылке было два. Оказалось, что товарищи жжсты подменяли ссылку и превращали ее в ссылку на их бесконечно хитрый редиректер (который сначала загружал ту же страницу ЖЖ, а потом делал редирект):
После этого происходил переход на адрес, в котором вместо from=%D0%9F%D0%B0%D1%80%D0%B8%D0%B6 оказывалась неэкранированная последовательность байт, эквивалентная запросу from=%c1%e0%f0%f1%e5%eb%ee%ed%e0. А это уже не изначальный UTF-8, а почему-то голый Windows-1251.
Реальные же непонятки возникли, когда оказалось, что оставшиеся параметры from_which и to_which были вообще не видны на сервере. Но такого же не может быть! В переменной $ENV{QUERY_STRING} строка была вся и целиком.
Мы пользуемся библиотекой libapreq2 для разбора параметров, чтения кук и загрузки файлов. Perl-модули Apache2::Request и др. предоставляют интерфейс, аналогичный традиционному CGI.pm, и в нехохошем до сих пор замечены не были.
А тут в списке параметров, возвращаемых вызовом $req->param(), оказывались только те, которые стояли в строке запроса до первой пары со значением в кодировке Windows-1251.
Круто, лезем с Сергеем в код libapreq2 (предварительно обновившись до последней версии, но неадекватное поведение осталось).
Сказать, что я был удивлен, — ничего не сказать. Libapreq2 при разборе строки делает бешеное число действий для того, чтобы распознать кодировку запроса. Казалось бы, зачем она это делает, если Apache2::Request все равно в итоге отдает октеты?
Алгоритм выявления кодировки (см. файл util.c) такой:
Да-да, Latin-1 и CP1252. (Сразу видно, что писано моноглотами.) Функция, выполняющая эти эвристики, кстати, называется apreq_charset_divine. Divine — англ. гадать, предсказывать, пророчествовать.
Первый байт строки %c1%e0%f0%f1%e5%eb%ee%ed%e0 содержит два единичных старших бита, и функция-пророк считала его началом UTF-последовательности. Правда, на втором же байте понимало, что это не так, и... и в итоге функция apreq_parse_query_string (см. файл param.c) завершала работу:
s = apreq_param_decode(¶m, pool, start, nlen, vlen);
if (s != APR_SUCCESS)
return s;
apreq_param_tainted_on(param);
apreq_value_table_add(¶m->v, t);
Ни сама пара, в которой содержалась строка в кодировке Windows-1251, ни все последующие не попадали в таблицу параметров строки запроса.
Теперь понятно, как по-быстрому исправить ошибку. Если декодирование не удалось, то просто не делаем действий по сохранению пары ключ=значение в таблице, но продолжаем разбор строки со следующего символа.
s = apreq_param_decode(¶m, pool, start, nlen, vlen);
if (s == APR_SUCCESS) {
apreq_param_tainted_on(param);
apreq_value_table_add(¶m->v, t);
}
Собираем libapreq2, подменяем libapreq.so, и — вуаля! — видим значения всех параметров после тех, что проигнорировали.
P. S. В исходниках есть моноглотские определения типа
enum apreq_charset_t { APREQ_CHARSET_ASCII = 0, APREQ_CHARSET_LATIN1 = 1, APREQ_CHARSET_CP1252 = 2, APREQ_CHARSET_UTF8 = 8 }
и самописаня функция apreq_cp1252_to_utf8, реализация которой напоминает мой код 2002 года :-)
Гугл-карты с третьей версией API научились рисовать путь между двумя пунктами на Земле по «большому кругу» (great circle), поэтому без жалости можно отправить в архив фрагмент кода, вычисляющий координаты дуги.
my $span = 10;
$span = int($distance) / 100 if int($distance) > $span * 100;
my $arc = (new Geo::GoogleEarth::Pluggable)
->GreatCircleArcSegment(
startPoint => {
lat => $coords_pair[0],
lon => $coords_pair[1]
},
endPoint => {
lat => $coords_pair[2],
lon => $coords_pair[3]
},
span => $span * 1000, # meters
);
my $arcNode = add_node($pairNode, 'arc');
for (@{$arc->coordinates}) {
add_node(
$arcNode,
'point', {
lat => $_->lat,
long => $_->lon,
},
);
}
Еще большая часть ушла из яваскрипта. Перед этим мой коллега проверил, насколько расходятся пути Гугла и наш. Чтобы заметить разницу, пришлось сократить число точек, иначе обе линии полностью сливались.

В феврале этого года O’Reilly издало книгу «RESTful Web Services Cookbook», и в этой связи есть смысл упомянуть модуль, который появился на спане в начале прошлого года, а сегодня обновился, — REST::Client, который, возможно будет в чем-то удобнее традиционных LWP et. al.
Использование весьма прямолинейно (хотя и не без странностей): названия методов REST::Client совпадают с именами методов HTTP-протокола: GET, PUT, POST, DELETE, OPTIONS, HEAD.
use v5.10;
use strict;
use REST::Client;
my $client = new REST::Client;
$client->OPTIONS('http://onperl.ru/');
say $client->responseCode();
say $client->responseContent();
$client->HEAD('http://onperl.ru/');
say $client->responseCode();
$client->GET('http://onperl.ru/');
say $client->responseCode();
Дополнительно имеется возможность использовать SSL, и, что интересно, создать для полученного XML-ответа XPath-контекст.
Вот пример того, как переписать скрипт, получающий курс для любой пары валют, с использованием модуля REST::Client, и с честным разбором XML.
#!/usr/bin/perl
use v5.10;
use strict;
use REST::Client;
my ($from, $to) = map {uc} @ARGV;
unless ($from) {
say "Usage: rate FROM [TO]\n";
exit;
}
$to //= 'RUB';
my $client = new REST::Client;
$client->GET("http://whoyougle.com/money/api/$from/$to");
say $client
->responseXpath
->findvalue('/currency/rate/value/text()');
Проверка:
$ ./rate usd
29.2416
$ ./rate eur usd
1.3393
Автор ясень, автоматически переводятся с английского языка.
Краткая версия: SayPerl.org сайт запущен. Она обеспечивает Английский перевод для Perl сообщения со всего мира.
Длинная версия:
Железный Человек и PlanetPerl.ru показали, что Есть много людей Perl, которые пишут о Perl и вы никогда не слышали о них.
Более того, по железной человек, которого мы видим множество отличных от английского должности, а те из нас, с не-носителями английского как правило, может читать только свой родной язык и английский язык. Например, много японских сообщения появляются, и я вижу только куски кода Perl понятно там.
Копирование-вставка после URL-адреса в Google Translate на ежедневной основе очень раздражает. Я поймал себя на начало чтения последних итальянских сообщения о предстоящих YAPC:: Европе, а затем переход к Google Translate, как я не могу много читать итальянский еще. То же самое с немецким. Оба они являются более понятными, чем японские, хотя :-)
Сегодня я начал SayPerl.org сайт, который блоге агрегатор сообщений в сочетании с автоматическим переводчиком на английский язык.
Все сообщения, вне зависимости от языка оригинала, появляются на SayPerl.org на английском языке, таким образом, теперь я могу читать все, включая японский, персидский и китайский языки!
Конечно, автоматический перевод не совершенен, но, благодаря Google, это дает быстрый и подходящие результаты, особенно, когда цель является английский.
Чтобы продемонстрировать преимущества SayPerl.org, сравнивать эти две должности, оригинальных и переводных:

Слово "Rakvdv", который выглядит так, как это будет написано древние римляне, четко понятно сейчас.
Я постараюсь исправить некоторые вопросы, и добавить больше функциональных возможностей на сайте в течение следующих недель.
Что касается технических деталей, я только хотел бы отметить еще одну проблему с обработкой Unicode. Так произошло и с WebService:: Google:: Язык. Такие ошибки являются одним из наиболее распространенных проблем с модулями Perl сегодня (вспомните, например, еще одна проблема с Unicode возникает тогда, когда Алекс кодирования PlanetPerl.ru).
Начало использования SayPerl.org прямо сейчас!
В серверном коде, обслуживащем сайт, блоки given/when весьма удобны в частности для того, чтобы последовательно протестировать URL запрошенного ресурса и выполнить соответствующие действия:
given($url) {
when(m{^/$}) {home_page()}
when(m{^/about/$} {about_page()}
. . .
}
В Perl 5.10 помимо операторов given/when доступны именованные сохраняющие скобки, которые могут облегчить выделение параметров из адреса одновременно с его разбором:
given($url) {
. . .
when(m{^/news/(?<year>\d+)/(?<month>\d+)/(?<day>)/$})
{news_that_day()}
}
Параметры, захваченные именованными скобками, попадают в хеши %+ и %-, которые следует передать дальше на обработку.
Однако, в зависимости от общей архитектуры (в нашем примере — схемы разбора URL) может проявиться ограниченность области видимости, в которой доступны упомянутые хеши. То есть сразу после закрывающей скобки, завершающей блок given, сохраненные значения перестают быть доступными.
given($url) {
. . .
}
say Dumper(\%+); # Пусто
В этом месте кода переменные %+ и %- сохраняют значения, полученные до начала блока given.
Вплоне логично, хорошо локализовано, но не всегда удобно.
Разумеется, возможно сохранить собранные данные непосредственно в блоке given, хотя при легкомысленном проектировании это легко приводит к большим фрагментам повторяющегося кода:
given($url) {
. . .
when(m{^/(?<type>this)_page/$})
{%data = %+; do_this()}
when(m{^/(?<type>that)_page/$})
{%data = %+; do_that()}
}
Небольшая предыстория.
Верстка сайта Microsoft сделана на XSLT. Разумеется, многие XSLT-файлы зависимы от других, и при верстке довольно часто приходилось пересохранять неизмененный основной файл, хотя изменения делались где-то в другом файле, который подключался. А это напрягало.
Студийный parser долгое время тоже не умел обновлять кеш зависимых XSLT-шаблонов. Это тоже напрягало (хотя более напрягал, конечно, сам parser).
По мере возможности я стараюсь кешировать разобранные и скомпилированные XSLT-файлы. На спане есть модуль XSLT::Cache, который, однако, не отслеживает зависимости. Недостаток призван компенсировать модуль XSLT::Dependencies.
use XSLT::Dependencies;
my @dep_list = XSLT::Dependencies->new->explore('myfile.xslt');
В списке @dep_list оказывается перечень всех файлов, которые прямо или косвенно подключены к файлу myfile.xslt.
Интересно, что даже на весьма шаблоноемком проекте нашлось лишь три-четыре шаблона, которые зависят от четырех других, в то время как большинство (130 файлов) зависит всего от двух.
В todo числится обучить модуль читать XSLT не только напрямую из файла, но и из уже разобранного дерева XML::LibXSLT.
P. S. На типовом сайте, однако, отслеживать зависимости и не нужно: при разработке кешировать ничего не требуется, а на продакшне ничего не обновляется :-)
Один из бешеных японцев™ — Наоки Томита — сегодня опубликовал на спане интересный модуль Tempalte::Semantic, один из немногих yet another-шаблонизаторов, на который стоит обратить внимание.
Сам шаблон представляет собой обычный HTML-фрагмент, а данные в него подставляются из хеша, структура которого похожа на описание правил CSS или путей XPath. Важно, что в HTML-код шаблона не требуется вписывать никаких искусственных конструкций для интерполяции переменных.
Например, чтобы вывести заголовок на страницу, сначала создаем HTML-заготовку:
<html>
<head>
<title></title>
</head>
</html>
А затем описываем правила а-ля CSS:
use v5.10;
use strict;
use Template::Semantic;
say Template::Semantic->process(
'title.html',
{
'html title' => 'My Title',
}
);
Эта программа напечатает HTML-код с подставленным заголовком:
<html>
<head>
<title>My Title</title>
</head>
</html>
— Fantastique!
Описания с CSS-селекторами или XPath-адресами допустимо создавать и более сложной структуры, в том числе с вложенными хешами и списками (последние позволяют размножать фрагменты HTML-шаблонов).
Примеры наглядно описаны в документации модуля, а кроме того собраны в отдельном файле Template::Semantice::Cookbook.
Недавно на спане появился модуль HTML::DOM, который одновременно — и парсер и билдер. Пара первых простейших экспериментов с этим модулем.
Создание HTML
use v5.10;
use strict;
use HTML::DOM;
my $dom = new HTML::DOM;
$dom->write('<html>');
say $dom->innerHTML;
Здесь модуль создаст HTML с полным набором нужных тегов:
<html><head></head><body></body></html>
Более того, правильно обрабатываются и неправильные вызовы, например такой код
$dom->write('<html>');
$dom->write('<title>My Title</title>');
аккуратно размещает тег <title> внутри <head>:
<html><head><title>My Title</title></head><body></body></html>
Чтение HTML
Базовая операция чтения файла проста:
use v5.10;
use strict;
use HTML::DOM;
my $dom = new HTML::DOM;
$dom->parse_file('test.html');
say $dom->innerHTML;
Опять же, происходит внятное преобразование до валидного HTML-кода. Если файл test.html содержит одну строку <title>My Title</title>, то на выходе получается следующее:
<html><head><title>My Title</title>
</head><body></body></html>
Этими примитивными действиями модуль не ограничивается. В его распоряжении набор методов, напоминающих методы, доступные в XML::LibXML и при обработке DOM с помощью JavaScript.
Проект WhoYOUgle, которым я занимаюсь с октября прошлого года, сегодня получил Премию рунета. Тут нужно отметить, что вся серверная часть сайта написана на Perl.