
Пообщались мы как-то с пользователем icoz по поводу покупок в books.ru и решения, как не купить одну и туже книжку случайно дважды. Диалог получился не очень, а вот решение вышло удобное и показывающее, какие книжки куплены, а какие — нет. Причем, никаких параметров для скрипта не надо. Скрипт получит логин и пароль для взаимодействия с сайтом сам. Если вы купили что-то, то достаточно выйти с сайта books.ru и зайти обратно, чтоб скрипт подхватил купленные Вами книги.

Что нам понадобится?
Машина с установленным перлом и операционной системой Ubuntu (но подойдет любой Linux) под Windows есть проблемы, из-за используемого fork , но их можно победить установив модуль через CPAN with force. Тесты он все равно не пройдет, но нужная нам часть заработает.
Шаг 1: Устанавливаем необходимые библиотеки. Если они уже установлены, бояться не надо — второй раз они не установятся. Для любителей ActivePerl есть ppm.bat
sudo apt-get install liburi-encode-perl libwww-perl libhtml-tokeparser-simple-perl libwww-mechanize-perl libdatetime-perl libhttp-proxy-perl
Шаг 2: Создаем proxy server не забыв установить нужные фильтры:
my $proxy = HTTP::Proxy->new( engine => 'Threaded', port=>8888, max_keep_alive_requests => 0, host=>'127.0.0.1', timeout=>120); my $filter = HTTP::Proxy::BodyFilter::simple->new(\&alter_page); $proxy->push_filter(mime => 'text/html', response => HTTP::Proxy::BodyFilter::complete->new(), response => $filter); $proxy->push_filter(method => 'POST', path =>'/member/login.php', request => HTTP::Proxy::HeaderFilter::simple->new(sub { my $booklog = uri_decode($1) if $_[2]->decoded_content =~ /login\=(.*?)(?:\&|$)/; my $bookpsw = uri_decode($1) if $_[2]->decoded_content =~ /password\=(.*?)(?:\&|$)/; my @new_books = init_proxy($booklog, $bookpsw, @OWN_BOOKS); {lock (@OWN_BOOKS); @OWN_BOOKS = @new_books;} }));
И запускаем его:
$proxy->start;
Используется два фильтра:
- Для модификации страницы: alter_page
- Для захвата логина и пароля: наименованная функция
Важные моменты:
- engine => 'Threaded'. Если выбрать что-либо иное под Windows не заработает, а под Linux заработает с ошибками из-за fork().
- {lock ( @OWN_BOOKS); @OWN_BOOKS = @new_books;}. Если написать напрямую, то возможны проблемы, ибо perl не считает нужным заботиться о многопоточности и считает все переменные thread local.
Шаг 3: Пишем фильтр модификации:
sub alter_page { my ( $self, $dataref, $message, $protocol, $buffer ) = @_; return unless ${$dataref}; return unless $message->headers->content_type; foreach my $haveid (@OWN_BOOKS) { my $str = $haveid.'/?show=1"'; my $spat = quotemeta $str; my $repl = $str." style=\"text-decoration: line-through;\""; ${$dataref} =~ s/$spat/$repl/sg; } }
Тут все просто: создаем нужный шаблон для замены пробегаясь по всем имеющимся у нас книжкам, и отмечаем их как купленные путем зачеркивания!
Шаг 4: Пишем инициализацию пробегаясь по всем заказам и собирая все книжки которые вы уже купили.
foreach my $order_id (@order_list) { $resp = $mech->get('http://www.books.ru/order.php?order='.$order_id); parse_hrefs($resp->decoded_content, sub {push @OWN_BOOKS, $1 if ($_[0] =~ /(\d+)\/download\/\?file_type\=\w{3}/);}); } my %seen = (); my @ubooks = grep { ! $seen{$_}++ } @OWN_BOOKS;
В конце, убираем из списка все повторы, если таковые есть.
Шаг 5: Казалось бы все, должно работать, но не работает, ибо надо написать:
my @OWN_BOOKS :shared;
В противном случае глобальная переменная @OWN_BOOKS будет для каждого потока своя.
Шаг 6: Устанавливаем FoxyProxy или любое иное расширение, позволяющее использовать per site proxy, и наслаждаемся удобной работай с сайтом books.ru.
Как обычно, прилагаю полный скрипт
#!/usr/bin/perl use WWW::Mechanize; use HTTP::Request::Common; use LWP; use LWP::UserAgent; use HTML::TokeParser; use DateTime; use Encode qw(decode encode); use HTTP::Proxy; use HTTP::Proxy::BodyFilter::simple; use HTTP::Proxy::Engine::Threaded; use HTTP::Proxy::BodyFilter::complete; use HTTP::Proxy::HeaderFilter::simple; use URI::Encode qw(uri_encode uri_decode); use threads; use threads::shared; use warnings; # initialisation binmode STDOUT, ":utf8"; my @OWN_BOOKS; share(@OWN_BOOKS); @OWN_BOOKS = (); my $proxy = HTTP::Proxy->new( engine => 'Threaded', port=>8888, max_keep_alive_requests => 0, host=>'127.0.0.1', timeout=>120); $proxy->engine()->max_clients(100); my $filter = HTTP::Proxy::BodyFilter::simple->new(\&alter_page); $proxy->push_filter(mime => 'text/html', response => HTTP::Proxy::BodyFilter::complete->new(), response => $filter); # $proxy->push_filter(method => 'POST', path =>'/member/login.php', request => HTTP::Proxy::HeaderFilter::simple->new(sub { my $booklog = uri_decode($1) if $_[2]->decoded_content =~ /login\=(.*?)(?:\&|$)/; my $bookpsw = uri_decode($1) if $_[2]->decoded_content =~ /password\=(.*?)(?:\&|$)/; my @new_books = init_proxy($booklog, $bookpsw, @OWN_BOOKS); {lock (@OWN_BOOKS); @OWN_BOOKS = @new_books;} print "You already has ".scalar @OWN_BOOKS." books.\n"; })); # this is a MainLoop-like method $proxy->start; sub init_proxy { my $mail = shift; my $password = shift; my @OWN_BOOKS = @_; my $mech = WWW::Mechanize->new(); $mech->agent_alias("Linux Mozilla"); my $resp = $mech->get('http://www.books.ru/member/login.php'); $mech->cookie_jar->set_cookie(0, 'cookie_first_timestamp',DateTime->now->epoch, '/', 'www.books.ru'); $mech->cookie_jar->set_cookie(0, 'cookie_pages', '1', '/', 'www.books.ru'); $resp = $mech->post('http://www.books.ru/member/login.php',[ 'login' => $mail, 'password' => $password, 'go' => 'login', 'x' => rand_from_to(20, 55), 'y' => rand_from_to(10, 19), 'token' => '' ]); $resp = $mech->get('http://www.books.ru/member/orders/'); my @order_list = $resp->decoded_content =~ /\<a\shref=\"http:\/\/www\.books\.ru\/order.php\?order\=(\d+)\"\>/gi; foreach my $order_id (@order_list) { $resp = $mech->get('http://www.books.ru/order.php?order='.$order_id); parse_hrefs($resp->decoded_content, sub {push @OWN_BOOKS, $1 if ($_[0] =~ /(\d+)\/download\/\?file_type\=\w{3}/);}); } my %seen = (); my @ubooks = grep { ! $seen{$_}++ } @OWN_BOOKS; return @ubooks; } sub parse_hrefs { my ($data, $functor) = @_; my $stream = HTML::TokeParser->new(\$data); $stream->empty_element_tags(1); while (my $token = $stream->get_token) { if ($token->[0] eq 'S' && $token->[1] eq 'a') { my $href = $token->[2]{'href'}; $functor->($href); } } } sub alter_page { my ( $self, $dataref, $message, $protocol, $buffer ) = @_; return unless ${$dataref}; return unless $message->headers->content_type; #print scalar @OWN_BOOKS."!!!!!\n"; foreach my $haveid (@OWN_BOOKS) { my $str = $haveid.'/?show=1"'; my $spat = quotemeta $str; my $repl = $str." style=\"text-decoration: line-through;\""; ${$dataref} =~ s/$spat/$repl/sg; } } sub rand_from_to { my($from, $to) = @_; return int(rand($to - $from)) + $from; }
PS: Если будет таково желание общества, то могу разместить модифицированную версию на своем сервере, хотя я бы сам ни за какие удобства не стал пользоваться неконтролируемым мной прокси.
