Багатопотоковий проксі на Perl, або як купувати на books.ru зручно

Picture from web-site blogs.perl.org
Поспілкувалися ми якось з користувачем 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: Якщо буде таке бажання суспільства, то можу розмістити модифіковану версію на своєму сервері, хоча я б сам ні за які зручності не став користуватися неконтрольованим мною проксі.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.