Pull to refresh

Удобное скачивание с сайта Books.ru или пристраиваем к делу WWW::Mechanize

Reading time 5 min
Views 12K
Картинка для привлечения внимания
Если книжка есть — это хорошо,
А когда наоборот — плохо
Вместо эпиграфа

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

Шаг 1. Подключаем необходимые модули.
Нам понадобится
use WWW::Mechanize;
use HTTP::Request::Common;
use LWP;
use LWP::UserAgent;

Сам модуль и несколько служебных модулей, от которых он зависит. Если вы, также как и я используете Ubuntu то скачивать WWW::Mechanize с CPAN противопоказано, а вместо этого лучше сказать
sudo apt-get install libwww-mechanize-perl

Шаг 2. Создаем объект механизации и забираем параметры скрипта из командной строки: логин и пароль.
my $mech = WWW::Mechanize->new();
$booklog =  $ARGV[0];
$bookpsw =  $ARGV[1];

Шаг 3. Логинимся на сайте
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(40, 50), 'y' => rand_from_to(1, 20), 
    'token' => ''
  ]);

Обращаю внимание строки 2 и 3. В оригинальном коде, эти куки формируются при помощи JavaScript, но только ради вычисления двух параметров подключать JavaScript не рационально и проще переписать его на перле.
Шаг 4. Получаем общий список наших заказов и создаем по нему итератор:
$resp = $mech->get('http://www.books.ru/member/orders/');
my @order_list = mkGunz($resp->content) =~ /\<a\shref=\"http:\/\/www\.books\.ru\/order.php\?order\=(\d+)\"\>/gi;
foreach my $order_id (@order_list) {...}

