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


RSS feed を JavaScript で HTML に埋め込む

by miyagawa at Thu, 30 Oct 2003 17:06

RSS を利用すると、自分の Blog サイトのサイドバーなどに、お気に入りの Blog サイトの更新情報などを表示することができます。このように、サイト間でのリンクの導線を動的に生成することができるのも、Blog の魅力の1つと言えます。今回は JavaScript Include と呼ばれる手法を使って、既存の Blog サイトに負荷をかけることなく、RSS を HTML に埋め込む手法を紹介します。


JavaScript Include

他サイトの RSS を自分の Blog サイトに持ってくるには、通常 Blog ツールに付属するプラグインやサードパーティツール(*1) を利用します。これらのツールは定期的に RSS を GET し、静的に生成される HTML を再構築したり、静的な HTML に書き出して、出力時に Include するといった手法をとります。

しかし、こうした手法は、

というデメリットがあります(*2)

今回は、こうした手間をかけず、かつわりと汎用的に使える手法として、 JavaScript Include (CSI: Client-Side Include とも呼ばれます) という手法を用います。JavaScript Include は、以前紹介した BlogRolling の更新情報を Blog サイトに埋め込む方法でも使用されている手法で、出力する HTML に

<script language="JavaScript" src="http://example.com/rss.js"></script>

といった JavaScript の Include 命令を書き込み、インクルードされる .js ファイル内には document.write で HTML を出力する JavaScript コードを書き込んでおくと、ブラウザ上でそれらのコードが実行されてマージされ、レンダリングされるという仕組みです。

この JavaScript Include 手法を RSS Feed の HTML 埋め込みに適用すると、

  1. cron 等で、RSS を javascript 化しておく (foo.js とする)
  2. Blog のテンプレートに <script src=".../foo.js"></script> を記述する
という個別の 2つの作業で、埋め込みができるようになります。また、この URL をパブリックに公開することによって、 という副次的な効果も見込めます。

サンプルコード

今回は RSS ファイルの URL を指定すると、それを JavaScript 化するスクリプト List 1 を作ってみました。HTTP GET で RSS を取得し、XML::RSS でパースしながら JavaScript の document.write によるコードに変換します。

use Digest::MD5 qw(md5_hex);
use Encode;
use HTML::Template;
use LWP::Simple;
use XML::RSS;
出力する HTML のテンプレートを記述するために HTML::Template を使用しています。若干オーバースペック気味ですが、元々バッチ起動ですので問題ないでしょう。

our $CacheDir = "cache";
mkdir $CacheDir, 0755 unless -e $CacheDir;
RSS ファイルをキャッシュするディレクトリを指定、存在しなければ初期化しています。

my $url = shift or die "URL needed!";
my $num = shift || 10;
my $encoding = shift || "utf-8";
コマンドラインの引数には RSS ファイルの URL, JavaScript に記録するエントリ数、出力する JavaScript のエンコーディングを指定します。それぞれデフォルトはエントリ数は 10, エンコーディングは UTF-8 としています。

my $digest = md5_hex($url);
my $cache = "$CacheDir/$digest.xml";

my $status = LWP::Simple::mirror($url, $cache);
if (is_error($status)) {
    die "$url not found!\n";
} else {
    rss2js($cache, $num, $encoding);
}
RSS を HTTP GET します。mirror を使用して無駄なトラフィックが発生しないようにしています。

