Pull to refresh

Многопоточный прокси на Perl, или как покупать на books.ru удобно

Reading time 5 min
Views 6.6K
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: Если будет таково желание общества, то могу разместить модифицированную версию на своем сервере, хотя я бы сам ни за какие удобства не стал пользоваться неконтролируемым мной прокси.
Tags:
Hubs:
+9
Comments 6
Comments Comments 6

Articles