Финансы Сайт на котором знают все про финансы

Пишем простой асинхронный парсер

Многие разработчики типового говнософта, ориентированного на работу с вебом, зачастую используют потоки для того, чтобы получить выигрыш в скорости. Данный подход, конечно, обладает своими плюсами, но все же не является оптимальным, например, с точки зрения потребляемых ресурсов системы (особенно когда речь идет о потребителях, любящих ставить сразу “тыщу потоков”).
Альтернативным и общеизвестным способом ускорения работы софта является асинхронная модель, то есть модель, при которой все вызовы методов являются неблокирующими. В данной статье я рассмотрю простой пример, который будет использовать асинхронные веб-запросы.
В качестве примера будет написан парсер идентификаторов приложений с Android Market, который пригодится в готовящейся статье, посвященной добычи трафика с маркета. Для простоты будем использовать модуль AnyEvent, он упрощает реализацию асинхронной событийной модели. Итак, приступим.

Для начала прагмы, необходимые инклюды и переменные:

use strict;
use warnings;
use AnyEvent::HTTP;
use Fcntl qw/:flock/;

#Список категорий, которые скрипт будет обрабатывать
my @cat_list = qw/ARCADE BRAIN CARDS CASUAL GAME_WALLPAPER RACING SPORTS_GAMES GAME_WIDGETS/;
#Лимит страниц для каждой категории
my $page_limit = 15;
#Файл для сохранения результата
my $res_file = 'result1.txt';

#Autoflush
$| = 1;
#Для последующего определения времени работы скрипта
my $time = time;

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

#Перебираем категории в массиве
for my $cat_name(@cat_list)
{
    #Создаем переменную, необходимую для последующей блокировки дальнейшего выполнения скрипта
    #(чтобы выполнять запросы пачками, по 15)
    #http://search.cpan.org/~mlehmann/AnyEvent-6.14/lib/AnyEvent.pm#CONDITION_VARIABLES
    my $cv = AnyEvent->condvar;
    #Ставим в очередь гет-запросы
    for(my ($i, $j) = (0, 0); $i < $page_limit; $i++) { print $cat_name, ' - ', $i, $/; #Определяем параметры гет-запроса http_get ( 'https://market.android.com/details?id=apps_topselling_paid&cat='.$cat_name.'&start='.($i * 25).'&num=25', headers => 
            {
                'User-Agent' => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2)'
            },
            timeout => 5,
            #Колбэк, который вызывается после выполнения запроса
            sub 
            {
                my ($data, $headers) = @_;
                #Если страница не существует, то android market возвращает 404
                #Проверяем этот момент
                if($headers->{Status} =~ /^200/)
                {
                    #Сохраняем в файл идентификаторы приложений
                    open F, '>>', $res_file or warn $!;
                    flock F, LOCK_EX;
                    
                    print F (join $/, $data =~ /data-docid="(.+?)"/g), $/;
                    
                    flock F, LOCK_UN;
                    close F;
                }
                #Если все страницы были обработаны, 
                if(++$j == $page_limit)
                {
                    #Снимаем блокировку, чтобы поставить в очередь следующую пачку запросов
                    $cv->send;
                }
            }
        );
    }
    #Блокируем переменную, ждем пока все запросы будут обработаны
    #(как-бы ждем "сигнала", когда будет вызван метод ->send)
    $cv->recv;
}
#Выводим время работы скрипта
print 'Time elapsed: ', time - $time, $/;

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

use strict;
use warnings;
use threads;
use threads::shared;
use IO::Socket::SSL;
use Fcntl qw/:flock/;


my $thr_cnt = 15;
my @cat_list : shared = qw/ARCADE BRAIN CARDS CASUAL GAME_WALLPAPER RACING SPORTS_GAMES GAME_WIDGETS/;
my $page_limit = 15;
my $res_file = 'result2.txt';

$| = 1;
my @trl = ();
my $w_lock : shared;

my $time = time;

$trl[$_] = threads->create(\&main) for 0..$thr_cnt - 1;
$_->join for @trl;

print 'Time elapsed: ', time - $time, $/;

sub main
{
    while(1)
    {
        my $cat_name = shift @cat_list or last;
        
        for(my $i = 0; $i < $page_limit; $i++) { print $cat_name, ' - ', $i, $/; my $socket = IO::Socket::SSL->new
            (
                PeerAddr => 'market.android.com',
                PeerPort => 443,
                PeerProto => 'tcp', 
                TimeOut => 5
            );
            
            if($socket)
            {
                my $req =
                "GET /details?id=apps_topselling_paid&cat=$cat_name&start=".($i * 25)."&num=25 HTTP/1.0\r\n".
                "Host: market.android.com\r\n".
                "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2)\r\n".
                "Connection: Close\r\n\r\n";
                
                print $socket $req;
                
                my $data;
                while(read $socket, my $buffer, 128)
                {
                    $data .= $buffer;
                }
                
                close $socket;
                
                if($data =~ /200 OK/)
                {
                    lock $w_lock;
                    open F, '>>', $res_file or warn $!;
                    flock F, LOCK_EX;
                    
                    print F (join $/, $data =~ /data-docid="(.+?)"/g), $/;
                    
                    flock F, LOCK_UN;
                    close F;
                }
            }
        }
    }
}

Скорость работы скриптов примерно одинакова, и там и там идет одновременное выполнение 15 веб-запросов, однако, если обратить внимание на потребляемую память и нагрузку на процессор, то мы увидим явное преимущество у первого скрипта. У меня количество потребляемой памяти для асинхронного варианта составляло ~ 12 мегабайт, а в случае с потоками оно увеличилось до ~ 53 мегабайт (хотя прожорливость perl + pthreads под win* – известная печалька).
Но даже если абстрагироваться от языка программирования и использовать WinAPI для работы с потоками, то можно увидеть, что при большом количестве потоков (конечно, ведь чем больше потоков, тем круче и быстрее (с) дефолт логика) нагрузка на процессор вырастет несоизмеримо по сравнению с асинхронной моделью.

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

Скрипты из статьи: скачать