понедельник, 13 октября 2008 г.

Perl: Асинхронный конвейер HTTP клиентов

Как-то давным давно почувствовал я на работе запах дыма. Думал - пожар, но выглянув в коридор, увидел, что дым не так уж и велик, и валит из офиса SEO (search engine optimization).

Хотя было вернуться к себе на рабочие место, но природное любопытство пересилило и я решил пойти посмотреть, что твориться у оптимизаторов. Оказалось, все просто - на экранах мельтешили окна, все были ужасно заняты, а дым шел от клавиатур - плавилась пластмасса от такой нагрузки!

Когда мне наконец-то удалось привлечь к себе внимание, выяснил, что для какого-то исследования оптимизатором надо было закачать несколько десятков тысяч web страниц. Что за исследования и где они взяли этот список url я уточнять не стал, просто пожелал им плодотворной работы, а сам запомнил где лежит этот список url.

Да, придется ликвидировать причину дымa, - подумал я на обратной дороге. Нет, оптимизаторов я ликвидировать не собирался, просто решил им сделать скриптик, которых закачает все эти странички, тем более, что у меня уже был опыт работы с модулем LWP::Parallel::UserAgent.

Задача сводилась к тому, чтобы по мере обработки читать с файла новые url, асинхронно запрашивать страницы и записывать HTTP ответы в файлы. Конечно можно было все сделать последовательно, но уж больно много времени потребовалось бы на это.

Как оказалось, для организации такого конвейера более подходит модуль HTTP::Async, а не LWP::Parallel::UserAgent.

Вот примерный код, который использовался для этого (работа с файлами опущена):

use HTTP::Message 1.57;
use HTTP::Request;
use HTTP::Async;

my @urls = (
'http://www.perl.com',
'http://www.perl.org',
'http://perlmonks.org',
'http://www.pm.org',
'http://kiev.pm.org',
'http://www.parrot.org',
'http://www.parrotcode.ks.ua',
);

my $async = HTTP::Async->new;

my $max_connects = 3;
my $cnt = @urls > $max_connects ? $max_connects : @urls;
add_request() foreach (1 .. $cnt);


while ( my $res = $async->wait_for_next_response ) {
if($res->is_success()) {
print "Succeeded for '", $res->request->url, "'\n";
# print $res->content, "\n";
}
add_request();
}


sub add_request {
my $url = shift @urls or return;
my $req = HTTP::Request->new(GET => $url,
['User-Agent' => "Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; DigExt)"]);
$async->add($req);
}

В это пример первоначально создается $max_connects запросов и по мере их завершения, создаются все новые и новые.

Все очень просто. Но опять у меня проснулась любопытство: а нельзя это все таки для такой конвейерной обработки приспособить модуль LWP::Parallel::UserAgent?

Оказалось можно. Для этого просто нужно переопределить события on_return и on_failure:

$SIG{PIPE} = 'IGNORE';
use LWP::Parallel::UserAgent;

my @urls = (
'http://www.perl.com',
'http://www.perl.org',
'http://perlmonks.org',
'http://www.pm.org',
'http://kiev.pm.org',
'http://www.parrot.org',
'http://www.parrotcode.ks.ua',
);

my $ua = LWP::Parallel::UserAgent->new();
$ua->agent("Mozilla/4.0 (compatible; MSIE 5.0; Windows 98; DigExt)");
$ua->nonblock(1);

my $max_connects = 3;
my $cnt = @urls > $max_connects ? $max_connects : @urls;
add_request() foreach (1 .. $cnt);


sub add_request {
my $url = shift @urls or return;
my $req = HTTP::Request->new(GET => $url);
$ua->register($req);
}


{
no warnings;
sub LWP::Parallel::UserAgent::on_return {
my ($self, $request, $response, $entry) = @_;
if($response->is_success()) {
print "Succeeded for '", $response->request->url, "'\n";
}
add_request();
}

sub LWP::Parallel::UserAgent::on_failure { add_request() }
}