sub rss2js {
    my($xml, $num, $encoding) = @_;
    my $rss = XML::RSS->new();
       $rss->parsefile($xml);
XML::RSS で RSS を parsefile します。

    my @items = map {
        +{ title => $_->{title},
           link  => $_->{link} }
    } splice(@{$rss->items}, 0, $num);
$rss->items のうち先頭 $numsplice で取り出し、map でハッシュリファレンスを生成しています。

    my $template = HTML::Template->new(filehandle => \*DATA);
    $template->param(
        title => $rss->{channel}->{title},
        link  => $rss->{channel}->{link},
        items => \@items,
    );
DATA ファイルハンドル(*3)をテンプレートとして、HTML::Template オブジェクトを生成し、RSS の channel 要素と、先ほど生成した @items をテンプレートの変数にバインドします。

__DATA__
<div class="rssChannel">
<h3 class="rssTitle">
<a href="<TMPL_VAR name=link escape=HTML>">
<TMPL_VAR name=title escape=HTML></a></h3>
<div class="rssItem">
<TMPL_LOOP name=items>
<a href="<TMPL_VAR name=link escape=HTML>">
<TMPL_VAR name=title escape=HTML></a><br />
</TMPL_LOOP></div>
</div>
DATA テンプレートはこのようになっており、items で TMPL_LOOP を回して、各エントリへのリンクを作っています。デザインを CSS でカスタマイズできるよう、rssChannelrssTitle, rssItem といったクラスを設定してあります。

    binmode STDOUT, ":encoding($encoding)";
    js_print($template->output());
binmode を使用して、STDOUT を $encoding のエンコーディングに設定します。例えば $encoding = "euc-jp"; であれば、STDOUT に print すると自動的に euc-jp にエンコードされる設定となります。encoding プラグマでも同様の効果を得ることができますが、binmode では任意のファイルハンドルに対して、実行時にエンコーディングレイヤを設定できます(*4)

HTML::Template の output メソッドで得られるテンプレートの出力を js_print でエスケープして出力します。

sub js_print {
    my @lines = split /\n/, shift;
    for (@lines) {
        s/\x27/&#x27;/g; # '
        print "document.writeln('$_');\n";
    }
}
LF (LineFeed) で split して、\x27 すなわち ' (シングルクォート) を HTML 数値参照 (&#x27;) に置換しながら、document.writeln() で囲んで出力します。シングルクォートは document.writeln のセパレータとバッティングするため、エスケープしています。他にも、s/'/\\'/g; のようにバックスラッシュでエスケープする方法でも問題ありません。

実行例

RSS の URL とエントリ数、エンコーディングを指定してコマンドラインで実行します。

% ./rss2js.pl http://blog.bulknews.net/mt/index.rdf 10 utf-8 > bulknews.js

出来上がった js ファイルは List 2 のようになります。これを HTTP アクセス可能な場所に配置し、URL を <script src="..."></script> で指定すれば、任意の HTML ページに、blog.bulknews.net の最新10件のエントリを埋め込むことができます。

このコマンドを crontab 等で定期的に実行して js ファイルを更新してやればよいでしょう。

JavaScript Include Tips

JavaScript Include のデメリットは、lynxw3m といった JavaScript を理解しないブラウザで表示ができないことです。また Googlebot などの検索エンジンスパイダーも JavaScript を理解しませんが、これはページの HTML に余計な要素が紛れこまないため、SEO 的によい効果をもたらすこともあります。 また今回のスクリプトでは、出力する JavaScript のエンコーディングを指定可能にしていますが、埋め込む HTML の script タグの charset 属性で、このエンコーディングを指定することができます。

<script language="JavaScript" src="..." charset="utf-8"></script>

これを使用すれば、元の HTML ページが euc-jp、JavaScript が UTF-8 という状況でも、文字化けせずに埋め込みが可能です(*5)

Apache::JavaScript::DocumentWrite

このように HTML を生成しておいて JavaScript Include するというのは様々な場面で使用できます。筆者の作成した CPAN モジュール Apache::JavaScript::DocumentWrite は、任意の HTML ファイルの URL 末尾 に .js を付加することによって、コンテンツの中身を JavaScript Include でインクルード可能にする mod_perl モジュールです。

See Also

Listings

List 1: rss2js.pl
#!/usr/local/bin/perl -w
# rss2js - aggregate RSS to JavaScript

use strict;
use Digest::MD5 qw(md5_hex);
use Encode;
use HTML::Template;
use LWP::Simple;
use XML::RSS;

our $CacheDir = "cache";
mkdir $CacheDir, 0755 unless -e $CacheDir;

my $url = shift or die "URL needed!";
my $num = shift || 10;
my $encoding = shift || "utf-8";

my $digest = md5_hex($url);
my $cache = "$CacheDir/$digest.xml";

my $status = LWP::Simple::mirror($url, $cache);
if (is_error($status)) {
    die "$url not found!\n";
} else {
    rss2js($cache, $num, $encoding);
}

sub rss2js {
    my($xml, $num, $encoding) = @_;
    my $rss = XML::RSS->new();
       $rss->parsefile($xml);

    my @items = map {
        +{ title => $_->{title},
           link  => $_->{link} }
    } splice(@{$rss->items}, 0, $num);

    my $template = HTML::Template->new(filehandle => \*DATA);
    $template->param(
        title => $rss->{channel}->{title},
        link  => $rss->{channel}->{link},
        items => \@items,
    );

    binmode STDOUT, ":encoding($encoding)";
    js_print($template->output());
}

sub js_print {
    my @lines = split /\n/, shift;
    for (@lines) {
        s/\x27/&#x27;/g; # '
        print "document.writeln('$_');\n";
    }
}

__DATA__
<div class="rssChannel">
<h3 class="rssTitle">
<a href="<TMPL_VAR name=link escape=HTML>">
<TMPL_VAR name=title escape=HTML></a></h3>
<div class="rssItem">
<TMPL_LOOP name=items>
<a href="<TMPL_VAR name=link escape=HTML>">
<TMPL_VAR name=title escape=HTML></a><br />
</TMPL_LOOP></div>
</div>
List 2: bulknews.js
document.writeln('<div class="rssChannel">');
document.writeln('<h3 class="rssTitle">');
document.writeln('<a href="http://blog.bulknews.net/mt/">');
document.writeln('blog.bulknews.net</a></h3>');
document.writeln('<div class="rssItem">');
document.writeln('');
document.writeln('<a href="http://blog.bulknews.net/mt/archives/000492.html">');
document.writeln('AllConsuming</a><br />');
document.writeln('');
document.writeln('<a href="http://blog.bulknews.net/mt/archives/000491.html">');
document.writeln('perl 5.8.2 RC1 / perl 5.9.0</a><br />');
document.writeln('');
document.writeln('<a href="http://blog.bulknews.net/mt/archives/000490.html">');
document.writeln('LAMP JukeBox</a><br />');
document.writeln('');
document.writeln('<a href="http://blog.bulknews.net/mt/archives/000489.html">');
document.writeln('Weblog SPAM</a><br />');
document.writeln('');
document.writeln('<a href="http://blog.bulknews.net/mt/archives/000488.html">');
document.writeln('CPAN 8th birthday</a><br />');
document.writeln('');
document.writeln('<a href="http://blog.bulknews.net/mt/archives/000487.html">');
document.writeln('script タグに charset アトリビュート</a><br />');
document.writeln('');
document.writeln('<a href="http://blog.bulknews.net/mt/archives/000486.html">');
document.writeln('Mail::TempAddress</a><br />');
document.writeln('');
document.writeln('<a href="http://blog.bulknews.net/mt/archives/000485.html">');
document.writeln('less presentation</a><br />');
document.writeln('');
document.writeln('<a href="http://blog.bulknews.net/mt/archives/000484.html">');
document.writeln('iPod アップデータ</a><br />');
document.writeln('');
document.writeln('<a href="http://blog.bulknews.net/mt/archives/000483.html">');
document.writeln('Syndication ML</a><br />');
document.writeln('</div>');
document.writeln('</div>');
*1) MovableType であれば mt-rssfeed, blosxom であれば blagg などのプラグインが利用できます。
*2) もちろんそれが手間だと思わなければ、別にデメリットではないです ;-)
*3) __DATA__ 以降に記述した文字列を、DATA ファイルハンドルとして扱うことができます。
*4) binmode でエンコーディングを指定できるのは、Perl 5.8 からの機能です。5.6 以前では、Win32 環境でファイルハンドルをバイナリモードにするのに使われていました。
*5) 手元で検証した限りでは、Windows IE 6.0 では OK ですが、Windows IE 5.5 ではこの charset 指定は無効なようでした。

Copyright©2002-2003 Tatsuhiko Miyagawa