S3.Blog

16 Апреля 2024
A A A   RSS-лента
"Я знаю, что ничего не знаю, но многие не знают и этого". Сократ [?].

Perl: Мини экскурс в AnyEvent - пишем паука

Дата последнего изменения: 28 Мая 2010
Метки статьи: Документация, Perl
Мы рассмотрим AnyEvent на примере yandex паука, который собирает организации с maps.yandex.ru по поисковому слову (например "аптека"). Но перед этим разберемся с тем, что нам понадобится.
 

Не много о главном - или что такое 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_request, получает коллбэк, в котором мы можем манипулировать полученными данными. В первый параметр он получает тело ответа (или undef в случае ошибки), во второй заголовки ответа.

  • 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.
Порядок работы:
  1. первым запросом получить первую партию данных и кол-во записей
  2. потом сделать $page_cnt-1 запросов для получения остальных данных
Если мы вызовем http_get n раз, то он откроет n соединений с хостом, сделает n запросов и будет ждать от них ответа. Как только придет один из ответов, тут же сработает коллбэк, тем самым достигается не блокирующее соединение.

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



Похожие материалы:




 
  Имя *:   Решите пример *: =
 
Полужирный Курсив Подчеркнутый Перечеркнутый
 
Вставить изображение Сделать цитатой Вставить ссылку Вставить код

Вставить смайл
 
 

 



© S3.Blog: Если критикуешь, не предлагая решения проблемы, то ты становишься частью этой проблемы.