2004-10-18  mixiで時間泥棒の被害にあわないために

あらまし

WindowsプラットフォームのPerlとPerlモジュールを使って,既存のWWWサイトのRSS(RDF Site Summary)を作る手順を簡単に説明します。RSSはサイトの概要を記述したものであり,ウェブログ(Weblog)の見出しによく使われています。

ソーシャルネットワークサービス「mixi」で日記を公開するには,mixi提供の「日記機能」を使うほかにも,よそのWWWサイトの日記にmixiからリンクしてしまうこともできます。これは元々,ウェブログにリンクするためのものですが,インターフェイスさえ合わせればウェブログでなくても構いません。RSSさえ用意できれば,手打ちのHTMLで作ったサイトもmixiの日記の替わりにできるのです。

いまのところmixiには,日記をエクスポートする方法がありません。このことが,どれほど問題なのか気にする人と気にしない人がいると思います。私は「これって時間泥棒かな」と思い(ふめい),その被害に遭わないために外のサイトを再利用してしまうことにしました。

RSSとテンプレート

「あらまし」では「日記っ,日記っ」などと,オモチャ売場で駄々をこねる少女かのように連呼してしまいました。そもそも「日記」って,どんなものなのでしょうか。ここでは,ひとつのHTMLファイルに複数のお話が時系列に並んだものとしておきます。お話には,件名と日付時刻が書かれているものとします。この条件から外れたもの,例えば「ふたつ以上のHTML」や「件名と日付時刻が書かれていないもの」は扱えないものとします。

RSSを作るには,HTMLファイルからひとつひとつのお話の件名,日付時刻を抽出しなければなりません。RSS云々のまえに,HTMLファイルからどうやって必要な情報を抽出するかを考えなければならないのです。

幸いTemplate::ExtractorというPerlモジュールを利用すると,テンプレートでファイルの文章構造を識別し,お目当ての文字列だけを取得することができます。今回,使ったテンプレートを以下に示します。なおテンプレートのメタ文字,構文などの説明は割愛します。

[% ... %]
<title>
[% wwwtitle %]
</title>
[% ... %]
</h2>
<!-- item -->
[% FOREACH records %]
<h3>
[% date_title %]
</h3>
[% ... %]
<!-- item -->
[% END %]

さて,HTMLファイルの構造によりますが,テンプレートを適用しただけでは抽出に失敗することが多いです。機械的に生成されたHTMLファイルならまだしも,ひとが書いたコードでは空白や改行の入り方がまちまちだからです。テンプレートを適用するまえに,それらの影響を取り除いておかなければなりませんが,問題解決の考え方はいろいろあると思いますので対応はお任せします。

RSSの生成

HTMLファイルにテンプレートを適用すれば,欲しい部分の文字列を取得することができます。つぎに考えることは,それらの情報からRSSを作ることです。RSSはXMLで記述するので,やることはXMLドキュメントを組み立てることになります。

「親ノードに子ノードをつぎつぎ追加して,ツリーを作ればいいのかしら」

と思った人がいらっしゃると思います。

「それって面倒(びっくりマーク)。createElementとかinsertBeforeとか延々とやるわけ(はてなマーク)簡単にできる方法ないの(はてなマーク)」

と思った人がいらっしゃると思います。

便利なことにXML::RSSというPerlモジュールを使うと,RSSの実体をひとつ生成して,その実体に項目(ハッシュ)をつぎつぎ追加していくことでRSSを作ることができます。XMLは気にしなくても構わないのです。「面倒そうなことが嫌い」「煩わしい関係がイヤ」という思いは万国共通なのです。

つぎに実際にできたプログラム(extract.pl)とRSSファイルを示します。使い方は「perl extract.pl HTMLファイル名 テンプレートファイル名」です。実行すると標準出力にRSSが出力されます。注意したいのは,出力されたファイルの文字エンコードがShiftJISだということです(注意:Windowsは厳密にはShiftJISではなくCP932という文字エンコードだそうです)。文字エンコードを変換する方法はいろいろありますが,私はnkfをパイプで繋いでUTF-8にすることにしました。

extract.pl

#!/usr/bin/perl -w

use strict ;
use encoding 'cp932' ;
use XML::RSS ;
use Template::Extract ;

