Atomフィード配信デスクトップCGIの作成
今回は、Atomフィードを配信するデスクトップCGI「rn2atom.cgi」を作成します。「rn2atom.cgi」は、著者のWeb日記「日曜プログラマのひとりごと」のAtomフィードを配信するスクリプトです。Web日記はシフトJISコードで書かれていますから、UTF-8に変換して、取り扱う必要があります。「rn2atom.cgi」は、デスクトップにあるHTMLファイルを処理することもできますし、WebサイトからLWP::Simpleモジュールのgetメソッドで読み込んで処理することもできるように書いていますから、どのように動作するか、実際に動かしてみることも可能です。また、できるだけ、他への応用をしやすいように説明していきます。
Atomフィード配信デスクトップCGI - 「rn2atom.cgi」(1/2)
CGI動作の概要
本CGIは、作成したAtomフィードをFTPで配信するか、デスクトップで関連付けされたアプリケーションでチェックするかを$ftpswで設定することができます。デフォルトは、$ftpsw = 0でFTP配信しませんから、FTP関連の設定をする必要はありません。関連付けされたアプリケーションを起動するためのデスクトップCGIについては、「start.cgi」については後述します。Atomフィードであるデフォルト名「renewal_atom.xml」は、「rn2atom.cgi」が起動されるディレクトリに作成されますから、$atompathに設定してください。
また、本CGIは、デスクトップ上のWeb日記(HTML)からAtomフィードを作成するか、Webサイト上のHTMLを読み込んでAtomフィードを作成するかを選択することができます。デフォルトでは$desksw = 0で、Webの著者サイトから日記ファイルを読み込むようになっていますから、とりあえず動作を試すためには$dirの設定は必要ありません。
CGIのパラメータとして、yearとmonthがあります。monthのパラメータが存在すると、「日曜プログラマのひとりごと」の該当する年月の日記HTMLファイルを読み込んで、Atomフィードを作成・配信します。monthパラメータがなければ、今月の日記を読み込んで、Atomフィードを配信します。
まず、Encodeモジュール、Atomフィードの対象となるWeb日記の構造と出力するAtomフィードの内容を簡単に紹介してから、詳細の説明に入りましょう。
Encodeモジュール
日記HTMLはシフトJIS文字コードで書かれています。Perl 5.8ではこれをUTF-8に変換して取り扱います。最終的に出力するAtomフィードもUTF-8がデフォルトですから合理的な選択と言えます。日記HTMLの各行を@contentに格納し、行単位で順に処理をするforeachループの最初に、from_to($line,'cp932','utf8');の行があります。Encodeモジュールのfrom_to()関数のcp932は、変換前の文字コードを指定しています。これを、shiftjisとしないのは、例えばローマ数字の「Ⅱ」などが文字化けするからです。マイクロソフト社のWindows上の文字コードは正規のシフトJISを拡張としたものになっているために、Windows上で表示できる文字をすべて変換するためには、cp932とする必要があります。参考資料に、Wikipediaの「Microsoftコードページ932」のページへのリンクを置きましたので、詳細についてはそちらを参照してください。
Web日記の構造
著者のWeb日記は、拙著『実用実践Perl』(2004年)の第4章「インターネット日記のすすめ」に紹介している日記マークアップ言語dmlで、エディタ上で半自動生成していますので、日記記事としての構造部分は定型的な書式になっています。記事の内容部分は自由なHTMLで記述可能ですから、画像やリスト、表、引用などを自在に置け、表現の自由度が高いのが特徴です。文字コードはWindows上で取り扱うのに便利なシフトJISです。日記記事の一つを取り出すと次のとおりです。
<DL> ・・・ <DT>6/6/2006 (Tue.)<DD> <div class="emph"><A HREF="renewal_index.html#perl">[Perl]</A> <A NAME="perl_1149601264">デスクトップCGIのセキュリティ</A></div> <p>デスクトップCGIでは、LANに接続していれば他のマシンからローカルサー バーにアクセスされる危険性があるわけで、それを避けるためには、サーバー へのアクセスをlocalhostだけに許可する設定にすればよいだろうと思う。 取り敢えず、CodeZineの記事と本日記のApacheを使う記事を書き直した。</p> <ul> <li><a href="renewal_2005_07.html#perl_1122128256"> [Perl] 「実践実用Perl」講座(XV)- Apacheを使ってみよう [7/23/2005 (Sat.)]</a> </ul> <p>Anhttpdでは、[オプション一般]→[アクセス制御]→[許可IP]の"*.*.*.*"を "127.0.0.1"に書き直せばよい。実践実用Perlサポートでもアナウンスしておこう。</p> <div class="updated">更新: 2006-06-07T21:38:35+09:00</div> ・・・ </DL>
Firefoxのブックマークと同様に日記記事は、DL開始タグとDL終了タグで囲まれています。日記の日付の表示は、DTタグとDDタグで日付曜日を挟んで改行することによって行います。記事の開始は、emphクラス属性のdivタグの囲みに、インデックスファイルへのリンクを持つカテゴリ名と、記事を特定するNAME属性を持つアンカータグでマークした記事タイトルが配置されています。次に現れるemphクラス属性を持つdivタグ行(タイトル行)か、日付を表示するDTとDDタグのセットの行(日付行)の前までが、日記記事内容になります。日付の後には、複数の記事を置くことが許されているからです。
記事を特定するアンカータグのNAME属性の値は、カテゴリに対応するキー文字列と記事生成時点の1970年1月1日UTCからの秒数をアンダーラインでつないでいます。記事生成時点の時間を正確に求めようと思えば、この秒数から計算することができます。
配信するAtomフィード
次のようなAtomフィードを配信します。entryコンテナ要素の内容は、前項の日記記事に対応しています。出力している要素については、Atomフィードの要素・属性一覧表と対応させて確認してください。
<?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom"> <title>日曜プログラマのひとりごと</title> <subtitle>TS Networkのために - 更新日記</subtitle> <updated>2006-06-24T07:54:29+09:00</updated> <id>http://homepage1.nifty.com/kazuf/renewal.html#perl_1149601264</id> <id>http://homepage1.nifty.com/kazuf/renewal_atom.xml</id> <link rel="alternate" type="text/html" hreflang="ja" href="http://homepage1.nifty.com/kazuf/renewal.html"/> <logo>http://homepage1.nifty.com/kazuf/png/tsnetworkc3.png</logo> <rights>Copyright (C) 2006 Kazuo Fujioka</rights> <generator uri="http://homepage1.nifty.com/kazuf/renewal.html" version="1.0"> rn2atom.cgi </generator> ・・・ <entry> <title>デスクトップCGIのセキュリティ</title> <link rel="alternate" type="text/html" href="http://homepage1.nifty.com/kazuf/renewal.html#perl_1149601264"/> <id>http://homepage1.nifty.com/kazuf/renewal.html#perl_1149601264</id> <published>2006-06-06T22:41:04+09:00</published> <updated>2006-06-07T21:38:35+09:00</updated> <category term="Perl" /> <author> <name>Kazuo Fujioka</name> </author> <content type="xhtml" xml:lang="ja" xml:base="http://homepage1.nifty.com/kazuf/renewal.html"> <div xmlns="http://www.w3.org/1999/xhtml"> <p>デスクトップCGIでは、LANに接続していれば他のマシンから ローカルサーバーにアクセスされる危険性があるわけで、それを避けるため には、サーバーへのアクセスをlocalhostだけに許可する設定にすればよい だろうと思う。取り敢えず、CodeZineの記事と本日記のApacheを使う記事を 書き直した。</p> <ul> <li><a href= "http://homepage1.nifty.com/kazuf/renewal_2005_07.html#perl_1122128256"> [Perl] 「実践実用Perl」講座(ⅩⅤ)- Apacheを使ってみよう [7/23/2005 (Sat.)]</a></li> </ul> <p>Anhttpdでは、[オプション一般]→[アクセス制御]→[許可IP]の"*.*.*.*"を "127.0.0.1"に書き直せばよい。実践実用Perlサポートでもアナウンスしておこう。</p> <div class="updated">更新: 2006-06-07T21:38:35+09:00</div> </div> </content> </entry> ・・・ </feed>
Web日記の構造をパターンマッチで読む
スクリプトのWeb日記の内容を行単位で読み込んだ@content配列を処理するforeachループの説明に入りましょう。Web日記の内容を正規表現のパターンマッチで読み取り、解析して、Atomフィードの該当要素に格納する部分です。日記の該当部分と対応する、スクリプトの正規表現によるパターンマッチの部分をまず示して、説明していきます。
<DL>
if($line =~ /^<DL>/i){ $sw = 1;$contsw = 0;next; }
DL開始タグにマッチすると、日記の開始部であると判定し、日記に入ったことを示すフラグ(旗)$swを1とします。foreachループが日記記事の内容に入ったことを示すフラグ$contswは0と明示しています。nextで次のループに移ります。
<DT>6/6/2006 (Tue.)<DD>
if($line =~ /^<DT>.+<DD>.*$/i){ if($contsw == 1){ unless($updated){ $updated = $published; } $content =~ s/(href|src)="(?:(?!http:))(.+?)"/$1="$baseurl\/$2"/gi; $items{$url . $name} = join("@@@",$published,$updated,$category,$title,$content); $published = "";$updated = "";$category = "";$title = "";$content = "";$contsw = 0; } next;
記事内容の日付表示部では、$contsw == 1の場合だけ、まず、content要素中のアンカータグのhref属性とsrc属性の相対URLを絶対URLに置換します。次に、記事を特定するアンカータグのNAME属性値の絶対URLをキーにした連想配列に、published要素(作成日時)、updated要素(更新日時)、category要素(カテゴリ)、title要素(タイトル)とcontent要素(記事内容)を@@@文字列で結合(join)して格納します。さらに、各要素を格納していた変数をそれぞれ空にします。$contswの値に関係なく、nextで次のループに移ります。
join関数で結合する各要素を区切る文字列には、\tのタブを使うのが普通ですが、$content要素には、タブを含む可能性が非常に高いので、まず有り得ない文字列として@@@を使います。しかし、例えば本稿を日記の記事にすると、有り得ない文字列が含まれる記事ができてしまいますから、実はいたちごっこになります。テキスト処理においては、テキストに含まれない文字列は存在しないと考えて、問題が起きうる可能性に、常に注意を払う必要があります。
<div class="emph"><A HREF="renewal_index.html#perl">[Perl]</A> <A NAME="perl_1149601264">デスクトップCGIのセキュリティ</A></div>
}elsif($line =~ /^<div class="[^"]+"><A HREF="[^"]+">\[([^]]+)\] <\/A> <A NAME="([^"]+)">([^<]*)(?:<A HREF="[^"]+">)*([^<]+)(?:<\/A>)* ([^<]*)<\/A><\/div>/i){ $tcategory = $1;$tname = "#" . $2;$ttitle = $3.$4.$5; ($time = $2) =~ s/^[a-z]+_(\d+)$/$1/;$tpublished = &date($time); if($contsw == 1){ unless($updated){ $updated = $published; } $items{$url . $name} = join("@@@",$published,$updated,$category,$title,$content); $updated = "";$category = "";$title = "";$content = ""; }else{ $content = "";$contsw = 1; } $category = $tcategory;$name = $tname;$title = $ttitle;$published = $tpublished; next;
日記記事の記事カテゴリ・タイトル部のサンプルに含まれるタイトルは単なる通常のテキストですが、「日曜プログラマのひとりごと」の記事のタイトルには、ハイパーリンクを含む場合があります。パターンマッチの正規表現には、タイトル中に一つだけハイパーリンクが含まれていいように書いてあります。また、「日曜プログラマのひとりごと」のカテゴリ・タイトル行を示すdivタグのclass属性値はemphですが、他の属性値に変更してもマッチするように書いてあります。本パターンマッチの後方参照で、category要素を$tcategoryに、NAME属性値を$tnameに、title要素を$ttilteに仮に格納します。
次に、NAME属性値は、カテゴリのキー文字列と記事の生成日時の1970年1月1日UTCからの秒数がアンダーラインで結合されている値だということを思い出しましょう。NAME属性値から、秒数日付部分をパターンマッチで取り出し、$timeに代入し、さらにdateサブルーチンで生成日時文字列に変換して、$publishedに代入します。
$contsw == 1の場合には、一つ前の記事のentryコンテナ要素を格納し、各子要素を初期化します。$contsw != 1の場合には、$contentを初期化し、$contsw = 1とします。前の記事を処理した後に、仮に格納しておいた次の記事のentryコンテナ要素を本来の変数に格納します。続いて、nextで次のループに移ります。
</DL>
}elsif($line =~ /^<\/DL>/i){
if($contsw == 1){
unless($updated){
$updated = $published;
}
$items{$url . $name} = join("@@@",$published,$updated,$category,$title,$content);
$published = "";$updated = "";$category = "";$title = "";$content = "";
}
last;
}
日記の終了部にマッチすると、$constsw == 1であれば、最後の記事のentryコンテナ要素を連想配列に格納して、各子要素の変数を初期化し、最後にforeachループから抜けます。
