SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

PerlでデスクトップCGI

デスクトップCGIでWebとデスクトップを融合する 第2回

Web日記のAtom配信


Atomフィード配信デスクトップCGI - 「rn2atom.cgi」(2/2)

日記記事内容をcontent要素に変換する

 foreachループの次の部分は、@contentに格納したWeb日記(HTML)の日記記事内容に該当する各行をXHTMLに変換しながら、$contentに格納していきます。HTMLの内容部分をそのままAtomフィードのようなXMLの中に取り込むとXMLパースエラーになりますから、XMLとして整形式になるように変換します。

content 要素に該当する日記の各行を処理するスクリプト部分
if($contsw == 1 && $line ne ""){
    if($line =~ /^<div class="updated">更新: (.[^<]+)<\/div>$/i){
        $updated = $1;
    }
    $line =~ s/&(?![#a-z0-9]{2,6};)/&/gi;
    $line =~ s/Ç/&#199;/g;
    $line =~ s/É/&#203;/g;
    $line =~ s/é/&#233;/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に変換するための注意事項をリストアップしておきます。

  1. &[#a-z0-9]{2,6};にマッチしない&、すなわち実体参照に含まれない&だけ、&に変換します。例えば、URLに含まれるCGIのパラメータを区切る&が整形式エラーになります。
  2. 文字表記実体参照はXMLでは整形式エラーになりますから、番号表記実体参照に変換しておきます。
  3. タグの要素タイプ名を小文字に変換します。
  4. タグの属性名を小文字に変換します。
  5. 空要素タグを/>で閉じます。
  6. <li>など、終了タグ</li>を通常使わないものは付け加えます。
  7. 相対URLは絶対URLに変換しておきます(これは整形式とは関係ありません)。

 本CGIは、Web日記(HTML)の解析結果を元に、Atomフィードを出力すると共に、Atomフィード配信ガイド画面用CGI出力を行います。これについては説明は不要でしょう。スクリプトの全体を示します。

rn2atom.cgi: 「日曜プログラマのひとりごと」の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/Ç/&#199;/g;
            $line =~ s/É/&#203;/g;
            $line =~ s/é/&#233;/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で実現できるでしょう。

start.cgi: ファイル関連付けでアプリケーションを起動する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パースして表示しています。

「rn2atom.cgi」出力画面
「rn2atom.cgi」出力画面
「start.cgi」実行画面
「start.cgi」実行画面

まとめ

 本稿では、著者の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と区別して明確に定義できたと思います。ありがとうございました。

参考資料

  1. デスクトップCGIでWebとデスクトップを融合する 第1回
  2. SPIDERING HACKS ウェブ情報ラクラク取得テクニック 101選』 Kevin Hemenway、Tara Calishain 著、村上雅章 訳、オライリー・ジャパン、2004年
  3. ActivePerl
  4. @IT:クロスサイトスクリプティング対策の基本
  5. The Atom Syndication Format
  6. ToClip for Windows
  7. Welcome! - The Apache Software Foundation
  8. Apache HTTP サーバ バージョン 2.0 ドキュメント - Apache HTTP サーバ
  9. Firefox - Web の再発見
  10. Internationalized Resource Identifiers (IRIs)
  11. Uniform Resource Identifier (URI): Generic Syntax
  12. An Introduction to Multilingual Web Addresses
  13. Microsoftコードページ932 - Wikipedia
  14. LWP::Simple - simple procedural interface to LWP - search.cpan.org
  15. Net::FTP - FTP Client class - search.cpan.org
  16. 著者サイト「日曜プログラマのひとりごと」
  17. Automated Web Awakening Knowledge, Information and News Gathering Project
修正履歴

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
PerlでデスクトップCGI連載記事一覧

もっと読む

この記事の著者

jscripter(ジェイスクリプター)

NIFTY SERVEのFGALEL(エントリーラボ)、FGALTS(テキスト&スクリプト)に参画し、FGALTSでシスオペを務める。現在、TSNETメンバー。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/450 2006/10/26 19:11

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー