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


関心空間の RSS を作成する

by miyagawa at Thu, 23 Oct 2003 20:19

Weblog テクノロジを解説する上で欠かせないのが、RSS という技術です。RSS は Netscape が My Netscape Portal に採用したシンジケーション専用の XML フォーマットで、現在では Weblog にとどまらず、ニュースサイト等のサイト更新情報をシンジケートするデファクトスタンダードになりつつあります。

今回は、RSS 作成の一例として、関心空間 の HTML をパースして、RSS を生成してみます。


関心空間

関心空間は、「自分が今興味を持っているモノをキーワードとして入力していくことで、その周辺情報を入手したり他の新たな関心事を発掘できるサイト」(サイトより引用) です。自分の登録したモノと、他人の登録したモノとを、「つながり」で表現することができ、サイト自体のコンセプトが Weblog や WikiWikiWeb と通じるものがあります。今回は キーワード一覧 のページを元にして、RSS を生成してみましょう(*1)

RSS の種類

ひとくちに RSS といってもいくつかのバージョンがあり、メジャーなものを簡単に分類すると以下のようになります。
version概要
0.91Netscape により考案され、Radio Userland により拡張されたオリジナル。Rich Site Summary の略。
1.0RDF をベースにしている。namespace によるモジュール拡張が可能。RDF Site Summary の略。
2.0RDF は複雑すぎるとして、RSS 0.91 をベースに、namespace による拡張をとりいれたもの。 Dave Winer (Radio Userland) により考案された。Really Simple Syndication の略。
3.0Aaron Swartz によるプレーンテキストなシンジケーションフォーマット。

実際には、これ以外にもいくつかの亜流があり、またこれらの対立やベンダー依存の体質を打破すべく新しい XML フォーマット Atom (*2) の実装も進められています。この辺のいきさつについては、CNET Japan - Blog戦争: RSSはどこへ行く? などを参照してください。

今回の関心空間 RSS では、最もシンプルな 0.91 を採用します(*3)

Perl で RSS の生成、パースをするには XML::RSS を使用します。これ以外の選択肢は、現時点では考えられません。RSS 0.91, 1.0, 2.0 に対応しており、また namespace などによる拡張もサポートしている、デファクトスタンダードな RSS モジュールです。XML::Parser モジュールを内部で使用しているため、expat のインストールが別途必要です。

サンプルコード

関心空間の キーワード一覧 の HTML を取得し、RSS を生成するスクリプトは List 1 のようになります。 このスクリプトを動作させるには、Perl 5.8.0 以上が必要です。

use encoding 'euc-jp', STDOUT => 'UTF-8';
Perl 5.8 より採用された encoding プラグマを使用します。今回はスクリプト自体に、サイトの HTML を解析するための正規表現(マルチバイト文字含む)を EUC-JP で埋めこんでいるため、このプラグマでその文字コードを指定します。

また、最終的に出力する RSS は UTF-8 でエンコードする必要があるため、STDOUT に UTF-8 を指定しています。これにより、ファイル内に EUC-JP で記述した文字列は、Perl 内部で Unicode 文字列として扱われ、STDOUT に print する際には自動的に UTF-8 にエンコードされることになります。

use Encode;
use HTML::Entities;
use LWP::Simple;
use XML::RSS;
動作に必要な各種モジュールを use しています。HTML エンコードのために HTML::Entities、URL から HTML の取得に LWP::Simple、また今回の主役である RSS 作成のための XML::RSS を使用します。

(my $Regex = <<'REGEX') =~ tr/\n//d;
<A href="(index\.php3\?mode=keyword&id=\d+)">
<B><FONT class=css3>(.*?)</FONT></B>
<IMG .*?></A>
<FONT size="-1" class=css2>
 \d+/\d+<!-- .*? --><img .*?>
 \[\d+\] .*?
<BR>
(.*?)<BR>
REGEX
    ;
キーワード一覧 の HTML ソースを見て、アイテム情報を抜き出すための正規表現を作成します。若干複雑な HTML となっていますが、img タグのアトリビュートなど、情報として不必要なものは .*? で処理してしまい、余計なコードにならないように工夫しています。またこうした正規表現は通常横長になってしまいがちですが、ここでは改行を入れて見やすくした上で、

(my $Regex = <<'REGEX') =~ tr/\n//d;

によって改行を除去しています(*4)

my $rss = XML::RSS->new(
    version => 0.91,
    encode_output => 0,
);
XML::RSS オブジェクトを作成します。version は何も指定しなければデフォルトで 1.0 となりますので、ここでは明示的に 0.91 を指定します。encode_output は、XML::RSS に渡す文字列を、出力の際に自動でXMLエンコードするかどうかのフラグで、デフォルトでは 1 となりますが、今回は HTML から拾った文字列を渡すため、二重エンコードがかかってしまう可能性があります。よって encode_output は 0 としています。

$rss->channel(
    title    => 'kanshin.com RSS',
    link     => 'http://www.kanshin.com/',
    description => "関心空間 最新キーワードのRSS",
    language => 'ja',
);
RSS の channel 要素を設定しています。channel 要素には、RSS で配信するサイトについてのメタ情報を記述します。

my $html = decode('Shift_JIS', get($URL));
最新キーワードの HTML を取得します。関心空間の HTML は Shift_JIS でエンコードされているため、Encode.pm の decode で、Unicode 文字列にデコードします。