################################################################################

binmode(STDERR, ':raw :encoding(cp932) ') ;

my $template ;
my $doc ;

open(IN, '<:encoding(cp932)', "$ARGV[0]") 
    || die "Can't open html file." ;

$doc = do {
    local $/ ;
    <IN> ;    
} ;
close( IN ) ;

open(IN, '<:encoding(cp932)', "$ARGV[1]")
    || die "Can't open template file." ;

$template = do {
    local $/ ;
    <IN> ;
} ;
close( IN ) ;

################################################################################

$doc =~ s!\x0d\x0a! !gs ;
$doc =~ s!<!\x0d\x0a<!gs ;
$doc =~ s!>!>\x0d\x0a!gs ;
$doc =~ s!\s*\x0d\x0a!\x0d\x0a!gs ;

$doc =~ s|\x0d\x0a<h3>|\x0d\x0a<!-- item -->\x0d\x0a<h3>|gs ;
$doc =~ s!\x0d\x0a!\n!gs ;

################################################################################

my $obj ;
$obj = (new Template::Extract())->extract( $template, $doc ) ;

my $rss ;
$rss = new XML::RSS( version => '1.0' ) ;

################################################################################

my ($sec, $min, $hours, $mday, $mon, $year) ;

($sec, $min, $hours, $mday, $mon, $year)
    = localtime() ;

my $now_iso8601 ;
$now_iso8601
    = sprintf( "%04d-%02d-%02dT%02d:%02d:%02d+09:00", $year + 1900, $mon + 1,
                    $mday, $hours, $min, $sec ) ;

($sec, $min, $hours, $mday, $mon, $year)
    = localtime( time() - 5 * 24 * 60 * 60 ) ;

my $offset ;
$offset
    = sprintf( "%04d-%02d-%02dT%02d:%02d:%02d+09:00", $year + 1900, $mon + 1,
                    $mday, $hours, $min, $sec ) ;

################################################################################

my $channel = {} ;

$channel->{ title } = $obj->{ wwwtitle } ;
$channel->{ link }
    = "http://www2s.biglobe.ne.jp/~yumi-ii/area_murono/htmlfiles/index.html" ;

$channel->{ dc }->{ date } = $now_iso8601 ;
$channel->{ dc }->{ language } = "ja" ;

$rss->channel( %{ $channel } ) ;

################################################################################

my %mon2mm = (
    JAN => 1, FEB => 2, MAR => 3, APR => 4,
    MAY => 5, JUN => 6, JUL => 7, AUG => 8,
    SEP => 9,
    OCT => 10, NOV => 11, DEC => 12
) ;

for ( @{ $obj->{ records }} )
{
    my $item = {} ;
    my ( $dd, $mon, $yyyy, $hh, $mm, $title ) ;
    
    if ( ( $dd, $mon, $yyyy, $hh, $mm, $title )
            = ( $_->{ date_title } =~ 
            /
            (\d{1,2})\s
            (\w{3})\s
            (\d{4})\s
            (\d{1,2})
            :
            (\d{2})
            \s?
            \x81\x75(.*)\x81\x76
            /x
        ) )
        {
            #
        }
        else
        {
            print "err:$_->{ date_title }" ;
        }

    my $date ;
    $date = sprintf( "%04d-%02d-%02dT%02d:%02d:00+09:00",
        $yyyy, $mon2mm{ $mon }, $dd, $hh, $mm ) ;

    if ( $offset le $date )
    {
        $item->{ title }
            = substr( "yumi-ii $title", 0, 20 ) ;

        $item->{ link }
            = "http://www2s.biglobe.ne.jp/~yumi-ii/"
                ."area_murono/htmlfiles/index.html#hamiyoko" ;

        $item->{ dc }->{ date }
            = $date ;
            
        $item->{ dc }->{ creator } = "MURONO Bunjin" ;

        $rss->add_item( %{ $item } ) ;
    }
}

################################################################################

print $rss->as_string ;

# [EOF]

生成されたRSSファイル

よく見ると属性がちょっと足りないみたいです。実用上,差し支えなかったのでそのままにしています。

<?xml version="1.0" encoding="UTF-8"?>

