理系学生日記

おまえはいつまで学生気分なのか

アニメの情報を効率よく集めるために OPML つくるスクリプトを作り人生が豊かになる

背景

この何ともならない人生をそれでもアニメ充するために、ここに列挙されている Web サイトだけは見ておかなくてはならぬ!そうしないと人間が死ぬ!!lたくさん死ぬ!!!!

上記サイトで紹介されているサイトの Feed を探し、RSS リーダーに登録すれば良い話ではあるのですけれども、そんな単純作業を手でやるほどぼくたちの時間は安いのでしょうか。いいえ、そんな単純作業を手でやるくらいなら、その時間でアニメを見ていた方がマシです。圧倒的にマシです。
ですから、ここはある程度自動化させた方が良いでしょうし、ある程度汎用的に使えるようにしたほうが良いでしょう。そういうスクリプトを作っておき、今後上記エントリのような非常に有用なエントリが出てきたときに、そのスクリプトを回せばよろしい。さぁアニメを見よう。

どんなのが良いか考えた

上記エントリで紹介されているサイトの Feed を RSS リーダに登録するために、必要となる処理を洗い出してみましょう。

  1. エントリから、紹介されているサイトの URL を抽出する
  2. URL を使ってサイトにアクセスし、Feed の URL を抽出する
  3. Feed の URL を、RSS リーダに登録する

わずか 3 ステップ、簡単ですね。

あとは、どれを自動化すれば良いのかというところですが、3. の RSS リーダの登録については、登録方法が RSS リーダ依存です。ぼくは専ら Livedoor Reader を使っており、Livedoor Reader では非公開の API を使えば Feed 登録できるのですが、そこを自動化してしまうと応用範囲が Livedoor Reader に限られてしまいます。
いつ Google Reader に浮気をするか分からないので、ここはもうすこし汎用的な方法が欲しい。
せっかく OPML というフォーマットがあるのですから、ここは OPML として Feed を書き出しておき、RSS リーダにインポートすればいいんだ!!今日び OPML に対応してない RSS リーダなんて知るか!!!!!

実装方法

  1. URL 抽出なんかは、Web::Scraper を使えば数行で書けると思いました。XPath を引数で受けとるようにすれば、汎用性も上がりますね。
  2. Feed の URL の抽出については、XML::Feed がそういう機能を持っていたので、それを使うことにします。(別に Feed::Find もあったのだけれど、XML::Feed も実はそれを使っていた)
    • ただし、
      • サイトによっては同じ内容が複数のフォーマット(RSS 2.0 や RDF などとして)で提供されていることがあります。
      • サイトによってはジャンル別に複数のフィードが提供されていることがあります。
    • 判断を自動化するの難しいから、ここは人間が購読するかどうかを選択できるようにしよう
  3. OPML 作成については XML::OPML 一択かしら。

実行してみましょう

$ ./make-opml.pl -f myfeed.opml http://d.hatena.ne.jp/numenunu/20120604/1338796080 '//div[@class="day"][1]//a'
<<各 URL に対して Feed 探しまくってる>>
http://d.hatena.ne.jp/numenunu/20120604 ... done.
http://d.hatena.ne.jp/numenunu/20120604/1338796080 ... done.
http://d.hatena.ne.jp/numenunu/searchdiary?word=%2A%5B%A5%A2%A5%CB%A5%E1%5D ... done.
http://www6.ocn.ne.jp/~katoyuu/ ... done.
http://b.hatena.ne.jp/entry/www6.ocn.ne.jp/~katoyuu/ ... done.
http://www.karzusp.net/ ... done.

