Advertisement
maya000

カクヨム投稿小説自動ダウンローダ

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