Published on Blog Developer's Cookbook.
Printer friendly version of http://blog.bulknews.net/cookbook/blosxom/opml/blogrolling2opml.html.


BlogRolling を利用して RSS 巡回先を管理 (OPML編)

by miyagawa at Sat, 25 Oct 2003 01:35

BlogRolling を利用して RSS の巡回先を管理できることは紹介しました。実は複数の RSS をまとめて記述する方式として一般的になってきているのが、OPML という XML フォーマットです。今回は BlogRolling で作成した URL リストを OPML に変換してみます。


OPML

OPML は opml.org に仕様が規定されている XML フォーマットの1つで、基本的には Outline Processor Markup Language が示す通り、アウトラインプロセッサ向けのデータ記述言語です。

ただ、Dave Winer の主催する Radio Userland で自分の Subscribe している RSS リストを Export する際のフォーマットとして OPML が使用されたのがきっかけで、現在では主要な RSS Aggregator が、巡回先データを Import/Export する際のデファクトスダンダードとして OPML を利用しています。

よって、BlogRolling で講読している Blog のリストから OPML を作成できれば、RSS Aggregator への Import もごく簡単に行えるというわけです。実は前回も紹介した通り、BlogRolling それ自体が OPML での出力をサポートしています。http://rpc.blogrolling.com/opml.php?r=$BlogRollingId で、巡回先を OPML フォーマットで取得することができます。しかし、BlogRolling で出力される OPML は List 1 のようなフォーマットになっており(*1)、RSS と同様、各サイトのシンジケーション RSS は記述されていません。今回のスクリプトでは、RSS Auto Discovery を使って、RSS も記述された OPML を作成します。

サンプルスクリプト

基本的には blogrolling2rss の大部分が再利用できます。テキストで RSS 一覧を出力していた部分を OPML に変更するだけです。BlogRolling の RSS から、RSS 講読先 OPML を出力するスクリプトは List 2 のようになります。

use HTML::RSSAutodiscovery;
use LWP::Simple;
use Storable;
use Template;
use XML::RSS;
使用するモジュールを use します。追加したのは Template モジュールのみです。

my $rss = XML::RSS->new();
   $rss->parse($xml);

my $title = $rss->{channel}->{title};
OPML の title 属性に使用するため、RSS の channel/title を取得します。

my @sites;
for my $item (@{$rss->{items}}) {
    $cache->{$item->{link}} ||= discover_rss($item->{link}) or next;
    push @sites, { title => $item->{title},
                   link => $item->{link},
                   rss  => $cache->{$item->{link}} };
}
各 item について、RSS の URL を Auto-Discovery で検索した後、@sites 配列にハッシュリファレンスを push します。

my $tt = Template->new();
$tt->process("blogrolling2opml.tt",
             { sites => \@sites, title => $title },
             "mySubscription.opml");
Template オブジェクトを new し、テンプレートファイル blogrolling2opml.tt (List 3) を処理します。テンプレートに渡す変数として、サイト情報の配列 sites と、OPML のタイトルとなる RSS channel の title をハッシュリファレンスで渡します。処理された結果は mySubscription.opml ファイルに出力されます(*2)

その他の部分の処理は、blogrolling2rss と同様ですので省略し、テンプレートの中身をみてみましょう。

<?xml version="1.0" encoding="utf-8" ?>
<opml version="1.0">
<head>
<title>BlogRolling OPML for [% title | html %]</title>
</head>
XML 宣言を行い、opml 要素を開始します。opml の下には head 要素と body 要素がぶら下がり、head の中に title があるというツリー構造となっています(*3)

<body>
[% FOREACH site = sites -%]
<outline
 title="[% site.title | html %]"
 description="Feed for [% site.title | html %] via BlogRolling"
 htmlUrl="[% site.link | html %]"
 type="rss"
 xmlUrl="[% site.rss | html %]" />
[% END -%]
</body>
body 要素の中で、sites についてループして、outline 要素を記述します。この outline 要素1つに、巡回先 1サイトの情報を格納していきます。

opml.org/spec には、

What is an <outline>?

An <outline> is an XML element, possibly containing one or more attributes, and containing any number of <outline> sub-elements.

とだけ記述されており、outline がどのような attribute を持つかの記述はありません。ここでは Radio Userland の OPML に記述されている、
attribute意味
titleRSS Feed 名
descriptionRSS Feed の記述
htmlUrlBlog の URL
typeFeed タイプ(rss)
xmlUrlRSS の URL
を模倣して採用しています。各種 RSS Aggregator に OPML をインポートする際にも、これらの情報が埋まっていれば大概の場合は、大丈夫なようです(*4)

実行例

blogrolling2opml.pl をコマンドラインから実行します。

% ./blogrolling2opml.pl

うまくいけば何も出力されず、mySubscription.opml という OPML ファイル(List 4)が出来上がります。このファイルを、OPML からインポート可能な RSS Aggregator に食わせてみましょう。ここでは SharpReader と FeedDemon で実験してみましたが、どちらもうまくインポートできました(*5)

Listings

List 1: blogrolling.opml
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--  OPML generated by Blogrolling.com --> 
<opml version="1.1">
<head>
<title></title> 
<dateModified></dateModified> 
<ownerName>Blogroll Owner</ownerName> 
<ownerEmail>opml@blogrolling.com</ownerEmail> 
</head>
<body>
<outline text="HAIL 2 U !! - Weblog" type="link"
 url="http://www2u.biglobe.ne.jp/%7Ekyo-n/blog/blosxom.cgi"
 lastmod="Last updated: 07:42:40 GMT on Friday, October 24"
 target=""  />
<outline text="blog.bulknews.net" type="link"
 url="http://blog.bulknews.net/mt/"
 lastmod="Last updated: 05:54:17 GMT on Friday, October 24"
 target=""  />
<outline text="Blog Developer's Cookbook" type="link"
 url="http://blog.bulknews.net/cookbook/blosxom"
 target=""  />
<outline text="Shibuya Perl Mongers" type="link"
 url="http://shibuya.pm.org/"  target=""  />
<outline text="NDO::Weblog" type="link"
 url="http://naoya.dyndns.org/"  target=""  />
</body>
</opml>
List 2: blogrolling2opml.pl
#!/usr/local/bin/perl -w
# blogrolling2opml - build OPML subscription from BlogRolling

use strict;
use HTML::RSSAutodiscovery;
use LWP::Simple;
use Storable;
use Template;
use XML::RSS;

our $CacheFile     = "rss.cache";
our $BlogRollingId = "fd16d26c9ad1029c21a48955b8c19731";

my $cache = eval { Storable::retrieve($CacheFile) } || {};

my $url = "http://rpc.blogrolling.com/rss.php?r=$BlogRollingId";
my $xml = LWP::Simple::get($url);

my $rss = XML::RSS->new();
   $rss->parse($xml);

my $title = $rss->{channel}->{title};

my @sites;
for my $item (@{$rss->{items}}) {
    $cache->{$item->{link}} ||= discover_rss($item->{link}) or next;
    push @sites, { title => $item->{title},
                   link => $item->{link},
                   rss  => $cache->{$item->{link}} };
}

my $tt = Template->new();
$tt->process("blogrolling2opml.tt",
             { sites => \@sites, title => $title },
             "mySubscription.opml");

Storable::nstore($cache => $CacheFile);

sub discover_rss {
    my $url = shift;
    my $discovery = HTML::RSSAutodiscovery->new();
    my $rss = $discovery->parse($url);
    unless (@$rss) {
        warn "no RSS found for $url\n";
        return;
    }
    return $rss->[0]->{href};
}
List 3: blogrolling2opml.tt
<?xml version="1.0" encoding="utf-8" ?>
<opml version="1.0">
<head>
<title>BlogRolling OPML for [% title | html %]</title>
</head>
<body>
[% FOREACH site = sites -%]
<outline
 title="[% site.title | html %]"
 description="Feed for [% site.title | html %] via BlogRolling"
 htmlUrl="[% site.link | html %]"
 type="rss"
 xmlUrl="[% site.rss | html %]" />
[% END -%]
</body>
</opml>
List 4: mySubscription.opml
<?xml version="1.0" encoding="utf-8" ?>
<opml version="1.0">
<head>
<title>BlogRolling OPML for blog.bulknews.net</title>
</head>
<body>
<outline
 title="blog.bulknews.net"
 description="Feed for blog.bulknews.net via BlogRolling"
 htmlUrl="http://blog.bulknews.net/mt/"
 type="rss"
 xmlUrl="http://blog.bulknews.net/mt/index.rdf" />
<outline
 title="Blog Developer's Cookbook"
 description="Feed for Blog Developer's Cookbook via BlogRolling"
 htmlUrl="http://blog.bulknews.net/cookbook/blosxom"
 type="rss"
 xmlUrl="http://blog.bulknews.net/cookbook/blosxom/index.rss10" />
<outline
 title="HAIL 2 U !! - Weblog"
 description="Feed for HAIL 2 U !! - Weblog via BlogRolling"
 htmlUrl="http://www2u.biglobe.ne.jp/%7Ekyo-n/blog/blosxom.cgi"
 type="rss"
 xmlUrl="http://www2u.biglobe.ne.jp/%7Ekyo-n/blog/blosxom.cgi/index.rss" />
<outline
 title="Shibuya Perl Mongers"
 description="Feed for Shibuya Perl Mongers via BlogRolling"
 htmlUrl="http://shibuya.pm.org/"
 type="rss"
 xmlUrl="http://shibuya.pm.org/blosxom/index.rss10" />
<outline
 title="NDO::Weblog"
 description="Feed for NDO::Weblog via BlogRolling"
 htmlUrl="http://naoya.dyndns.org/"
 type="rss"
 xmlUrl="http://naoya.dyndns.org/~naoya/mt/index.rdf" />
</body>
</opml>
*1) 実際は outline 要素が1行で記述されており、横幅が長くなるため整形しました
*2) 第3引数を省略すれば STDOUT に出力されるため、コマンドラインのリダイレクションを使って任意のファイルに保存することも可能です。
*3) HTML と非常によく似ていますね。
*4) title, htmlUrl, xmlUrl だけでも大丈夫そうです
*5) SharpReader では File ⇒ Import Subscriptions、FeedDemon では File ⇒ New ⇒ New Channel Group でインポートします。

Copyright©2002-2003 Tatsuhiko Miyagawa