<<購読するかどうか判断>>
subscribe 稀にライトノベルを読むよ^0^/ (http://d.hatena.ne.jp/numenunu/rss) ? (y/n):  (y/n) : y
subscribe 稀にライトノベルを読むよ^0^/ (http://d.hatena.ne.jp/numenunu/rss2) ? (y/n):  (y/n) : n
subscribe 稀にライトノベルを読むよ^0^/ (http://d.hatena.ne.jp/numenunu/searchdiary?word=%2A%5B%A5%A2%A5%CB%A5%E1%5D&mode=rss) ? (y/n):  (y/n) : n
subscribe カトゆー家断絶(http://ic.edge.jp/page2feed/http://www6.ocn.ne.jp/~katoyuu/) ? (y/n):  (y/n) : y
subscribe カトゆー家断絶(http://b.hatena.ne.jp/entry/rss/http://www6.ocn.ne.jp/~katoyuu/) ? (y/n):  (y/n) : n
subscribe &#227;&#227;&#188;&#227;SP(http://www.karzusp.net/atom.xml) ? (y/n):  (y/n) : n
subscribe かーずSP(http://www.karzusp.net/index.xml) ? (y/n):  (y/n) : y
subscribe かーずSP(http://b.hatena.ne.jp/entry/rss/http://www.karzusp.net/) ? (y/n):  (y/n) : n
subscribe 楽画喜堂(http://b.hatena.ne.jp/entry/rss/http://www.rakugakidou.net/) ? (y/n):  (y/n) : y

なんか文字化けしてるのヤバいけどもういい。
結果として OPML が出力されますので、これをお好みの RSS リーダにインポートしてやればよろしい。

ソース

ソースはこんな感じです。

#!/usr/bin/env perl
use strict;
use warnings;
use v5.10;
use XML::Feed;
use XML::OPML;
use Web::Scraper;
use URI;
use Pod::Usage;
use Encode;
use IO::Prompt::Simple;
use List::MoreUtils qw(uniq);
use Getopt::Long;
# use Data::Printer {
#     filters => { 
# 	'XML::Feed::Format::RSS' => sub {
# 	    encode( 'utf8' => join "\n", map {
# 		"\t$_ => " . $_[0]->$_
# 	    } qw/title link description/ );
# 	}
#     }
# };

my $enc = find_encoding('utf8');
$XML::Feed::Format::RSS::PREFERRED_PARSER = "XML::RSS::LibXML";
GetOptions(
    'f|filename=s' => \my $filename,
    'h|help'       => \my $help
) or pod2usage();

pod2usage() unless ($filename);
pod2usage() unless @ARGV == 2;
pod2usage() if $help;

main($ARGV[0], $ARGV[1], $filename); exit;

sub main {
    my ($url, $xpath, $filename) = @_;
    
    # XPath に基づいて対象 URI をスクレイピングし、URL のリストを取得する
    my $urls = scraper { 
	process $xpath, 'url[]' => '@href'; 
    }->scrape( URI->new($url) )->{url};
    
    
    my @feeds = find_uniq_feeds( @$urls );            
       @feeds = filter_feeds_by_user( @feeds );
    make_opml($filename, 'my feed', @feeds);
}

# 与えられた URI のリストから Feed を探し、Uniq な URL リストを返す
sub find_uniq_feeds {
    my @urls = @_;
    
    my @feeds;
    for my $url (@urls) {
	print "$url ... ";
	push @feeds, XML::Feed->find_feeds( $url ) or do { 
	    say "skipping ", XML::Feed->errstr;
	    next;
	};
	say "done.";
    }

    return uniq @feeds;
}

# Feed の URL リストを元にユーザが購読したい Feed の情報を返却する
sub filter_feeds_by_user {
    my @feed_urls = @_;

    my @filtered_feeds;
    for my $url (@feed_urls) {

	my $feed  = XML::Feed->parse( URI->new($url) ) or next;
	my $title = $enc->encode($feed->title);
	if ( prompt("subscribe $title($url) ? (y/n): ", { yn => 1 } ) ) {
	    push @filtered_feeds, [$url => $feed];
	}
    }
    return @filtered_feeds;
}

# フィード情報を含んだ OPML を生成する
sub make_opml {
    my ($filename, $title, @feeds) = @_;

    my $opml = XML::OPML->new(version => '1.1');
    $opml->head( title => $title );

    foreach my $feed (@feeds) {
	$opml->add_outline(
	    title       => $enc->encode( $feed->[1]->title ),
	    description => $enc->encode( $feed->[1]->description ),
	    type        => 'rss',
	    htmlUrl     => $feed->[1]->link,
	    xmlUrl      => $feed->[0],
	);
    }

    $opml->save($filename) or die XML::Feed->errstr;
}

__END__

=head1 SYNOPSIS

    $ make-opml.pl -f myfeed.opml http://d.hatena.ne.jp/kiririmode '//div[@class="day"][1]//a'