Обращаю внимание на функцию mkGunz, которая автоматически разжимает данные если сервер их запаковал при помощи gzip.
Шаг 5. Теперь нам надо из страницы извлечь авторов книги и ее название. Так как мы используем модуль HTML::TokeParser для разбора страницы, то проще всего в потоковом режиме выхватывать нужные нам данные ориентируясь по URL.
  my $fname =  '';
  my $authors = '';
  while (my $token = $stream->get_token) 
  {
    if ($token->[0] eq 'S' && $token->[1] eq 'a') 
    {
      my $href = $token->[2]{'href'};
      $authors .= $stream->get_trimmed_text('/a').',' if ($href =~ /\/author\//);
      if ($href =~ /show\=1/)
      {
        $fname = $stream->get_trimmed_text('/a');
        $fname =~ s/\(файл\sPDF\)//gi;
      }
      if ($href =~ /download\/\?file_type\=pdf/)
      {
        chop($authors);
        $fname = trim($authors.','.$fname);
        $fname =~ tr/\//_/;
        $fname .= '.pdf';
        ....
       }
     }

Шаг 6. Получить и сохранить PDF. Тут сразу несколько интересных моментов: если не сделать clone, то скачается только одна книжка, по-видимому, баг на сайте books.ru. Нельзя для сохранения файлов с русскими буквами использовать модуль IO::File, баг в модуле для версии перла v5.14.2. ну и вызов binmode, чтоб не поломать PDF файлы.
        my $gbm = $mech->clone();
        $resp = $gbm->get($href);
        $resp = $gbm->submit_form(with_fields => {'agreed' => 'Y', 'go' => 1});
        my $pdfFile = $resp->content;
        $pdfFile = mkGunz($resp->content) unless ($resp->content =~ /^\%PDF/);
        print "Saving ".$fname." as ".length($pdfFile)." bytes.\n" ;
        open(my $fh, ">", $fname);
        if (defined $fh) 
        {
          binmode($fh);
          print $fh $pdfFile; 
          close($fh);
        }


Ну и, наконец, все в сборе.
#!/usr/bin/perl
use WWW::Mechanize;
use HTTP::Request::Common;
use LWP;
use LWP::UserAgent;
use URI::Escape;
use HTML::TokeParser;
use DateTime;
use Compress::Raw::Zlib;
use Encode qw(decode encode);
use warnings;

sub trim($);
my $mech = WWW::Mechanize->new();
$booklog =  $ARGV[0];
$bookpsw =  $ARGV[1];
#die "Usage: books.su.pl <login> <password> \n" if (scalar @ARGV < 2);


$mail = $booklog;
$password = $bookpsw;

$mech->agent_alias("Linux Mozilla");
#$mech->proxy('https', 'http://127.0.0.1:8888/');
#$mech->proxy('http', 'http://127.0.0.1:8888/');
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');
#print mkGunz($resp->content)."\n";
$resp = $mech->post('http://www.books.ru/member/login.php',[
    'login'  => $mail,
    'password' => $password,
    'go' => 'login',
    'x' => rand_from_to(40, 50), 'y' => rand_from_to(1, 20), 
    'token' => ''
  ]);
#print mkGunz($resp->content)."\n";
$resp = $mech->get('http://www.books.ru/member/orders/');
my @order_list = mkGunz($resp->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);
  my $hcont = mkGunz($resp->content);
  my $stream = HTML::TokeParser->new(\$hcont);
  $stream->empty_element_tags(1);
  my $fname =  '';
  my $authors = '';
  while (my $token = $stream->get_token) 
  {
    if ($authors eq '' && $fname ne "" && $token->[0] eq 'S' && $token->[1] eq 'br') 
    {
      $authors .= cnv($stream->get_trimmed_text('/p')).','; 
    }
    if ($token->[0] eq 'S' && $token->[1] eq 'a') 
    {
      my $href = $token->[2]{'href'};
      
      if ($href =~ /show\=1/)
      {
        $fname = cnv($stream->get_trimmed_text('/a'));
        $fname =~ s/\(файл\sPDF\)//gi;
      }
      if ($href =~ /download\/\?file_type\=pdf/)
      {
        chop($authors);
        $fname = trim($authors.','.$fname);
        $fname =~ tr/\//_/;
        $fname .= '.pdf';
        my $gbm = $mech->clone();
        $resp = $gbm->get($href);
        $resp = $gbm->submit_form(with_fields => {'agreed' => 'Y', 'go' => 1});
        my $pdfFile = $resp->content;
        $pdfFile = mkGunz($resp->content) unless ($resp->content =~ /^\%PDF/);
        print "Saving ".$fname." as ".length($pdfFile)." bytes.\n" ;
        open(my $fh, ">", $fname);
        if (defined $fh) 
        {
          binmode($fh);
          print $fh $pdfFile; 
          close($fh);
        }
        else
        { 
          die "Unable to open:".$fname."\n";
        }
        $authors = '';
        $fname =  '';
      }
    }
  }
}

sub cnv {return shift;}#encode('cp1251', decode('UTF-8', shift));}

sub rand_from_to
{
  my($from, $to) = @_;
  return int(rand($to - $from)) + $from;
}

sub mkGunz
{
  my ($ind) = @_;
  return $ind if($ind =~ /html/);
  my $gun = new Compress::Raw::Zlib::Inflate(WindowBits => WANT_GZIP);
  {
    my $out;
    my $status = $gun->inflate($ind, $out);
    if ($status == Z_OK || $status == Z_STREAM_END)
    {
        return $out;
    }
    else
    {
        die $status.":".$ind;
    }
  };
}

sub trim($)
{
	my $string = shift;
	$string =~ s/^\s+//;
	$string =~ s/\s+$//;
	return $string;
}




Примечание для любителей Windows:
Cкорее всего, надо изменить строку $fname =~ tr/\//_/; на $fname =~ tr/\/\:\*\?\\/_/; так как в NTFS больше запрещенных символов чем в ext4 и повозиться с кодировкой, для чего предусмотрена функция cnv.

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

Legal disclaimer: Так как по лицензионному соглашению после скачивания переименовывать файлы запрещается, то скачивать их надо сразу под правильным и удобным именем, что данный скрипт и делает!
Tags:
Hubs:
+9
Comments 11
Comments Comments 11

Articles