Perl: Мини экскурс в AnyEvent - пишем паука
Мы рассмотрим AnyEvent на примере yandex паука, который собирает организации с maps.yandex.ru по поисковому слову (например "аптека"). Но перед этим разберемся с тем, что нам понадобится.
AnyEvent — это прежде всего фрэймворк для событийно-ориентированного программирования (event loops). Особенностью подобных фрэймворков является одиночное применение, т.е если ты используешь один из них, то нет возможности использовать какой-либо другой в том же коде. AnyEvent другой. Он является некой абстракцией событийных машин, как DBI является абстракцией многих апи баз данных.
Событийный подход к программированию включает использование объектов, способных реагировать на события, происходящие в системе. Событийный подход используется при разработке как самостоятельных программ, так и операционных систем, например, Microsoft Windows или OS/2 Presentation Manager. Вот пример ввода данных через STDIN:
Через событийную машину это можно реализовать при помощи коллбэков (callback) - функций, которые срабатывают при появлении события:
Как правило, в реальных задачах оказывается недопустимым длительное выполнение обработчика события, поскольку при этом программа не может реагировать на другие события. Было бы хорошо выполнить какие-либо действия, пока мы ожидаем ввода данных. Ожидание в первом примере блокирует процесс до получение данных. Во втором примере мы просто зарегистрировали событие на чтение, которое не блокирует процесс. Просто когда произойдет ввод данных, вызовется коллбэк, который и прочитает данные.
Метод AnyEvent->io создает I/O "watcher", я его называю "смотрящим". Он так называется потому, что он слушает файловый (либо какой-либо другой) дескриптор, на факт происхождения нужного нам события.
Вернемся к I/O "watcher. Этот пример не совсем рабочий. Наш коллбэк не вызовется - мы должны запустить событийную машину. Событийная машина может блокироваться, если ей нечего делать или нет больше событий. Например, если использовать POE и не вызвать stop - то он будет висеть пока не сработает таймаут, а это порядка 10 сек. Мало кому это понравится.
В AnyEvent этого можно избежать используя "условные переменные" (condition variables). Это можно назвать синхронизатором или ядром AnyEvent. Condition variables имеет две стороны: заказчик (ждет выполнения условия) и исполнитель (их исполняет).
В нашем примере, в качестве исполнителя выступает коллбэк. Но у нас нет заказчика, надо это исправить:
Мы создаем AnyEvent condvar вызовом метода AnyEvent->condvar, это и есть наше ядро. Потом создаем watcher, но в коллбэке мы вызываем send у ядра. Заказчик вызывает recv, а исполнитель send. $name_ready->recv прекращает работу ядра, пока мы не получим $name - указав выполнение условия послав $name_ready->send. При помощи send и recv можно отправлять и получать данные:
AnyEvent::HTTP представляет собой не блокирующий HTTP/HTTPS клиент. Он поддерживает GET, POST и HEAD запросы, куки и многое другое, и все это на низком уровне.
Как и в примере I/O watcher, в коллбэке мы вызываем send, тем самым завершая работу.
Вот мы и добрались до паука. На http://maps.yandex.ru/ есть возможность найти не только улицу или город, но и любую организацию. После ввода данных в строке запроса, через JS отправляется запрос на урлу:
http://maps.yandex.ru/?text=что_ищем&where=где_ищем
в where можно указать не только улицу, но и город. Например, по запросу
http://maps.yandex.ru/?text=аптека&where=ростов-на-дону
мы получим все аптеки в ростове-на-дону. Данные от сервера приходят в JSON формате - это очень удобно. Но вот незадача, он присылает только по 10 организаций на странице, и делает постраничную навигацию. Тут надо использовать AnyEvent::HTTP. Посмотрев на урлу каждой страницы, можно заметить, что в качестве дополнительного параметра, к выше описанному запросу, просто добавляется skip:
http://maps.yandex.ru/?text=аптека&where=ростов-на-дону&skip=число
этот параметр указывает на то, сколько данных надо пропустить (на второй странице skip=10 и т.д.). Итак, получается, что нам надо сделать число GET запросов, равное кол-ву страниц. Но как нам узнать сколько всего данных найдено, чтобы узнать сколько страниц? Напомню - приходит JSON. Разработчики yandex предусмотрели нашу проблему, и в ответе на каждый запрос они присылают общее кол-во найденных записей. Напомню, что надо руками делать url escape.
Порядок работы:
AnyEvent::HTTP пока не поддерживает keep-alive соединения, поэтому он откроет $page_cnt отдельных соединений. По умолчанию он может сделать к одному хосту только 4 соединения. Поэтому если нам надо сделать 5 запросов, то сразу он сделает 4, а пятое поставит в очередь. И как только одно из соединений закроется, он тут же откроет следующее. Для завершения работы паука нам нужно считать кол-во обработанных ответов, оно равно кол-ву отправленных запросов.
Статья взята тут: http://likhatskiy.livejournal.com/1966.html
Еще почерпнуть немного информации про AnyEvent можно здесь: http://friends.rambler.ru/dsimonov31/friends/64939153/tags/anyevent
Не много о главном - или что такое AnyEvent
AnyEvent — это прежде всего фрэймворк для событийно-ориентированного программирования (event loops). Особенностью подобных фрэймворков является одиночное применение, т.е если ты используешь один из них, то нет возможности использовать какой-либо другой в том же коде. AnyEvent другой. Он является некой абстракцией событийных машин, как DBI является абстракцией многих апи баз данных.
Событийно-ориентированное программирование
Событийный подход к программированию включает использование объектов, способных реагировать на события, происходящие в системе. Событийный подход используется при разработке как самостоятельных программ, так и операционных систем, например, Microsoft Windows или OS/2 Presentation Manager. Вот пример ввода данных через STDIN:
$| = 1; print "enter your name> "; my $name = <STDIN>;
Через событийную машину это можно реализовать при помощи коллбэков (callback) - функций, которые срабатывают при появлении события:
use AnyEvent; $| = 1; print "enter your name> "; my $name; my $wait_for_input = AnyEvent->io ( fh => \*STDIN, # за каким дескриптором смотрим poll => "r", # какое событие ожидать (r - чтение, w - запись) cb => sub { # коллбэк $name = <STDIN>; # читаем } ); # делаем что нибудь еще
Как правило, в реальных задачах оказывается недопустимым длительное выполнение обработчика события, поскольку при этом программа не может реагировать на другие события. Было бы хорошо выполнить какие-либо действия, пока мы ожидаем ввода данных. Ожидание в первом примере блокирует процесс до получение данных. Во втором примере мы просто зарегистрировали событие на чтение, которое не блокирует процесс. Просто когда произойдет ввод данных, вызовется коллбэк, который и прочитает данные.
Метод AnyEvent->io создает I/O "watcher", я его называю "смотрящим". Он так называется потому, что он слушает файловый (либо какой-либо другой) дескриптор, на факт происхождения нужного нам события.
Общий принцип - Condition Variables
Вернемся к I/O "watcher. Этот пример не совсем рабочий. Наш коллбэк не вызовется - мы должны запустить событийную машину. Событийная машина может блокироваться, если ей нечего делать или нет больше событий. Например, если использовать POE и не вызвать stop - то он будет висеть пока не сработает таймаут, а это порядка 10 сек. Мало кому это понравится.
В AnyEvent этого можно избежать используя "условные переменные" (condition variables). Это можно назвать синхронизатором или ядром AnyEvent. Condition variables имеет две стороны: заказчик (ждет выполнения условия) и исполнитель (их исполняет).
В нашем примере, в качестве исполнителя выступает коллбэк. Но у нас нет заказчика, надо это исправить:
use AnyEvent; $| = 1; print "enter your name> "; my $name; my $name_ready = AnyEvent->condvar; my $wait_for_input = AnyEvent->io ( fh => \*STDIN, poll => "r", cb => sub { $name = <STDIN>; $name_ready->send; } ); # делаем что нидь еще # теперь ждем пока придут данные на вход: $name_ready->recv; undef $wait_for_input; # watcher нам больше не нужен print "your name is $name\n";
Мы создаем AnyEvent condvar вызовом метода AnyEvent->condvar, это и есть наше ядро. Потом создаем watcher, но в коллбэке мы вызываем send у ядра. Заказчик вызывает recv, а исполнитель send. $name_ready->recv прекращает работу ядра, пока мы не получим $name - указав выполнение условия послав $name_ready->send. При помощи send и recv можно отправлять и получать данные:
use AnyEvent; $| = 1; print "enter your name> "; my $name_ready = AnyEvent->condvar; my $wait_for_input = AnyEvent->io ( fh => \*STDIN, poll => "r", cb => sub { $name_ready->send (scalar <STDIN>) } ); # делаем что нидь еще # теперь ждем и подставляем данные на входе my $name = $name_ready->recv; undef $wait_for_input; # watcher нам больше не нужен print "your name is $name\n";
Получение данных при помощи AnyEvent::HTTP
AnyEvent::HTTP представляет собой не блокирующий HTTP/HTTPS клиент. Он поддерживает GET, POST и HEAD запросы, куки и многое другое, и все это на низком уровне.
- http_request $method => $url, key => value..., $cb->($data, $headers)
делает HTTP запрос методом $method (например GET, POST). Урла ($url) должна быть абсолютной. Дополнительные параметры key => value не обязательны.
Вот некоторые из них:
- headers => $hashref - заголовки запроса;
- timeout => $seconds - таймаут соединения;
- body => $string - тело запроса;
- cookie_jar => $hash_ref - включение поддержки куков, $hash_ref должна быть ссылкой на хеш, который будет автоматически обновляться.
- http_head $url, key => value..., $cb->($data, $headers)
делает HTTP-HEAD запрос, описание параметров смотри в http_request
- http_post $url, $body, key => value..., $cb->($data, $headers)
делает HTTP-POST запрос, описание параметров смотри в http_request
- http_get $url, key => value..., $cb->($data, $headers)
делает HTTP-GET запрос, описание параметров смотри в http_request
Внимание: AnyEvent::HTTP пока не делает url escape автоматически, приходится делать это руками
use AnyEvent::HTTP;
my $cv = AnyEvent->condvar;
http_get "http://www.someuri.com/",
cookie_jar => {},
headers => {},
sub {
my ($body, $hdr) = @_;
if ($hdr->{Status} =~ /^2/) {
... everything should be ok
} else {
print "error, $hdr->{Status} $hdr->{Reason}\n";
}
$cv->send;
};
$cv->recv;
Как и в примере I/O watcher, в коллбэке мы вызываем send, тем самым завершая работу.
И наконец паук - AnyEvent yandex spider
Вот мы и добрались до паука. На http://maps.yandex.ru/ есть возможность найти не только улицу или город, но и любую организацию. После ввода данных в строке запроса, через JS отправляется запрос на урлу:
http://maps.yandex.ru/?text=что_ищем&where=где_ищем
в where можно указать не только улицу, но и город. Например, по запросу
http://maps.yandex.ru/?text=аптека&where=ростов-на-дону
мы получим все аптеки в ростове-на-дону. Данные от сервера приходят в JSON формате - это очень удобно. Но вот незадача, он присылает только по 10 организаций на странице, и делает постраничную навигацию. Тут надо использовать AnyEvent::HTTP. Посмотрев на урлу каждой страницы, можно заметить, что в качестве дополнительного параметра, к выше описанному запросу, просто добавляется skip:
http://maps.yandex.ru/?text=аптека&where=ростов-на-дону&skip=число
этот параметр указывает на то, сколько данных надо пропустить (на второй странице skip=10 и т.д.). Итак, получается, что нам надо сделать число GET запросов, равное кол-ву страниц. Но как нам узнать сколько всего данных найдено, чтобы узнать сколько страниц? Напомню - приходит JSON. Разработчики yandex предусмотрели нашу проблему, и в ответе на каждый запрос они присылают общее кол-во найденных записей. Напомню, что надо руками делать url escape.
Порядок работы:
- первым запросом получить первую партию данных и кол-во записей
- потом сделать $page_cnt-1 запросов для получения остальных данных
AnyEvent::HTTP пока не поддерживает keep-alive соединения, поэтому он откроет $page_cnt отдельных соединений. По умолчанию он может сделать к одному хосту только 4 соединения. Поэтому если нам надо сделать 5 запросов, то сразу он сделает 4, а пятое поставит в очередь. И как только одно из соединений закроется, он тут же откроет следующее. Для завершения работы паука нам нужно считать кол-во обработанных ответов, оно равно кол-ву отправленных запросов.
use strict; use AnyEvent::HTTP; use URI::Escape; use JSON::XS; use utf8; my $conf = { 'search' => { 'str' => 'аптека', }, 'where' => { 'str' => 'ростов-на-дону', }, }; $conf->{$_}->{'encode'} = url_encode($conf->{$_}->{'str'}) for qw/search where/; my $items = []; my $cv = AnyEvent->condvar; http_get gen_uri(), get_params(), sub { my ($body, $hr) = @_; $cv->send("yandex don`t enable!") unless $hr->{'Status'} == 200; my $pages_cnt = get_data($body); $cv->send("result is empty!") unless $pages_cnt; my $resp_cnt = 0; for (1..$pages_cnt) { http_get gen_uri($_*10), get_params(), sub { my ($body, $hr) = @_; $resp_cnt++; my $temp = get_data($body); $cv->send("OK") if $resp_cnt >= $pages_cnt; }; } }; print $cv->recv; sub url_encode {URI::Escape::uri_escape_utf8(shift)} sub gen_uri { sprintf("http://maps.yandex.ru/?text=%s&where=%s&skip=%d", $conf->{'search'}->{'encode'}, $conf->{'where'}->{'encode'}, shift || 0); } sub get_data { my $json_str = (shift =~ /<\!\[CDATA\[(.*)\]\]><\/script>/)[0]; $json_str =~ s/NaN/"NaN"/g; my $json = JSON::XS->new->decode ($json_str); push @$items, @{$json->{'service'}->{'data'}->{'businesses'}->{'items'} || []}; int(($json->{'service'}->{'data'}->{'businesses'}->{'length'} || 0)/10); } my $user_agents = [ 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.4.154.25 Safari/525.19', 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; YPC 3.0.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)', 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)', 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.1) Gecko/20061204 Firefox/2.0.0.1', 'Mozilla/5.0 (Windows; U; Windows NT 6.0; ru; rv:1.9.0.3) Gecko/2008092417 Firefox/3.0.3', 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1) Gecko/20090624 Firefox/3.5', 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50215) Netscape/8.0.1', 'Opera/9.50 (Windows NT 5.1; U; ru)', 'Opera/9.0 (Windows NT 5.1; U; en)', 'Opera/9.60 (J2ME/MIDP; Opera Mini/4.2.13337/724; U; ru) Presto/2.2.0', 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/521.25 (KHTML, like Gecko) Safari/521.24', ]; sub get_params { %{{ cookie_jar => {}, headers => { 'Host' => 'maps.yandex.ru', 'Referer' => 'http://maps.yandex.ru/', 'User-Agent' => $user_agents->[int rand @$user_agents], }, }}; }
Статья взята тут: http://likhatskiy.livejournal.com/1966.html
Еще почерпнуть немного информации про AnyEvent можно здесь: http://friends.rambler.ru/dsimonov31/friends/64939153/tags/anyevent