複数のRSSを1つにまとめてFTPでサーバーに置くプログラムmergerss
動機
mixiにはブログのRSS配信を「日記」として登録できるが、登録できるRSSは1つだけである。自分の複数のブログの更新をmixiで知らせたいと思い、RSS配信を1つにまとめるFrankenfeedを使ってみたが、思ったようには動作してくれなかった。
そこで、mergerssというプログラムをPerlで書いた。最初はCGIとして書こうかと思ったが、そうするとCGIを実行できる場所を確保しなければならない。使用目的がブログの更新を知らせることなので、複数のRSSをまとめたRSSファイルを作成して、それをウェブサーバーに置いておけばいいと考えた。ブログの記事を書いた際に、mergerssを実行しなければならないが、大した手間ではない。
ほかの実現手段
2006-08-05追記
はてなブックマークで「それPla」というタグが幾つも付いていて、何のことか分からなかったのだが、宮川さんのPlaggerモジュールを使えばいいということだとやっと気が付いた。確かにその通りで、6月20日に追記したこのプログラムの存在意義も薄いことになる。強いて言えば、Plaggerを使うよりはRSSのデータをより直接触る分、RSSの処理に関する理解が深まるかも知れないということか。
2006-06-20追記
はてなブックマークで「それはてな RSS でやってる。」というご指摘もいただいた。確かに、はてなRSSに、自分のブログの「グループ」を作り、そこに自分のブログを登録しておけば、複数のRSSをまとめたRSSを得ることができる。たとえばmyblogsという名前のグループを使うとすると、まとめたRSSのURLは http://r.hatena.ne.jp/himazublog/myblogs/rss となる。試してはいないが、こうやって はてなRSSで作成したRSSはmixiで使えるだろう。
したがって、mergerssの意義は、自分で好きに改造できることと、RSSやAtomをPerlで扱う例題となっている、ということになる。
仕様
mergerssの仕様は以下の通りである。
- Unix*1のコマンド行で使う。コマンド行引数はなし。
- 検証は自分のブログでおこなっているのみ。実行環境はCygwinのperl。特別なことはしていないので、どんなUnixでも動くと思う。
- 入力はRSS 1.0, RSS 2.0, Atom。
- 出力はRSS 2.0。
- 出力中には各記事の題名とURLが含まれるのみ。mixiにはこれで十分なので。
- 作成したRSS 2.0のファイルは指定された場所に保存し、その上で指定されたFTPサーバーの指定された場所に置く。
- 設定はプログラムの最初の部分に定数宣言として書いてある。
- 起動するとFTPサーバーのパスワードを聞いてくる。プログラム中にFTP_PASSWD定数として定義することも可能。
- CGIとして動作させる場合(.cgiの拡張子で保存されている場合)は、FTP関係の定数宣言は不要である。
mergerssの出力例はこれである。
コード
コードは以下の通り。
#!/usr/bin/perl -w use strict; my $RCSID = '$Id: mergerss,v 1.4 2006/06/19 20:45:28 himazu Exp $'; my $VERSION = (split(' ', $RCSID))[2]; use constant FEEDS => [ [atom => "http://himazu.blogspot.com/atom.xml"], [rss => "http://d.hatena.ne.jp/himazublog/rss"], ]; # As you see, an Atom feed is specified with "[atom => URL]" # and an RSS 1.0 or RSS 2.0 feed is specified with "[rss => URL]" use constant OUTPUT_FILENAME => "mergedrss.xml"; use constant OUTPUT_DIR => "$ENV{HOME}/public_html"; use constant REMOTE_DIR => "/himazu"; use constant FTP_SERVER => "ftp.geocities.jp"; use constant FTP_USER => "himazu"; #use constant FTP_PASSWD => "xxxxxxxx"; use Date::Parse; ######################################################################## use LWP::Simple; use XML::RSS; sub get_rss { my $url = shift; my $rss = XML::RSS->new; my $feedcont = get($url); $rss->parse($feedcont); my @items; for my $item ( @{$rss->items} ) { my $title = $item->{title}; Encode::_utf8_off($title); my $pubdate = $item->{dc}->{date} || $item->{pubDate}; push(@items, {title => $title, link => $item->{link}, pubDate => str2time($pubdate)} ); } my $title; my $link; if ( $rss->{channel} ) { $title = $rss->{channel}->{title}; $link = $rss->{channel}->{link}; } else { $title = $rss->{title}; $link = $rss->{link}; } Encode::_utf8_off($title); return { title => $title, link => $link, items => \@items, }; } ######################################################################## use XML::Atom::Feed; sub get_atom { my $url = shift; my @items; my $feed = XML::Atom::Feed->new($url); for my $entry ( $feed->entries ) { my $link = ""; for my $l ( $entry->link ) { my $rel = $l->get("rel"); if ( $rel eq "alternate" ) { $link = $l->get("href"); } } next if ( $link eq "" ); push(@items, {title => $entry->title, link => $link, pubDate => str2time($entry->updated)} ); } my $feedlink = ""; for my $l ( $feed->link ) { my $rel = $l->get("rel"); if ( $rel eq "alternate" ) { $feedlink = $l->get("href"); } } return { title => $feed->title, link => $feedlink, items => \@items, }; } ######################################################################## sub escape_markup { local $_ = shift; s/\&/\&/g; s/\</\</g; s/\>/\>/g; s/\"/"/g; s/\'/'/g; $_; } use Date::Format; sub rfc822date { time2str("%a, %e %b %Y %T %z", $_[0]); } ######################################################################## use IO::File; use Net::FTP; sub main { my $is_cgi = $0 =~ /\.cgi$/i; my $passwd; unless ( $is_cgi ) { if ( defined &FTP_PASSWD ) { $passwd = &FTP_PASSWD; } else { print "password: "; system('stty -echo'); $passwd = <STDIN>; print "\n"; chomp $passwd; } system('stty echo'); } my @feeds; for my $i ( @{FEEDS()} ) { my ($type, $url) = @$i; if ( $type eq "rss" ) { push(@feeds, get_rss($url)); } elsif ( $type eq "atom" ) { push(@feeds, get_atom($url)); } else { warn "invalid feed type: $type"; } } my $rss = XML::RSS->new(version => "2.0"); $rss->channel( title => $feeds[0]->{title}, link => $feeds[0]->{link}, description => "merged RSS feeds", generator => "mergerss $VERSION", ); my @all_items; for my $i ( @feeds ) { push(@all_items, @{$i->{items}}); } for my $i ( sort {$b->{pubDate} <=> $a->{pubDate}} @all_items ) { $rss->add_item(title => escape_markup($i->{title}), link => $i->{link}, pubDate => rfc822date($i->{pubDate})); } if ( $is_cgi ) { print "Content-Type: application/xml\r\n"; my $rss_as_string = $rss->as_string; print "Content-Length: ", length($rss_as_string), "\r\n"; print "\r\n"; print $rss_as_string; } else { my $output_filepath = &OUTPUT_DIR . "/" . &OUTPUT_FILENAME; my $outputfh = IO::File->new("> $output_filepath"); $outputfh->print($rss->as_string); $outputfh->close; my $ftp = Net::FTP->new(&FTP_SERVER, Passive => 1) or die "failed to connect to @{[&FTP_SERVER]}\n"; $ftp->login(&FTP_USER, $passwd) or die "filed to login " . $ftp->message . "\n"; $ftp->binary; $ftp->put($output_filepath, &REMOTE_DIR . "/" . &OUTPUT_FILENAME); $ftp->quit; } } main(); exit 0;