my $entries = $ua->wait(3);

Что ж не так красиво, как при использовании HTTP::Async, но свою задачу этот код выполняет. Разве что очень не понравилась необходимость установить $SIG{PIPE} = 'IGNORE';

Ну вот и все история... :-) Да... давно это было... Но тем не-менее, кому-то может и пригодиться этот опыт и сейчас.

Копия: http://kiev.pm.org/node/253

четверг, 18 сентября 2008 г.

О ключике -w замолвите слово

Прагм strict и warnings обычно достаточно.
Ключик -w использую в основном в однострочниках.
Но, как оказалось, о нем забывать не стоит!

Вот реальная история.

При разработке системы, использующих подгруздку модулей, в одном из таких модулей была сделана ошибка. По какой-то причине кто-то поставил двоеточие после слова package, подобно вот этому:

> cat Foo.pm
package: Foo;

use strict;
use warnings;

# ...

1;

Однако, при загрузке этого модуля никаких предупреждений не выводится:

> perl -c -MFoo -e 1
-e syntax OK

Хотя перед загрузкой включены и strict, и warnings:

> perl -c -Mstrict -Mwarnings -MFoo -e 1
-e syntax OK

Все дело в том, что в файле Foo.pm прагма warnings подключается после объявления модуля.
Если в подключать прагму до объявления модуля, то ошибка легко диагностируется:

> cat Foo.pm
use strict;
use warnings;

package: Foo;

# ...

1;

> perl -c -MFoo -e 1
Bareword "Foo" not allowed while "strict subs" in use at Foo.pm line 4.
Compilation failed in require.
BEGIN failed--compilation aborted.

Но писать так как-то не очень привычно и не хочется, да и где гарантия, что другой разработчик будут также писать так?

Вот тут и приходит на помощь ключик -w:

> perl -wc -MFoo -e 1
Useless use of a constant in void context at Foo.pm line 1.
-e syntax OK

Хотя файл Foo.pm и загружен, но предупреждение позволяет легко понять, что имеется ошибка.

Так что ключик -w рано выкидывать на свалку истории, как это можно подумать из perldoc perllexwarn.

Копия: http://kiev.pm.org/node/235

четверг, 19 июня 2008 г.

Perl: Cooking warnings

Данная заметка касается некоторых тактических приемов обработки предупреждений и не претендует на полноценный обзор систем логирования будь-то Log::Dispatch или Log::Log4perl.

Обработка предупреждений

Обычно имея лишь предупреждение подобное этому: "Use of uninitialized value in hash element at ...", - практически не возможно установить, что привело к нему. Необходима дополнительная информация: минимум входящие данные. Поэтому обработку предупреждений можно разделить на два уровня.

Первый уровень обработки предупреждений простой: он отлавливает предупреждения и ошибки, которые возникают до того, как заработает второй уровень. Для этого можно использовать CGI::Carp, например:

use CGI::Carp qw(carpout);
open (LOG, ">>/var/log/.../foo.log");
carpout(*LOG);

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

Простейшая реализация обработки предупреждений второго уровня показана ниже.

Подключение обработчика и вызов подпрограммы записи предупреждений:

use Foo::Log;

sub main {
# ...
local SIG{__WARN__} = warn_trap();
# ...
write_warn($id_source, $id_input_data);
}

Непосредственно модуль обработки (без экспорта имен и полезных прагм):

package Foo::Log;

my @warnings = ();

sub warn_trap() {
@warnings = ();
my $old_warn_trap = $SIG{__WARN__};
return sub {
my $warn = shift;
push @warnings, $warn;
if ($old_warn_trap) {
# Конечно, если хотим передать сообщение
# вышестоящему обработчику.
local $SIG{__WARN__} = $old_warn_trap;
eval { &$old_warn_trap($warn) };
}
};
}

sub write_warn(;$$) {
my ($id_source, $id_input_data) = @_;
if (@warnings) {
# Открываем лог файл и
# сохраняем $id_source, $id_input_data и @warnings.
# ...
}
}

