maya000

アルファポリス投稿小説自動ダウンローダ

Jun 28th, 2017
383
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Perl 14.03 KB | None | 0 0
  1. #!/usr/bin/perl
  2. #
  3. # アルファポリスの投稿小説を青空文庫形式にしてダウンロードする。
  4. # Copyright (c) 2017 ◆.nITGbUipI
  5. # license GPLv2
  6. #
  7. # Usage.
  8. # ./alpha-novel-dl.pl 目次url > 保存先ファイル名
  9. #
  10. # としてリダイレクトすれば青空文庫形式で保存される。
  11. #
  12. # 特徴
  13. # ・挿絵対応
  14. #     カレントディレクトリに保存さる。
  15. # ・ルビ対応
  16. # ・傍点対応
  17. # ・cp932対応
  18. #     utf8 な環境でしかテストしていない。
  19. #     一応WinではShift_JISで出力するようにはしている。
  20. # ・巡回機能
  21. #
  22. # 変更履歴
  23. # 2017年06月21日(水曜日) 16:55:46 JST
  24. # アルファポリスのサイトリニューアルに伴い、取得できるように書き換えた。
  25. # 2017年06月23日(金曜日) 12:12:01 JST
  26. # 追加されたエピソードだけ取得をする機能を追加。
  27. #    -u YY.MM.DD の形式で日付を指定すれば、その日付以降だけをダウンロードする。
  28. # 2017年06月24日(土曜日) 15:30:42 JST
  29. # 巡回機能を追加。
  30. # 巡回リストを指定すると自動で巡回してくれる。
  31. #
  32. #     alpha-novel-dl.pl -c check.lst -s ~/Desktop
  33. # と指定するとcheck.lstを読み込んで、~/Desktop/以下に個別Dirを作成して保存してくれる。
  34. # リスト形式はサンプルを参照。
  35. # 2017年06月25日(日曜日) 13:59:36 JST
  36. # 巡回機能で追加分だけダウンロードするようにした。
  37. # 初回何もなければ全部ダウンロードし、次回からはダウンロードした後に
  38. # 追加された分だけダウンロードする。
  39. # 2017年06月27日(火曜日) 15:00:10 JST
  40. # レンタルにされた部分はスルーするようにした。
  41. # おかげでやる気減退。
  42. #
  43.  
  44. use strict;
  45. use warnings;
  46. use LWP::UserAgent;
  47. use HTML::TreeBuilder;
  48. use utf8;
  49. use Encode;
  50. use File::Basename;
  51. use Time::Local 'timelocal';
  52. use Getopt::Long qw(:config posix_default no_ignore_case gnu_compat);
  53. use Cwd;
  54. use File::Spec;
  55.  
  56. my $url_prefix = "https://www.alphapolis.co.jp";
  57. my $user_agent = 'Mozilla/5.0 (X11; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0';
  58. my $separator = "▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼▲▼\n";
  59. my $kaipage = "[#改ページ]\n";
  60. my ($main_title, $author );
  61. my $chapter_title;
  62. my ($chklist, $savedir, $split_size, $update, $show_help );
  63. my $last_date;  #前回までの取得日
  64. my $base_path;  #保存先dir
  65. my $charcode = 'UTF-8';
  66.  
  67. if ($^O =~ m/MSWin32/) {
  68.     $charcode = "cp932";
  69. }
  70.  
  71. sub get_contents {
  72.     my $address = shift;
  73.     my $http = LWP::UserAgent->new;
  74.     $http->agent($user_agent);
  75.     my $res = $http->get($address);
  76.     my $content = $res->content;
  77.     return $content;
  78. }
  79.  
  80. # htmlパース
  81. sub html2tree {
  82.     my $item = shift;
  83.     my $tree = HTML::TreeBuilder->new;
  84.     $tree->no_space_compacting;
  85.     $tree->parse($item);
  86.     return $tree;
  87.     $tree->delete;
  88. }
  89.  
  90. # 目次作成
  91. sub get_index {
  92.     my $item = shift;
  93.     my $url_list = [];          # リファレンス初期化
  94.     my $count = 0;
  95.     $item = &html2tree($item);
  96.     my $mokuji = $item->look_down('class', 'table-of-contents novels')
  97.                       ->look_down('class', "episodes");
  98.     foreach my $tag ($mokuji->look_down('class', "episode")) {
  99.         my $url = $tag->find('a')->attr('href');            # url
  100.         my $title = $tag->look_down('class', 'title')->as_text; # title
  101.         utf8::decode($title);
  102.         $url = $url_prefix . $url;
  103.         my $open_date = $tag->look_down('class', 'open-date')->as_text;
  104.         $open_date =~ s|(\d{4}\.\d{2}\.\d{2}) \d.+|$1|;
  105.         $open_date = &epochtime( $open_date );
  106.         $url_list->[$count] = [$title, $url, $open_date]; # タイトル、url、公開日
  107.         $count++;
  108.     }
  109.  
  110.     if ($update) {
  111.         my @reverse = reverse( @$url_list );
  112.         my @up_list = ();
  113.         for (my $i = 0; $reverse[$i]->[2] > $last_date; $i++) {
  114.             push(@up_list, $reverse[$i]);
  115.         }
  116.         @up_list = reverse( @up_list );
  117.         $url_list = \@up_list;
  118.     }
  119.     return $url_list;
  120. }
  121.  
  122. # 作品名、著者名取得
  123. sub header {
  124.     my $item = shift;
  125.     $item = &html2tree( $item );
  126.     $item = $item->look_down( 'class', 'content-main');
  127.     $main_title = $item->look_down( 'class', 'title')->as_text;
  128.     $author = $item->look_down( 'class', 'author')->find('a')->as_text;
  129.     utf8::decode($main_title);
  130.     utf8::decode($author);
  131.     return sprintf("%s", $main_title . "\n" . $author . "\n\n\n");
  132. }
  133.  
  134. # 本文処理
  135. sub honbun {
  136.     my $item = shift;
  137.     utf8::decode($item);
  138.     $item =~  s|\x0D\x0A|\n|g;  #改行コード変換。
  139.     # 章見出し取得
  140.     $item =~  m|<div class="chapter-title">(.+?)</div>|s;
  141.     $chapter_title =  $1;
  142.     $chapter_title =~ s|\t||g;
  143.     $chapter_title =~ s|\n||g;
  144.     # 本文取得
  145.     $item =~  m|.*<div class="text " id="novelBoby">(.+)</div>.+<a href="/Users/login.+|s;
  146.    $item =   $1;
  147.    $item =~  s|<br />||g;
  148.    $item =~  s|&nbsp;| |g;
  149.    $item =~  s|\t\t||;         # 一行目のタブを削除。
  150.    $item =~  s|<ruby>(.+?)<rt>(.+?)</rt></ruby>||$1《$2》|g;
  151.    $item =~  s|<em>(.+?)</em>|[#傍点]$1[#傍点終わり]|g;
  152.    $item =~  s|</?span>||g;
  153.    $item =~  s|!!|!!|g;
  154.    $item =~  s|!?|!\?|g;
  155.    $item =~ tr|\x{ff5e}|\x{301c}|; #全角チルダ->波ダッシュ
  156.    # 挿絵処理
  157.    if ( $item =~ m|story-image| ) {
  158.        $item =~  s|<a href=.+? class="story-image"><img src="(.+?)" alt=""/></a>|&get_pic($1)|eg;
  159.    }
  160.    return $item;
  161. }
  162.  
  163. # 挿絵保存
  164. sub get_pic {
  165.    my $address = shift;
  166.    my $fname = basename( $address );
  167.    unless ( defined($base_path) ) {
  168.        $fname = basename( $address );
  169.    }
  170.    else {
  171.        $fname = &get_path($base_path, $fname);
  172.    }
  173.    my $http = LWP::UserAgent->new;
  174.    $http->agent($user_agent);
  175.    my $res = $http->get( $address, ':content_file' => $fname );
  176.    if ( $res->is_success ) {
  177.        print STDERR encode($charcode, "success:: $fname\n");
  178.    }
  179.    else {
  180.        print STDERR encode($charcode, "error:: $fname\n");
  181.    }
  182.    # 挿絵リンク処理
  183.    return "[#挿絵" .
  184.        "(" .
  185.        File::Basename::basename( $address ) .
  186.            ")入る]\n";
  187. }
  188.  
  189. sub get_all {
  190.    my $index = shift;
  191.    my $count = scalar(@$index);
  192.    my $item;
  193.    for ( my $i = 0; $i < $count; $i++) {
  194.        my $text = &get_contents( scalar(@$index[$i]->[1]) );
  195.        $text = &honbun( $text );
  196.        my $title = scalar(@$index[$i]->[0]);
  197.        my $time = &timeepoc( scalar(@$index[$i]->[2]) );
  198.        $item = &honbun_formater( $text, $title );
  199.        print STDERR encode($charcode, "success:: $time : $title \n");
  200.        print encode($charcode, $item);
  201.    }
  202. }
  203.  
  204. sub honbun_formater  {
  205.    my ($text, $title) = @_;
  206.    my $item;
  207.    my $midasi = "\n[#中見出し]" . $title . "[#中見出し終わり]\n\n\n";
  208.    if ( $chapter_title ne "" ) {
  209.        $chapter_title = "\n" . $chapter_title . "\n";
  210.        $item = $kaipage . $separator .
  211.            $chapter_title .
  212.            $midasi . $text . "\n\n" . $separator;
  213.    }
  214.    else {
  215.        $item = $kaipage . $separator .
  216.            $midasi . $text . "\n\n" . $separator;
  217.    }
  218.    return $item;
  219. }
  220.  
  221. #コマンドラインの取得
  222. sub getopt() {
  223.    GetOptions(
  224.               "chklist|c=s" => \$chklist,
  225.               "savedir|s=s" => \$savedir,
  226.               "update|u=s"  => \$update,
  227.               "help|h"      => \$show_help
  228.              );
  229. }
  230.  
  231. sub help {
  232.  print STDERR encode($charcode,
  233.        "alpha-novel-dl.pl  (c) 2017 ◆.nITGbUipI\n" .
  234.        "Usage: alpha-novel-dl.pl [options]  [目次url] > [保存ファイル]\n".
  235.        "\tアルファポリス投稿小説ダウンローダ\n".
  236.        "\tまとめてダウンロードし標準出力に出力する。\n".
  237.        "\n".
  238.        "\tOption:\n".
  239.        "\t\t-c|--chklist\n".
  240.        "\t\t\t引数に指定したリストを与えると巡回チェックし、\n".
  241.        "\t\t\t新規追加されたデータだけをダウンロードする。\n".
  242.        "\t\t-s|--savedir\n".
  243.        "\t\t\t保存先ディレクトリを指定する。\n".
  244.        "\t\t\t保存先にサブディレクトリを作って個別に保存される。\n".
  245.        "\t\t-u|--update\n".
  246.        "\t\t\tYY.MM.DD形式の日付を与えると、その日付以降の\n".
  247.        "\t\t\tデータだけをダウンロードする。\n".
  248.        "\t\t-h|--help\n".
  249.        "\t\t\tこのテキストを表示する。\n"
  250.      );
  251.  exit 0;
  252. }
  253.  
  254. # YYYY.MM.DD -> epoch time.
  255. sub epochtime {
  256.    my $item = shift;
  257.    my ($year, $month, $day) = split(/\./, $item);
  258.    timelocal(0, 0, 0, $day, $month-1, $year-1900);
  259. }
  260.  
  261. # epochtime -> YYYY.MM.DD
  262. sub timeepoc {
  263.    my $item =shift;
  264.    my ($mday,$month,$year) = (localtime($item))[3,4,5];
  265.    sprintf("%4d.%02d.%02d", $year+1900, $month+1, $mday);
  266. }
  267.  
  268. # リスト読み込み
  269. sub load_list {
  270.    my $file_name = shift;
  271.    my $LIST;
  272.    my (@item, @list);
  273.    my %hash;
  274.    my $oldsep = $/;
  275.    $/ = "";                    # セパレータを空行に。段落モード
  276.    open ( $LIST, "<:encoding($charcode)" ,"$file_name") or die "$!";
  277.    while (my $line = <$LIST>) {
  278.        push(@item, $line);
  279.    }
  280.    close($LIST);
  281.    $/ = $oldsep;
  282.    # レコード処理
  283.    for (my $i =0; $i <= $#item; $i++) {
  284.        my @record = split('\n', $item[$i]);
  285.        foreach my $field (@record) {
  286.            if ($field =~ /^(title|file_name|url|update)/) {
  287.                my ($key, $value) = split(/=/, $field);
  288.                $key   =~ s/ *//g;
  289.                $value =~ s/^ *//g;
  290.                $value =~ s/"//g;
  291.                 if ($value eq "") {
  292.                     print STDERR encode($charcode, "Err:: $field\n");
  293.                     exit 0;
  294.                 }
  295.                 $hash{$key} = $value; #ハッシュキーと値を追加。
  296.             }
  297.         }
  298.         if ($hash{'title'}) {
  299.             $list[$i] = {%hash}; # ハッシュを配列に格納
  300.         }
  301.         undef %hash;
  302.     }
  303.     undef @item;                #メモリ開放
  304.     return @list;
  305. }
  306.  
  307. sub save_list {
  308.   my($path, $list) = @_;
  309.   open(STDOUT, ">:encoding($charcode)", $path);
  310.   foreach my $row (@$list) {
  311.     print encode($charcode,
  312.                  "title = " .     $row->{'title'} .     "\n" .
  313.                  "file_name = " . $row->{'file_name'} . "\n" .
  314.                  "url = " .       $row->{'url'} .       "\n" .
  315.                  "update = " .    $row->{'update'} . "\n\n\n"
  316.                  );
  317.   }
  318.   close($path);
  319. }
  320.  
  321. sub jyunkai_save {
  322.     my $check_list = shift;
  323.     my $count = @$check_list;
  324.     my $path;
  325.     my $save_file;
  326.     for (my $i = 0; $i < $count; $i++) {
  327.         my $fname = $check_list->[$i]->{'file_name'};
  328.         my $url   = $check_list->[$i]->{'url'};
  329.         my $title = $check_list->[$i]->{'title'};
  330.         my $time  = $check_list->[$i]->{'update'};
  331.         if ( defined($time) ) {
  332.             $last_date = &epochtime( $time );
  333.             $update = 1;
  334.         }
  335.         $base_path = File::Spec->catfile( $savedir, $fname );
  336.         $save_file = &get_path($base_path, $fname) . ".txt";
  337.         open(STDOUT, ">>:encoding($charcode)", $save_file);
  338.         my $body = &get_contents( $url );
  339.         my $dl_list = &get_index( $body ); # 目次作成
  340.         if (@$dl_list) {
  341.             print STDERR encode($charcode, "START :: " . $title . "\n");
  342.             unless ($update) {
  343.                 print encode($charcode, &header( $body ) );
  344.             }
  345.             &get_all( $dl_list );
  346.             my $num = scalar(@$dl_list) -1;
  347.             # 最後の更新日をcheck listに入れる。
  348.             $check_list->[$i]->{update} = &timeepoc( $dl_list->[$num]->[2] );
  349.         }
  350.         else {
  351.             print STDERR encode($charcode, "No Update :: " . $title . "\n");
  352.         }
  353.         $base_path = undef;
  354.         $last_date = undef;
  355.         $update = undef;
  356.     }
  357.     close($save_file);
  358.     &save_list( $chklist, $check_list );
  359. }
  360.  
  361. sub get_path {
  362.     my ($path, $name) = @_;
  363.     my $fullpath;
  364.     if ( -d $path ) {
  365.         $fullpath = File::Spec->catfile($path, $name);
  366.     }
  367.     else {
  368.         require File::Path;
  369.         File::Path::make_path( $path );
  370.         $fullpath = File::Spec->catfile($path, $name);
  371.         print STDERR encode($charcode, "mkdir :: $fullpath\n");
  372.     }
  373.     return $fullpath;
  374. }
  375.  
  376. #main
  377. {
  378.     my $url;
  379.     &getopt;
  380.  
  381.     if ($chklist) {
  382.         unless ($savedir) {
  383.             $savedir = Cwd::getcwd();
  384.         }
  385.         #   print "$chklist\n";
  386.         my @check_list = &load_list( $chklist );
  387.         &jyunkai_save( \@check_list );
  388.         exit 0;
  389.     }
  390.  
  391.     if ($update) {
  392.         if ($update =~ m|\d{2}\.\d{2}\.\d{2}| ) {
  393.             $last_date = "20" . $update;
  394.             $last_date = &epochtime( $last_date);
  395.         }
  396.         else {
  397.             print STDERR encode($charcode,
  398.                                 "YY.MM.DD の形式で入力してください\n"
  399.                                );
  400.             exit 0;
  401.         }
  402.     }
  403.  
  404.   if (@ARGV == 1) {
  405.       if ($ARGV[0] =~ m|$url_prefix/novel/\d{8,9}/\d{8,9}/?$|) {
  406.           $url = $ARGV[0];
  407.           my $body = &get_contents( $url );
  408.           my $list = &get_index( $body ); # 目次作成
  409.           print encode($charcode, &header( $body ) );
  410.           &get_all( $list );
  411.       }
  412.       elsif ($ARGV[0] =~ m|$url_prefix.+/episode/|) {
  413.           print STDERR encode($charcode,
  414.                               "個別ページダウンロード未対応\n"
  415.                              );
  416.       }
  417.       else {
  418.           print STDERR encode($charcode,
  419.                               "URLの形式が、『" .
  420.                               "$url_prefix/novel/8〜9桁の数字/8〜9桁の数字" .
  421.                               "』\nと違います" . "\n"
  422.                              );
  423.       }
  424.   }
  425.   else {
  426.       &help;
  427.       exit 0;
  428.   }
  429.  
  430.   if ($show_help) {
  431.       &help;
  432.       exit 0;
  433.   }
  434. }
Advertisement
Add Comment
Please, Sign In to add comment