<rdf:RDF
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 xmlns="http://purl.org/rss/1.0/"
 xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/"
 xmlns:dc="http://purl.org/dc/elements/1.1/"
 xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
 xmlns:admin="http://webns.net/mvcb/"
>

<channel rdf:about="http://www2s.biglobe.ne.jp/~yumi-ii/area_murono/htmlfiles/index.html">
<title>
yumi-ii/area_murono
</title>
<link>http://www2s.biglobe.ne.jp/~yumi-ii/area_murono/htmlfiles/index.html</link>
<description></description>
<dc:language>ja</dc:language>
<dc:date>2004-10-16T02:25:27+09:00</dc:date>
<items>
 <rdf:Seq>
  <rdf:li rdf:resource="http://www2s.biglobe.ne.jp/~yumi-ii/area_murono/htmlfiles/index.html#hamiyoko" />
  <rdf:li rdf:resource="http://www2s.biglobe.ne.jp/~yumi-ii/area_murono/htmlfiles/index.html#hamiyoko" />
  <rdf:li rdf:resource="http://www2s.biglobe.ne.jp/~yumi-ii/area_murono/htmlfiles/index.html#hamiyoko" />
  <rdf:li rdf:resource="http://www2s.biglobe.ne.jp/~yumi-ii/area_murono/htmlfiles/index.html#hamiyoko" />
 </rdf:Seq>
</items>
</channel>

<item rdf:about="http://www2s.biglobe.ne.jp/~yumi-ii/area_murono/htmlfiles/index.html#hamiyoko">
<title>yumi-ii 筋トレの真似3年目</title>
<link>http://www2s.biglobe.ne.jp/~yumi-ii/area_murono/htmlfiles/index.html#hamiyoko</link>
<dc:creator>MURONO Bunjin</dc:creator>
<dc:date>2004-10-15T23:25:00+09:00</dc:date>
</item>

<item rdf:about="http://www2s.biglobe.ne.jp/~yumi-ii/area_murono/htmlfiles/index.html#hamiyoko">
<title>yumi-ii mixi始めて7日目</title>
<link>http://www2s.biglobe.ne.jp/~yumi-ii/area_murono/htmlfiles/index.html#hamiyoko</link>
<dc:creator>MURONO Bunjin</dc:creator>
<dc:date>2004-10-13T22:25:00+09:00</dc:date>
</item>

<item rdf:about="http://www2s.biglobe.ne.jp/~yumi-ii/area_murono/htmlfiles/index.html#hamiyoko">
<title>yumi-ii 公衆便所は危険な香り</title>
<link>http://www2s.biglobe.ne.jp/~yumi-ii/area_murono/htmlfiles/index.html#hamiyoko</link>
<dc:creator>MURONO Bunjin</dc:creator>
<dc:date>2004-10-12T19:30:00+09:00</dc:date>
</item>

<item rdf:about="http://www2s.biglobe.ne.jp/~yumi-ii/area_murono/htmlfiles/index.html#hamiyoko">
<title>yumi-ii mixi始めて5日目</title>
<link>http://www2s.biglobe.ne.jp/~yumi-ii/area_murono/htmlfiles/index.html#hamiyoko</link>
<dc:creator>MURONO Bunjin</dc:creator>
<dc:date>2004-10-11T07:39:00+09:00</dc:date>
</item>

</rdf:RDF>

気づいたこと

やっていることは難しくないのですが,簡単そうなものに限ってやっかいな罠が潜んでいるものです。気づいたことを挙げておきます。

文字エンコード:古くて新しい問題です。標準入出力はShiftJIS(CP932),Perlのプログラム本体もShiftJISです。XML:: RSSが吐くXMLのエンコードは何になるでしょう。RSSファイルを開いてみると冒頭に「<?xml version="1.0" encoding="UTF-8"?>」なんて書いてあるから,てっきりUTF-8かと思ったら実際はShiftJISのままでした。

データ構造:「ハッシュのハッシュ」が登場します。これは「ハッシュの中にハッシュが入っている」のではなくて,「ハッシュのリファレンスのハッシュ」です(たしか←なぞ)。はじめはデータ構造に悩むかもしれません。間違うと空文字が返ってくるか,未定義値を参照したという警告が実行時に出力されます。