Почему не устанавливаю ловушку для __DIE__?
Потому что предпочитаю ловить die в eval и вызывать warn $@, да и первый уровень на страже.

Отладка

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

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

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

Нет проблем: используем callar и @DB::args в __WARN__ обработчике.

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

Конечно, полученные данных из базы и логику можно разнести по разным подпрограммам - так и стараюсь делать, но не всегда это возможно сделать красиво. Ведь данные из базы извлекаются при необходимости по мере прохождения по логическим цепочкам.

Более красивым решением может быть тактика точечных обработчиков предупреждений.

Локальный точечный обработчик устанавливается максимально близко к месту возникновения ошибки и ему указывается что необходимо сделать при возникновении предупреждения.

Пример реализации точечного обработчика:

use Data::Dumper;

sub pointed_warn_trap(@) {
my @data = @_;
my $old_warn_trap = $SIG{__WARN__};
return sub {
my $message = join "", shift, Dumper @data;
$old_warn_trap ? eval { &$old_warn_trap($message) } :
warn $message;
};
}

Пример использования точечного обработчика:

local $SIG{__WARN__} = pointed_warn_trap($foo, $too);

В этом примере $foo может быть обрабатываемыми данными, а $too - информацией из базы.

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

Заключение

Эти приемы обработки предупреждений хоть и просты, но очень помогают отлавливать ошибки и сделать код более качественным.

P.S.
Копия: http://kiev.pm.org/node/188.

вторник, 27 мая 2008 г.

Один нюанс использования map

Когда блок map должен вернуть ссылку на анонимный хеш и этот блок содержит более чем одну логическую строку, то необходимо подсказать, что фигурные скобки являются обозначением анонимного хеша, а не блока лексической области видимости. В качестве подсказки могут выступать круглые скобки, в которые обрамляется анонимный хеш.

Примеры.

Простой блок - perl понимает сам, что имеет дело с анонимным хешем:

> perl -MData::Dumper -e 'print Dumper [map { { 1 .. 4 } } (1 .. 2)]'
$VAR1 = [
{
'1' => 2,
'3' => 4
},
{
'1' => 2,
'3' => 4
}
];

Сложный блок - perl думает, что имеет дело с лексической областью видимости:

> perl -MData::Dumper -e 'print Dumper [map { 1; { 1 .. 4 } } (1 .. 2)]'
$VAR1 = [
1,
2,
3,
4,
1,
2,
3,
4
];

Подсказка про анонимный хеш при помощи круглых скобок:

> perl -MData::Dumper -e 'print Dumper [map { 1; ({ 1 .. 4 }) } (1 .. 2)]'
$VAR1 = [
{
'1' => 2,
'3' => 4
},
{
'1' => 2,
'3' => 4
}
];

Кстати, когда анонимный хеш возвращается из обычной подпрограммы при помощи return, то нет нужды в подсказке в виде круглых скобок, так как return устраняет вышеописанную неопределенность. А вот если return отсутствует и подпрограмма возвращает последние вычисленное значение, то подсказка необходима. В блоке map return использовать нельзя.

http://kiev.pm.org/node/182

пятница, 22 февраля 2008 г.

Осторожно, Perl!

Шуточная на половину статья, в которой упоминаются некоторые перловые хаки. Perl самый опасный язык программирования, поскольку он развивает "отвратительные" качества в людях. Некоторые их называют "добродетелями" программистов: лень, нетерпение, высокомерие (laziness, impatience, hubris).

Первоначально задумывалось достоинства Perl показать как недостатки, а недостатки - как достоинства. Но, кажется, немного отклонился в сторону. Даже думал, как Гоголь, предать огню эту статью, чтобы не пугать неокрепших духом, но потом решил оставить. Правильно поступил или нет - об этом судить читателю.

Вот собственно статья.

