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

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

    Как все знают, недавно была акция с возможностью приобретения большого количества электронных книг на сайте 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: Так как по лицензионному соглашению после скачивания переименовывать файлы запрещается, то скачивать их надо сразу под правильным и удобным именем, что данный скрипт и делает!
    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 11

      0
      Имеет смысл как-то сохранять отдельно список числовых id, которые есть в конце имени оригинального файла.
      Позднее можно запилить его в userscript и с его помощью избежать повторных покупок одних и тех же позиций в будущем
      (потому что сам books.ru никак не даст знать, что «это вы уже купили на весенней распродаже»).
        0
        Большое спасибо за информацию об акции!
        Прикупил себе пяток хороших книг. Качаем!
          0
          Не могу понять, почему используется конструкция mkGunz($resp->content) вместо $resp->decoded_content
            0
            Таже самая проблема, что и с IO:File. Когда я делал бота для выигрыша гага сделок на алиэкспрессе наблюдал презанятный баг с пустым decoded_content и вполне валидным content. К сожалению, как обычно, надо было сделать чтоб заработало, стабильно и быстро, а потому тогда и была написана mkGunz, а исследование бага оставлено до лучших времен, которые так и не наступили…
            +1
            Рад, что мое поделие пошло в массы. И тем более рад, что оно кому-то не нравится, и этот кто-то делает что-то свое. Это и есть прогресс.
            Да здравствует open source!
              0
              Кстати у них сегодня такая же акция проходит books.ru/knigi-so-svobodnoi-tsenoi-18-let-3752878/ — книги по свободной цене.
                +1
                Да, в первом абзаце так и написано.
                  0
                  Посыпаю голову пеплом… Дейсвительно, не заметил.
                0
                Спасибо вам. Даже запустил старую виртуалку с Линуксом для такого дела.
                  0
                  Перекодировка имени файла для Windows:
                  sub cnv {return encode('cp1251', decode('utf-8', shift));}
                    0
                    Спасибо! Внес изменения в статью.

                  Only users with full accounts can post comments. Log in, please.