Многопоточный парсер ТИЦ на Perl

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

#!/usr/bin/perl -w
 
# Многопоточный парсер значения ТИЦ
# by ParserPro (http://parse.com.ua | http://parserpro.com)
# 12/01/2008
 
use strict;
use threads;
use threads::shared;
use LWP::Simple;
 
use constant BASE_URL => 'http://bar-navig.yandex.ru/u?ver=2&show=32&url=';   # базовый адрес откуда будем получать значение ТИЦа
use constant MAX_THREADS => 20;                                               # максимальное количество потоков. Можете поставить и больше ;)
 
my $thread_count : shared;                                                    # счетчик потоков
$thread_count=1;
 
# По умолчанию скрипт принимает 2 аргумента:
# - файл с урлами
# - имя файла, в который будут записаны значения ТИЦа
if(@ARGV != 2) {
    die "Usage: tyc.pl <urls_file> <output_file>\n";
}
 
open(URLS,'<'.$ARGV[0]) or die "Не могу открыть файл: $! \n";
my @lines;                  # массив для урлов
while (<URLS>) {
    chop;                   # убрали переводы строки    
    s/^\s+//;               # убрали предшествующие пробелм
    s/\s+$//;               # убрали постшествующие пробелы
    next unless length;     # если после этого длина = 0 перейти к следующей строке
    push @lines, $_;        # запихиваем урлы в большой массив
}
 
open(OUTPUT,'>>'.$ARGV[1]) or die "Не могу создать файл: $!\n";
my @proc;       # массив потоков
my $s = times;  # секундомер
foreach (@lines) {
    while ($thread_count > MAX_THREADS) {}      # пока количество потоков больше MAX_THREADS ждем
    push @proc, threads->create(\&getTYC,$_);   # запускаем поток
    $proc[$#proc]->join();                      # приатачиваем последний запущенный поток к главной программе
}
$s=(times-$s)*60;                               
print "Elapsed time: $s seconds\n";             
 
sub getTYC($) {
    ++$thread_count;     # запустили поток
    my $tycUrl = shift;
    my $url = BASE_URL.$tycUrl;
    my $content = get $url;
    if(defined $content) {
        if ($content =~ /value\=\"(.*?)\"/) {
            if ($1 eq '') {
                print "$tycUrl;!\n";
                print OUTPUT "$tycUrl;!\n";
            } else {
                print "$tycUrl;$1\n";
                print OUTPUT "$tycUrl;$1\n";
            }            
        } else {
            print "$tycUrl;!!\n";
            print OUTPUT "$tycUrl;!!\n";
        }
    } else {
        print "$tycUrl;!!!\n";
        print OUTPUT "$tycUrl;!!!\n";        
    }
    --$thread_count;    # завершили поток
}

У данного кода есть несколько преимуществ:

  • многопоточность
  • простота

и ряд недостатков:

  • использование очень медленной LWP (и как ее подвида LWP::Simple)
  • невозможность работы через прокси (добавить это легко, но мне было лень :) )
  • никакого контроля за адресами и их валидностью - тупо, что получили, то Яндексу и кормим (это добавить еще легче, но мне было еще больше в лень)
  • использование “дурного” метода обработки завершения потока.
    Я хотел использовать, что-то другое, но для него или не подходил мой Перл или работало оно явно не очень хорошо (как в случае с Threads::Queue).

Также скрипт не показал себя сильно производительным. На моем компе проверку 100 URL он произвел за 220 секунду, то есть по 2 секунды на адрес - ОЧЕНЬ МЕДЛЕННО! Также удивил тот факт, что такой примитивный скрипт отгрыз около 5 метров оперативной памяти (наверное ты это имел ввиду, SHAman, когда мы с тобой говорили про потоки). Притом при создании нового потока объем используемой памяти увеличивался. Со временем память высвобождалась. Но само поведение меня потоков меня напрягало.

Так что пробуйте скрипт у себя. Говорите о результатах, делитесь опытом, так сказать. А для себя я еще раз убедился, что PERL POE будет оптимальным решением для моих задач!

Удачи!