Среди множества языков программирования один занимает особое место. Это язык Perl! Если вы его не знает и получите предложения его освоить - бегите как можно дальше, иначе...

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

Причем это происходит постепенно и от того более коварным является этот процесс. Человек этого не замечает и даже потом, когда Perl полностью им завладевает, человек не будет это осознавать. Более того, он будет даже превозносить эти отвратительные черты, как великие добродетели программистов. Но какие это же добродетели!

Возьмем, например, высокомерие. Оно, якобы, помогает писать замечательные программы. Глупости все это. Высокомерие Perl программистов настолько велико, что они даже встревают в спор С++ и OCaml программистов о производительности их языков. Сам был очевидцем такого спора. В споре шел разговор как оптимально генерируется ассемблерный код, а также затрагивались вопросы оптимизации хвостовой рекурсии... Решили провести тесты, например, посредством вычисления чисел Фибоначчи.

А тут мимо проходил Perl программист. И что вы себе думает, он посмел высокомерно утверждать, что вычислит на Perl числа Фибоначчи быстрей чем они. Поспорили на бутылку коньяка. Программисты C++ и Ocaml знали, что из себя представляет Perl, поэтому сразу согласились на спор. Но они не знали, что из себя представляют Perl программисты. Кто бы мог подумать, что этот Perl программист выиграет спор! Он первый вычислил числа Фибоначчи! Когда остальные участники спора, отказываясь верить словам, подошли увидеть все своими глазами, Perl программист запустил на исполнение следующий код:

use strict;
use warnings;
use Memoize;
memoize('fib');

sub fib {
my $n = shift;
return $n if $n < 2;
fib($n-1) + fib($n-2);
}

my $n = $ARGV[0];
print "fin($n) = ", fib($n), "\n";

Конечно, для тех, кто знаком с модулем Memoize, все становиться ясно с первого взгляда.

Perl самый быстрый язык, не потому, что его интерпретатор быстр, а потому, что быстры и ясны мысли Perl программистов. Когда другие жонглирую байтами, Perl программисты не забывают об оптимизаций алгоритмов.

С дрогой стороны, Perl не только "самый быстрый язык", но и самый ленивый. Нет, не надо говорить, что самый ленивый - это Haskell. Вы бы посмотрели, как иногда Haskell программисты борются с ленивостью своего языка. Perl ленив по другому. Правильно, он ленив, потому, что ленивы его использующие люди. Кстати, и потому что они ленивы, они даже не нагружают свой компьютер лишней работой: вспомните про модуль Memoize. А ленивые списки и прочие есть и у Perl, например: Data::Lazy, Scalar::Defer, Object::Lazy, Tie::Scalar, Tie::StdScalar и даже DBIx::LazyMethod.

Perl также славиться тем, что "делает за вас вашу работу". Вроде-бы это очень хорошо, но постойте, это же самая большая "медвежья услуга". Со временем вы обленитесь и ваша лень не будет иметь границ.

Ну а поскольку Perl очень быстр и ленив, то он делает людей его использующих очень нетерпеливыми.
Быстро сделав свою работу, вы будете с нетерпением ждать, когда, например, программисты, реализующие клиентскую часть проекта, напишут и отладят свой код. Вы будите скучать от безделья, нервничать от осознания того, что вас сдерживают и проект движется черепашьими темпами. Придется пить валерьянку.