while ($html =~ m/$Regex/gs) {
    $rss->add_item(
        link  => encode_entities("http://www.kanshin.com/$1"),
        title => $2,
        description => normalize($3),
    );
}
先ほど定義した正規表現 $Regex にマッチさせます。マッチした結果は
$1URL (index.php 相対パス)
$2アイテムのタイトル
$3アイテムの見出し文
のように格納されています。link 属性については、本来であれば HTML エンコードの必要はありませんが、元 HTML で & 要素がエンコードされていない(*5)ため、HTML::Entities の encode_entities によってエンコードしています。

また description 属性には、全角スペースや改行が含まれているため、後述する normalize 関数により除去します。

if ($ENV{GATEWAY_INTERFACE}) {
    require CGI;
    print CGI::header('text/xml; charset=utf-8');
}
このスクリプトは、コマンドラインで RSS を生成する目的で作成しましたが、CGI として動作させることもできるように対策してあります。CGI で動作すると、環境変数 GATEWAY_INTERFACECGI/1.1 といった文字列が格納されます。ここでは、$ENV{GATEWAY_INTERFACE} が設定されていた場合には、CGI.pm の header メソッドにより、Content-Type: を適切に出力するようにしています。

print $rss->as_string();
XML::RSS オブジェクトから as_string メソッドを呼び出し、STDOUT に RSS を出力します。本来であれば、

print encode('UTF-8', $rss->as_string());

のように、結果の文字列を UTF-8 にエンコードする必要がありますが、今回は encoding プラグマで自動的に行われるようにしてあります。

sub normalize {
    local $_ = shift;
    tr/ \r\n//d;
    return $_;
}
description 要素を正規化するサブルーチンです。$_ に引数を格納し、tr で全角スペースと CRLF を削除しています。ここでは tr 内に全角スペースを使用していますが、先頭で encoding プラグマにより定義しているため、問題なく動作します(*6)

実行例

コマンドラインで実行して、RSS を生成してみます(*7)

% ./kanshin_rss.pl
<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE rss PUBLIC "-//Netscape Communications//DTD RSS 0.91//EN"
            "http://my.netscape.com/publish/formats/rss-0.91.dtd">
...
</channel>
</rss>

標準出力に RSS が出力されますので、リダイレクトを使用してファイルに書き出します。

% ./kanshin_rss.pl > kanshin.xml

作成された kanshin.xml を HTTP でアクセス可能な場所に移動します。念のため、.htaccess 等で

AddType text/xml .xml
AddDefaultCharset utf-8

のように設定しておくとよいでしょう(*8)

また kanshin_rss.pl を CGI を実行できるディレクトリ(cgi-bin など)に格納して、直接ブラウザからアクセスして RSS を出力することもできます。

まとめ

今回作成した RSS を、RSS リーダーなどのアグリゲーションツールで読み込むことにより、関心空間の新着アイテムがチェックできます。こうして既存の更新情報を RSS で取り込むと、Web ブラウザでちまちまと巡回していた手間が、RSS リーダーなどのツールに一元化され非常に便利です。

Listings

List 1: kanshin_rss.pl
#!/usr/local/bin/perl -w
# kanshin_rss - generate RSS for kanshin.com

use strict;
use encoding 'euc-jp', STDOUT => 'UTF-8';
use Encode;
use HTML::Entities;
use LWP::Simple;
use XML::RSS;

my $URL = "http://www.kanshin.com/index.php3?mode=search";

(my $Regex = <<'REGEX') =~ tr/\n//d;
<A href="(index\.php3\?mode=keyword&id=\d+)">
<B><FONT class=css3>(.*?)</FONT></B>
<IMG .*?></A>
<FONT size="-1" class=css2>
 \d+/\d+<!-- .*? --><img .*?>
 \[\d+\] .*?
<BR>
(.*?)<BR>
REGEX
    ;

my $rss = XML::RSS->new(
    version => 0.91,
    encode_output => 0,
);
$rss->channel(
    title    => 'kanshin.com RSS',
    link     => 'http://www.kanshin.com/',
    description => "関心空間 最新キーワードのRSS",
    language => 'ja',
);

my $html = decode('Shift_JIS', get($URL));
while ($html =~ m/$Regex/gs) {
    $rss->add_item(
        link  => encode_entities("http://www.kanshin.com/$1"),
        title => $2,
        description => normalize($3),
    );
}

if ($ENV{GATEWAY_INTERFACE}) {
    require CGI;
    print CGI::header('text/xml; charset=utf-8');
}

print $rss->as_string();

sub normalize {
    local $_ = shift;
    tr/ \r\n//d;
    return $_;
}
*1) 執筆時点で関心空間では、RSS 等によるデータ配信は行っていません。
*2) The Atom Project
*3) 日付情報などが不完全であるため 1.0 には適さないという理由もあります。
*4) 正規表現の x オプションを使用する手もありますが、その場合、スペースを \s に置換する必要があります。
*5) a タグの href 要素内では、 & は &amp; にエンコードする必要がありますが、されていないサイトが多いようです。
*6) Perl 5.8.0 では encoding プラグマと tr の動作に不具合があることが報告されていますが、5.8.1 では問題ありません。
*7) 今回の URL は検索などに負荷がかかるようで、若干レスポンスに時間がかります。
*8) 一般に公開する場合は、元サイトの規約に反しないよう注意が必要です。

Copyright©2002-2003 Tatsuhiko Miyagawa