34 комментариев

  1. SHAman:

    Говорил же, что при создании нового потока, делается fork!:) Вот память и кушается. Под отдельный Perl. Запустил 10 потоков - считай 10 перлов запустил. Мда. Видимо, многопоточность - не конек Perl…

    Посмотрим что будет в Perl6.

  2. admin:

    Не люблю я фраз на подобии “я же тебе говорил”. Ты настолько сильно аргументировал … ;)))
    Ну ладно.
    Самое забавное заключается в том, что по тестам даже PERL POE не блестнул по скорости. Очень не блестнул. И это меня опечалило. Такое впечатление, что единственный выход это неблокирующие сокеты. Но видел я это уродство (в плане кода) в пхп и не сказал, что я хотел бы это использовать :)

  3. SHAman:

    Ты меня сбил с толку своей уверенностью в том, что use threads дают реальные триды, а не форки. Да, приходится констатировать, что скриптовые языки плохо подходят для многопоточной работы.

    Может, стоит попробовать Java, как тебе кто-то предлагал в одном из предыдущих постов? Правда, не знаю, насколько быстро там получится парсить. Зато триды там красивые и удобные. Да и работают быстрее.

  4. admin:

    А я, имхо, нигде не писал, что threads делают fork’и своих процессов ;)
    Я просто написал, что меня не удовлетворяет скорость работы и количество пожираемой памяти.
    Может, в принципе, различий и не много, но после форма у процесса появляется свой pid, а после использования threads - нет. Да какая короче разница. Не подходит и все :)

    А насчет Java - ну уж извините, но переписывать тучу кода ради того, что получить красивые триды я не буду. Я лучше уже погеморюсь и сделаю мега либу на неблокирующих сокетах :)

  5. SHAman:

    Ну вперед. А потом ее можно дать под скачку желающим. Думаю, будет пользоваться популярностью. Многопоточность - дело хорошее…

  6. admin:

    Желающим под скачку лучше вообще ничего не давать. Потому что как показывает практика, где появляется шара, там отключается мозг :)
    Буду куски кода показывать, но целостную программулину пусть все сами собирают.

  7. Алексей:

    Спасибо за кусочки мазайки. :)
    Блин, знать бы Перл еще. )

  8. admin:

    Я уже на этом блоге давал решение на PHP и что самое поразительное - она даже быстрее работает чем многопоточное на Перл. :)

  9. Серг:

    Админ спасиб за данный скрипт, щас еще на блоге поищу на рнр

  10. вика:

    какой -то мутный скрип, я в нем никак разобраться не могу.

  11. admin:

    Как по мне, так он вообще крайне примитивный.

  12. Demiurg:

    Я что-то не догоняю - что за время этот код выводит? Работает он явно быстрее чем пишет.

  13. Demiurg:

    Вопрос снят - разобрался.

  14. Demiurg:

    Рекомендую chop заменить на chomp

  15. Demiurg:

    Очень интересно понаблюдать за переменной $thread_count - она у меня не бывает больше 2. Очень большое подозрение, что процедура getTYC блокирует цикл создания тредов. Немного поменял программу под себя - блокировка стала заметна невооруженным взглядом (при достаточно большом времени выполнения процедуры getTYC). Не получаются параллельные треды, увы.

  16. Demiurg:

    Опять разобрался - это гадкий join убивает многопоточность. Без него все просто супер - главное дождаться пока детки отработают. В вашем коде это можно сделать вставив вот это после цикла создания тредов:
    while ($thread_count > 1) {};

    А еще можно сходить вот сюда:
    http://nopox.wordpress.com/2007/11/06/multithreading-in-perl-part-2/
    и узнать много полезного.

  17. Demiurg:

    И еще - данное приложение течет, и весьма сильно. А вот если созданные треды не пихать в массив, то ситуация становится лучше. Массив этот ни для чего не нужен на самом деле. Всего тредов (у меня) может быть 255. Т.к. дохлые треды из массива никто не чистит, то система на 256-ом валится по сегментейшену.

  18. вика:

    все самое простое всегда сложное. но вроде уже все нормально, кажется до меня дошло:)

  19. вика:

    теперь я поняла, что все гениально просто. сама себя запутала:) вот лошара:)

  20. admin:

    Опять разобрался - это гадкий join убивает многопоточность. Без него все просто супер - главное дождаться пока детки отработают. В вашем коде это можно сделать вставив вот это после цикла создания тредов:
    while ($thread_count > 1) {};

    А еще можно сходить вот сюда:
    http://nopox.wordpress.com/2007/11/06/multithreading-in-perl-part-2/
    и узнать много полезного.

    И еще - данное приложение течет, и весьма сильно. А вот если созданные треды не пихать в массив, то ситуация становится лучше. Массив этот ни для чего не нужен на самом деле. Всего тредов (у меня) может быть 255. Т.к. дохлые треды из массива никто не чистит, то система на 256-ом валится по сегментейшену.

    Хм. А если нужно проверить 10к ссылок, то он будет валиться на каждой 256. Зачем тогда такой скрипт нужен? А по поводу того течет он или нет - это не особо важно, так как приложение не особо критичное, да и сожрать особо много оно даже в крайнем случае не сможет.

  21. Demiurg:

    Я что пытался до вас донести - ваш код не многопоточный, т.к. после вызова join программа блокируется до конца выполнения треда.
    И еще - попробуйте скормить ему побольше ссылок (хотя бы 300). Он будет валиться не на каждой 256-ой, а вообще на 256-ой. Использованные треды надо освобождать. А 255 их может быть ОДНОВРЕМЕННО - вы использованные не вычищаете, следовательно именно ваш скрипт будет падать. И память он прилично так откушивает.
    На основе вашего кода сваял себе граббер/парсер. Могу поделиться. Там нет массива с тредами. У меня тред живет только пока он нужен. Так что, если интересно, могу исходник с комментариями скинуть. Парсит сайт olx.com.

  22. admin:

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

    Удачи!

  23. создающий сайты бесплатно бюджетникам:

    Не получаются параллельные треды.

  24. Watman:

    Demiurg a можно выслать мне ? не смог сделать без утечек памяти ;( или подсказать где можно посомтреть на работающий пример
    почта watmans[AT]gmail.com

  25. Гвоздь:

    >>>>>
    На моем компе проверку 100 URL он произвел за 220 секунду, то есть по 2 секунды на адрес - ОЧЕНЬ МЕДЛЕННО!
    >>>>>
    а почему так медленно-то?
    я не так давно писал на PHP многопоточный парсер ТИЦ и ЯК
    работал он в 100 потоков
    на моей машине эта куча отрабатывала менее чем за 7 секунд, гарантировано.
    на хостинге за 0.1-0.2 секунды в среднем, а то и сотые секунды
    из-за большого выставленного таймаута, один поток мог затормозить процесс до 3секунд на 100 URL-ов
    что замечательно, что этот URL для чекинга не банится яндексом, по количеству запросов.

  26. admin:

    Выше в комментах есть подсказки на решение.

  27. admin:

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

  28. Гвоздь:

    >>>
    У пхп нет нормального механизма реализации многопоточности, кроме всяких хитростей наподобии неблокирующих сокетов и т.п.
    >>>
    неблокирующие сокеты и multiCURL
    это только для запросов. с их же помощью можно создать новый процесс скрипта, что является продолжением темы многопоточности
    это из того, что имеет распространение.
    вроде есть еще пара методов, но они малораспространены и для их работы нужно поднимать VPS, потому что они являются каким-то довольно нестандартным расиширением. по крайней мере бинарников под венды в архиве PHP нету

    для дела парсинга этих эмуляций многопоточности вполне достаточно, ИМХО

  29. admin:

    У каждого процесса есть свой pid (process id), а у потока - нет. Поэтому любое приложение порождающее процессы называется многопроцесовым, а не многопоточным. И это даже близко не эмуляция многопоточности.

  30. nagual:

    А что РОЕ непомогает? и без всяких тредов …

  31. admin:

    Помогает. Но каждую задачу можно решить как минимум двумя путями. Вы назвали второй, я - первый ;)

  32. nagual:

    Треды пока что нестабильны … и на многих машинах перл собран без них так что и поэкспериментировать неполучится …

  33. admin:

    Согласен. Но это еще не повод, чтобы с ними не эксперементировать.

  34. nagual:

    Как заставить перл освобождать память занятую отработавшими тредами?

Оставить комментарий