Atomフィード配信デスクトップCGI - 「rn2atom.cgi」(2/2)
日記記事内容をcontent要素に変換する
foreachループの次の部分は、@contentに格納したWeb日記(HTML)の日記記事内容に該当する各行をXHTMLに変換しながら、$contentに格納していきます。HTMLの内容部分をそのままAtomフィードのようなXMLの中に取り込むとXMLパースエラーになりますから、XMLとして整形式になるように変換します。
if($contsw == 1 && $line ne ""){ if($line =~ /^<div class="updated">更新: (.[^<]+)<\/div>$/i){ $updated = $1; } $line =~ s/&(?![#a-z0-9]{2,6};)/&/gi; $line =~ s/Ç/Ç/g; $line =~ s/É/Ë/g; $line =~ s/é/é/g; $line =~ s/<(\/?)(\w+)/<$1\L$2\E/g; #要素タイプ名を小文字に $line =~ s/ (\w+)="/ \L$1\E="/g; #属性名を小文字に $line =~ s/(ALIGN|HSPACE|CLEAR)=([^" >]+)/\L$1="$2"\E/g; #空要素タグを /> で閉じる $line =~ s/<(br|hr|img|link|input|col|base|meta|area|param)([^>]*?[^>\/])>/<$1$2 \/>/g; $line =~ s/<(br|hr)>/<$1$2 \/>/g; $line =~ s/(<li>.+)$/$1<\/li>/; $line =~ s/(href|src)="(?:(?!http:|#))(.+?)"/$1="$baseurl\/$2"/gi; $line =~ s/href="(\#.+?)"/href="$baseurl\/$gethtml$1"/gi; $line =~ s/<a name="(.+?)"/<a name="$1" id="$1"/g; #id属性を加える $content .= $line . "\n"; }
$contsw == 1であり、かつ行の内容$lineが空でなければ、処理を行います。最近の「日曜プログラマのひとりごと」のWeb日記では、記事更新時に、divタグのupdatedクラス属性でマークして、更新日時をAtom配信フォーマットのDateコンストラクトの形式で記録することにしています。この行にパターンマッチすれば、updated要素を取得します。
HTMLをXMLに変換するための注意事項をリストアップしておきます。
&[#a-z0-9]{2,6};にマッチしない&、すなわち実体参照に含まれない&だけ、&に変換します。例えば、URLに含まれるCGIのパラメータを区切る&が整形式エラーになります。- 文字表記実体参照はXMLでは整形式エラーになりますから、番号表記実体参照に変換しておきます。
- タグの要素タイプ名を小文字に変換します。
- タグの属性名を小文字に変換します。
- 空要素タグを
/>で閉じます。 <li>など、終了タグ</li>を通常使わないものは付け加えます。- 相対URLは絶対URLに変換しておきます(これは整形式とは関係ありません)。
本CGIは、Web日記(HTML)の解析結果を元に、Atomフィードを出力すると共に、Atomフィード配信ガイド画面用CGI出力を行います。これについては説明は不要でしょう。スクリプトの全体を示します。
#!C:/Perl5.8/bin/perl.exe use strict; use warnings; use LWP::Simple; use Net::FTP; use Encode qw(from_to); use CGI qw(:cgi); my $cgidir = $ENV{'CGIDIR'};# 環境変数からCGIディレクトリの取得(/cgi-bin/codezine など) # FTP配信設定 my $ftpsite = 'ftp.your.com'; # FTPホスト my $ftpaccount = 'ftpaccount'; # FTPアカウント my $ftppassword = 'password'; # FTPパスワード my $ftpsw = 0; # スクリプトでFTP配信を行う # デスクトップ設定 my $gethtml = 'renewal.html'; # 解析対象のHTMLファイル名 my $outfile = 'renewal_atom.xml'; # Atomフィードのファイル名 my $atompath = "C:\\anhttpd\\cgi-bin\\codezine";# Atomフィードの作成されるデスクトップパス my $dir = "C:\\sites\\tsnetwork"; # 解析対象のHTMLファイルの存在するデスクトップディレクトリ my $desksw = 0; # デスクトップのHTMLを解析して、Atomフィードを送信する場合は、→ 1 # ホームページ設定 my $userdir = '/homepage'; # ホームページルートディレクトリ my $baseurl = "http://homepage1.nifty.com/kazuf"; # ホームページのベースURL my $imgurl = "png/tsnetworkc3.png"; # ホームページの画像ファイル # feed コンテナ要素の設定 my $feedtitle = "日曜プログラマのひとりごと"; # title 要素 my $feedsubtitle = "TS Networkのために - 更新日記"; # subtitle 要素 my $feedrights = "Copyright (C) 2006 Kazuo Fujioka"; # rights 要素 my $generator = "rn2atom.cgi"; # generator 要素 # generator 要素の uri 属性値 my $generatoruri = "http://homepage1.nifty.com/kazuf/renewal.html"; my $generatorversion = "1.0"; # generator 要素の version 属性値 my $author = "Kazuo Fujioka"; # author 要素の name 要素 # CGI パラメータの取得 if(param('month')){ my $p_year = param('year'); my $p_month = parma('month'); $gethtml =~ s/\.html$/_${p_year}_${p_month}.html/; ($outfile = $gethtml) =~ s/\.html/_atom\.xml/; } my $url = "$baseurl/$gethtml"; my $atom = "$baseurl/$outfile"; my $date = &date(time);# feed コンテナ要素の updated 要素(Atomフィード作成日時) my $logo; # feed コンテナ要素の logo 要素 # Atomフィード配信ガイド画面用CGI出力開始 print <<HTML; Content-type: text/html; charset=UTF-8 <html> <head> <title>${feedtitle}Atom配信</title> <meta http-equiv="content-type" content="text/html;charset=UTF-8"/> <link rel="stylesheet" type="text/css" href="/mystyle.css"/> </head> <body> <div class="emph">${feedtitle}Atom配信: $date</div> <p>$gethtmlのAtomを$outfileとして配信開始します。</p> HTML # entry コンテナ要素(日記記事)取得 my @content; # Web日記HTMLを行単位で格納する配列 my $line; # 日記行を foreach ループで格納する変数 my $sw = 0; # 日記部分に入ったことを知らせるフラグ my $contsw = 0; # 日記の各記事内容に入ったことを知らせるフラグ my %items = (); # 日記記事の要素結合文字列を、記事を特定する絶対URLをキーに格納する連想配列 my $published = ""; # published 要素 my $updated = ""; # updated 要素 my $category = ""; # category 要素 my $name = ""; # アンカータグの NAME 属性値 my $title = ""; # title 要素 my $content = ""; # content 要素 my $tcategory; # 一時的に category 要素を格納 my $tname ; # 一時的にアンカータグの NAME 属性値を格納 my $ttitle; # 一時的に title 要素を格納 my $tpublished;# 一時的に published 要素を格納 my $time; # NAME 属性値から、記事生成日時秒数を取り出すための変数 # Web日記HTMLを行単位で格納 if($desksw == 1){ open(IN, "$dir\\$gethtml") or die "Can't open file!: $!"; chomp(@content = <IN>);# デスクトップのファイルから、Web日記HTMLを読み込む close(IN); }else{ @content = split(/\n+/, (get $url));# WebからWeb日記HTMLを読み込む } # Web日記HTMLを行単位で解析 foreach $line (@content){ from_to($line,'cp932','utf8'); if($line =~ /^<DL>/i){ $sw = 1;$contsw = 0;next; } if($sw == 1){ 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; }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/;$published = &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; }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; } if($contsw == 1 && $line ne ""){ if($line =~ /^<div class="updated">更新: (.[^<]+)<\/div>$/i){ $updated = $1; } $line =~ s/&(?![#a-z0-9]{2,6};)/&/gi; $line =~ s/Ç/Ç/g; $line =~ s/É/Ë/g; $line =~ s/é/é/g; $line =~ s/<(\/?)(\w+)/<$1\L$2\E/g; #要素タイプ名を小文字に $line =~ s/ (\w+)="/ \L$1\E="/g; # 属性名を小文字に $line =~ s/(ALIGN|HSPACE|CLEAR)=([^" >]+)/\L$1="$2"\E/g; #空要素タグを /> で閉じる $line =~ s/<(br|hr|img|link|input|col|base|meta|area|param)([^>]*?[^>\/])>/<$1$2 \/>/g; $line =~ s/<(br|hr)>/<$1$2 \/>/g; $line =~ s/(<li>.+)$/$1<\/li>/; $line =~ s/(href|src)="(?:(?!http:|#))(.+?)"/$1="$baseurl\/$2"/gi; $line =~ s/href="(\#.+?)"/href="$baseurl\/$gethtml$1"/gi; $line =~ s/<a name="(.+?)"/<a name="$1" id="$1"/g; #id 属性を加える $content .= $line . "\n"; } } } # Atomフィード feed コンテナ要素出力 open(OUT, ">", $outfile); my $header = <<HERE1; <?xml version=\"1.0\" encoding=\"utf-8\"?> <feed xmlns="http://www.w3.org/2005/Atom"> <title>$feedtitle</title> <subtitle>$feedsubtitle</subtitle> <updated>$date</updated> <link rel="self" type="application/atom+xml" href="$atom"/> <link rel="alternate" type="text/html" hreflang="ja" href="$url"/> <logo>$baseurl/$imgurl</logo> <rights>$feedrights</rights> <generator uri="$generatoruri" version="$generatorversion"> $generator </generator> HERE1 print OUT $header; # Atomフィード entry コンテナ要素出力 foreach (sort {$items{$b} cmp $items{$a}} keys %items){ ($published,$updated,$category,$title,$content) = split(/@@@/,$items{$_}); my $item = <<ITEM; <entry> <title>$title</title> <link rel="alternate" type="text/html" href="$_"/> <id>$_</id> <published>$published</published> <updated>$updated</updated> <category term="$category" /> <author> <name>$author</name> </author> <content type="xhtml" xml:lang="ja" xml:base="$baseurl/$gethtml"> <div xmlns="http://www.w3.org/1999/xhtml"> $content </div> </content> </entry> ITEM print OUT $item; } print OUT "</feed>\n"; close(OUT); # AtomフィードをFTPで配信しつつ、配信ガイド画面CGI出力を完了する if($ftpsw == 1){ print "FTPで$ftpsiteに接続します。<br />"; my $ftp = Net::FTP->new($ftpsite); print "ログインします。<br />"; $ftp->login($ftpaccount, $ftppassword); print "$userdirに移動します。<br />"; $ftp->cwd($userdir); print "アスキーモードに変更します。<br />"; $ftp->ascii(); print "$outfileを送ります。<br />"; $ftp->put($outfile, $outfile); print "終了します。<br />"; $ftp->quit(); print <<HTML2; <p>$gethtmlのAtomを$outfileとして配信しました。</p> <p><a href="$atom">配信Atomチェック</a></p> </body> </html> HTML2 }else{# デスクトップで配信用ファイルをチェックする my $url_encoding = &url_encode("$atompath\\$outfile"); print "<p>$gethtmlのAtomを$outfileとして作成しました。</p>\n"; print "<p><a href=\"$cgidir/start.cgi?file=$url_encoding\">配信Atomチェック</a></p>\n"; print "</body>\n</html>\n"; } # サブルーチン # JST(日本標準時)のDateコンストラクトを生成するサブルーチン sub date{ my($time) = @_; my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($time); $mon += 1; $year += 1900; return sprintf("%4d-%1.2d-%1.2dT%1.2d:%1.2d:%1.2d+09:00",$year,$mon,$mday,$hour,$min,$sec); } # URL エンコーディング sub url_encode{ my($str) = @_; $str =~ s/([^a-z0-9\-_.!*'\(\)~ ])/sprintf("%%%02X", ord($1))/egi; $str =~ s/ /+/g; return $str; }
ファイル関連付けアプリケーション起動用デスクトップCGI - 「start.cgi」
拙著『実践実用Perl』では、URLにfileスキームを使う方法でデスクトップのテキストファイルや画像ファイルなどのデータファイルを起動していたのですが、Windows XPのSP2からは、fileスキームが使えなくなりました。これに替わる方法として考えたのが、system関数から、startを使ってファイル関連付けアプリケーションを起動する方法です。今回は単に、Atomフィードをファイル関連付けで起動して出力結果を確認するために用いるだけですが、工夫すれば、一種のファイラーをデスクトップCGIで実現できるでしょう。
#!/Perl5.8/bin/perl.exe use strict; use warnings; use CGI qw(:cgi); my $file; # CGI パラメータの取得 if(param('file')){ $file = param('file');# フルパスで関連付けで起動するファイル名を渡す } print "Content-type: text/html; charset=UTF-8\n\n"; print "<html><body><p>$fileを関連付けで起動します。</p></body></html>\n"; system("start $file");
最後に「rn2atom.cgi」の出力画面と「rn2atom.cgi」出力から「start.cgi」を実行した画面を示しておきます。「start.cgi」実行画面では、FirefoxがCGI出力を表示し、「start.cgi」のXMLファイル関連付けで起動されたインターネットエクスプローラが、Atomフィード「renewal_atom.xml」をXMLパースして表示しています。
まとめ
本稿では、著者のWeb日記を題材にして、Atomフィードを配信するデスクトップCGIを作成しました。定型的な書式を持つテキストファイルからパターンマッチで必要な部分を取り出し、新しい構造を持つテキストを生成する基本的な方法になります。また、文字コードの異なるテキストから別の文字コードに変換して取り出す例としても見ることができます。さらには、HTMLをXMLに変換する例でもあります。Perlでデスクトップにあるファイルを変換することも可能ですし、Webにあるファイルもデスクトップ同様、簡単に扱えることも分かったと思います。その他、スクリプトでファイルを自動生成すると同時に、FTPプロトコルで配信する方法についても説明しました。
また、デスクトップCGIから、Windowsのファイル関連付けによってアプリケーションが起動できることも示しました。
それでは、本稿のデスクトップCGIを起動してみましょう。CGIを「/cgi-bin/codezine」に置くとすれば、「http://localhost/cgi-bin/codezine/rn2atom.cgi」で起動できます。
次回は、Web日記のHTMLにメタ情報をmicroformatsで埋め込み、HTMLとXMLの間を自在に行き来する方法を考え、次々回では、リレーショナルデータベースにRSS/Atomフィードを蓄積して再利用する方法を考える予定です。
謝辞
TSNETのPerlの部屋で、CGIのセキュリティの問題についてご指摘いただいたBruce.氏、Apacheサーバーの設定の問題についてご指摘いただいたり、著者のAtomフィード配信の問題についてメールをいただき、深夜までTSNETで議論していただいたZazel氏に感謝いたします。デスクトップCGIを初めて、一般のCGIと区別して明確に定義できたと思います。ありがとうございました。
参考資料
- デスクトップCGIでWebとデスクトップを融合する 第1回
- 『SPIDERING HACKS ウェブ情報ラクラク取得テクニック 101選』 Kevin Hemenway、Tara Calishain 著、村上雅章 訳、オライリー・ジャパン、2004年
- ActivePerl
- @IT:クロスサイトスクリプティング対策の基本
- The Atom Syndication Format
- ToClip for Windows
- Welcome! - The Apache Software Foundation
- Apache HTTP サーバ バージョン 2.0 ドキュメント - Apache HTTP サーバ
- Firefox - Web の再発見
- Internationalized Resource Identifiers (IRIs)
- Uniform Resource Identifier (URI): Generic Syntax
- An Introduction to Multilingual Web Addresses
- Microsoftコードページ932 - Wikipedia
- LWP::Simple - simple procedural interface to LWP - search.cpan.org
- Net::FTP - FTP Client class - search.cpan.org
- 著者サイト「日曜プログラマのひとりごと」
- Automated Web Awakening Knowledge, Information and News Gathering Project