Многие не в силах выдержать это, ломаются - бреют голову, посыпают ее пеплом и уходят в монастырь (http://perlmonks.org). Там они в спокойной обстановке, вдалеке от суетного мира занимаются каллиграфией, то есть обфускацией (http://www.perlmonks.org/index.pl?node=Obfuscated%20Code).

Лень привела также к тому, что Perl является сосредоточением множество закорлючер и заклинаний. (Про семейство APL языков скромно умолчу.) И все это преподноситься под соусом "cуществует более одного способа сделать это". Да ни один силач не поднимет книгу Perl рецептов.

А посмотрите, что Perl программисты сделали с совершенной и строгой концепцией Объектно-Ориентированного программирования!
Ну разве это объекты? Правда Damian Conway (Tie::SecureHash, Class::Contract) пытался образумить Perl программистов, да что толку. Как-бы в ответ, появились модули, позволяющие делать всевозможные хаки, залезать в чужие области видимости и менять значения локальных структур! Ужас! (Перечень модулей, опасных для неокрепших духом, вырезан цензором.) Хотя, надо отдать должное, на основе этих идей появилась реализация Аспектно-Ориентированного программирования на Perl (Aspect).

Так что, программисты, берегитесь Perl - это очень опасный язык! Когда Perl проникнет в ваши умы, вы никогда больше не сможете нормально писать на других языках программирования. И этому также будет способствовать то, что Perl из всех языков программирования дальше всего находиться от компьютера и ближе всего к человеку. Не дай Бой, вы из программиста станете писателем или поэтом. А ведь были уже прецеденты, например, королева поэзии Perl Sharon Hopkins.

С другой стороны, чтобы стать настоящим мастером, надо пройти через все это. Но берегись.
Perl - это как обоюдоострый меч, которым можно делать чудеса, а можно серьезно пораниться.

Вы спросите, как же мне, использующему Perl, удалось избежать его пагубного влияния?
А у меня иммунитет - я же не программист. Я химик! :-

Коментарии и пожелания оставляте в http://kiev.pm.org/?q=node/157

среда, 6 февраля 2008 г.

Perl JSON модули с поддержкой UTF-8

Введение

Статья представляет обзор perl модулей для работы с JSON в контексте UTF-8. Подразумевается работа с UTF-8 на уровне символов, а не байтов. Если нужна работа на уровне байт, то подойдет любой из нижеперечисленных моделей, который устроит по скорости.

На CPAN существуют следующие модули:

  • JSON - parse and convert to JSON (JavaScript Object Notation).

  • JSON::XS - JSON serialising/deserialising, done correctly and fast

  • JSON::Syck - JSON is YAML

  • JSON::PC - fast JSON Parser and Converter (JSON::PC is a XS version of JSON).

  • JSON::DWIW - JSON converter that Does What I Want

В качестве тестовых данных будем использовать следующий JSON:

 { "hello":"Hello, \u00ab\u043f\u0440\u0438\u0432\u0435\u0442\u00bb \u2116 \u263a. Good by." }

В JSON содержится фраза ``Hello, <<привет>> #. Good by.'', в которой кавычки, номер и смайлик ``уникодные''. Именно как \uxxxx в JSON кодируются UTF-8 символы (http://json.org).

Этому JSON соответствует следующая Perl структура:

 { 'hello' => "Hello, \x{ab}\x{43f}\x{440}\x{438}\x{432}\x{435}\x{442}\x{bb} \x{2116} \x{263a}. Good by." }

Ну что-ж теперь приступим к тесту моделей.

Обзор модулей

JSON.

Для работы у UTF-8 в конструкторе необходимо это указать:

 JSON->new(utf8 => 1);

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

JSON::PC.

Как указано в документации на модуль, JSON::PC является XS версией модуля JSON, и как истинный приемник наследует туже проблему с русскими или французскими кавычками.

Модуль сам разбирается где UTF-8, ничего явно указывать не нужно.

JSON::Syck.

Модуль JSON::Syck написан сообразительной китаянкой, которая заметила что JSON по сути является YAML, и просто использовала библиотеку libsyck.

UTF-8 не поддерживается вообще, модель ориентирован на работу с октетами, о чем честно написано в документации. Подразумевается \uxxxx, а не ImplicitUnicode опция.

JSON::XS.

А вот модуль JSON::XS показал себя достойно со всех сторон. Правда перед преобразованием из JSON в Perl надо указать флаг utf8, а при обратном преобразовании - флаг ascii.

Из JSON в Perl:

 use JSON::XS;
my $json_xs = JSON::XS->new();
$json_xs->utf8(1);
$json_xs->decode($json_data);

Из Perl в JSON:

 use JSON::XS;
my $json_xs = JSON::XS->new();
$json_xs->ascii(1);
$json_xs->encode($perl_data);

Можно также задать флаг pretty для красивого форматирования результирующего JSON.

JSON::DWIW.

Последний модуль JSON::DWIW также ведет себе отлично, необходимо лишь в конструкторе сообщить об UTF-8:

 use JSON::DWIW;
my $json_dwiw = JSON::DWIW->new({ escape_multi_byte => 1 });
$json_dwiw->from_json($json_data);
$json_dwiw->to_json($perl_data);

Бонусы

Два пакета JSON::XS и JSON::DWIW нормально обрабатывают ситуацию, когда передаваемая им JSON строка не является последовательностью октетов, а является perl строкой с UTF-8 - не надо делать лишнюю проверку.

Benchmark

До этапа тестирования производительности успешно добрались лишь два модуля: JSON::XS и JSON::DWIW, - к которым нет ни одной претензии.

Собственно тест скорости на не слишком большой структуре:

 use Benchmark qw(cmpthese);
 cmpthese(10000, {
'JSON::XS' => sub {
my $json_xs = JSON::XS->new();
$json_xs->utf8(1);
$json_xs->decode($json_data_u);
$json_xs->ascii(1);
$json_xs->encode($perl_data_expected)
},
'JSON::DWIW' => sub {
my $json_obj = JSON::DWIW->new({ escape_multi_byte => 1 });
$json_obj->from_json($json_data_u);
$json_obj->to_json($perl_data_expected)
},
});

А вот и результаты:

               Rate JSON::DWIW   JSON::XS
JSON::DWIW 5246/s -- -78%
JSON::XS 24151/s 360% --

Вывод

Для меня вывод однозначен --- JSON::XS. Модуль ведет себя отлично с UTF-8, корректно преобразовывает данные в Perl строки и к тому же обладает приличной скоростью.

P.S. Для комментарий - http://kiev.pm.org/?q=node/153

понедельник, 28 января 2008 г.

Объекты? Зачем?! Точнее, не всегда. Замыкания!

Заметка не предназначена для разжигания священной войны: хорошо или плохо объектно ориентированное программирование. Я сам использую его наравне с другими.

Цель заметки --- показать, что иногда проще обойтись без объектов, а использовать замыкания.

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

Рассмотрим подобный объект на примере абстрактного "сумматора" чисел. При инициализации "сумматору" передается первоначальное значение, а при каждом вызове метода к текущему значению прибавляется передаваемое. Вот код такого "сумматора" в объектно-ориентированном стиле:

package Foo;

sub new {
my $class = shift;
my $foo = shift;
return bless \$foo;
}

sub foo {
my $self = shift;
$$self += shift;
print $$self, "\n";
}

1;

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

use Foo;
my $foo = Foo->new(3);
$foo->foo(1);
$foo->foo(1);
$foo->foo(2);
$foo->foo(2);

Теперь сделаем аналогичный по функционалу "сумматор" с использованием замыкания:

sub make_foo {
my $foo = shift;
return sub {
$foo += shift;
print $foo, "\n";
};
}

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

my $foo = make_foo(3);
$foo->(1);
$foo->(1);
$foo->(2);
$foo->(2);

Как видим вариант с замыканием проще, компактней и элегантней.

Пожалуй на этом и остановимся --- читатель дальше сможет сам развить тему. Приведу лишь цитату Стива Маджевски: "Объект --- это совокупность данных вместе с привязанными к ним процедурами... Замыкание --- это процедура вместе с привязанной к ней совокупностью данных."


P.S.
Кстати, Parrot оперирует объектами (PMC),
но при вызовах подпрограмм использует стиль передачи продолжений, который не мыслим без замыканий.

P.P.S
Копия и комментарии находятся на kiev.pm.org