Last updated on September 29, 2005 (THU) 06:54 (このページには既に歴史的な意味しかありません).
ここでは,書評掲示板の開発を中心として,CGIの開発について簡単に説明する。何かの役に立てば幸いである。なお,1999年10月20日より,日記編集・生成スクリプトの開発についても書くことにしたので,タイトルを変更した。2001年2月より,ここがLycos Japanのディレクトリサービスからリンクされたこともあり,sendmailなどのMTAの代わりにPerlのモジュールを使ったフォームメールCGIの記載を追加した。
上記ダウンロード対象ファイルは,拡張子tgzのものはtar+gzip圧縮,zipのものはzip圧縮されており,どちらもフリーソフトの+Lhacaなどで展開することができる。
実行可能な環境であれば(つまりhttpdの設定がCGIを不許可にしていないならば),CGIをおくべきディレクトリに実行可能なファイル(シェルスクリプトでもいいし,Perlのスクリプトでもいいし,Cなどでコンパイルしたファイルでもいい)を置けば,そのファイルはブラウザから呼び出して実行させることができる。実行結果として標準出力に出てきたものがブラウザに表示されるのだが,それがHTMLファイルであることをブラウザにわからせるためには,出力の最初を
Content-type: text/html
として,その後一行の空行を置かなくてはならない(Perlなら"Content-type: text/html\n\n"をプリントする)。後は,普通に<!DOCTYPE HTML...ではじまるHTMLファイルを出力すればよい。
なお,HTMLを直接標準出力するのではなくて,いったんファイルに書き出してから表示させるという場合もある。このやりかたのメリットは,掲示板を見るだけの人はCGIを動かす必要がないということであり,サーバへの負荷軽減が期待される。この場合,HTMLファイルは,あらかじめtouchなどで空ファイルを作り,属性を666にしておく必要がある(CGIをCなどで書いてsetuidするなど,特殊な場合は例外)ことに注意する必要がある。もちろん,Content-type: text/htmlと空行は不要で,いきなり<!DOCTYPE HTML...から書けばよい。HTMLファイルが更新されたあとで,すぐそれを表示したい場合は,Content-type: text/htmlから始まるもう一つのHTMLを標準出力してリロードさせる方法と,Location: に続けてHTMLファイルのURLを標準出力する方法があるが,「入力ありがとうございました」などのメッセージを表示しなくてもよいならば,後者の方が圧倒的に手軽である。
unix系OS+apacheをhttpサーバとして使う場合,CGIとして指定されたファイルは,実行属性ビットが立っていないと実行できないので,例えばchmod 755 program.cgiなどとして(実効ユーザがnobodyでも実行できるようにして)おく。直接アンカータグやフレームのソースでCGIを呼び出す場合は,CGIファイル名の後に?に続けて書いた文字列が,環境変数QUERY_STRINGを経由してCGIに渡される(FORMでMETHOD=GETの場合と同じである)が,FORMのACTIONに指定した場合は,METHODをGETの他,POSTにも指定することができる。
POSTの場合は,FORMの中でNAMEで指定した文字列が,&で区切られて,標準入力としてCGIに渡される。渡された文字列の長さは環境変数CONTENT_LENGTHにセットされているので,その長さだけ読むようにする。例えばPerlなら,
read(STDIN,$buffer,$ENV{'CONTENT_LENGTH'});
とする。POSTとGETは両立可能である。つまり,FORMのACTIONでCGIを呼び出すときに?に続けて文字列を書いて,かつMETHOD=POSTにしておけば,?に続けて書いた文字列はQUERY_STRINGに入るし,FORM内で指定した文字列は標準入力として渡される。このことを利用すればパラメータによって動作が異なる,マルチファンクションのCGIを作成することができる。CGIプログラム自体は,このことを踏まえて作成すれば,unixのシェル上で動作する普通のプログラムと変わりないので,デバッグもコマンドラインでできる。GETで渡したい文字列は,実行前にsetenv QUERY_STRING action=registerなどとしておけばhttpdに呼び出させたときと同様に動作する。
もう一つ注意すべきことは,日本語文字列の処理である。ブラウザから投入される文字列は,普通エンコード(URIエスケープ)されているので,それをデコード(URIアンエスケープ)した上で,eucに変換しなくてはならない。サーバがNTだったりすると,sjisに変換したりユニコードに変換したりする必要がある場合もあるかもしれないが,普通はunix系と思うので,eucで処理すればよい筈である。最終的に表示するHTMLファイルの文字コードをeuc-jp以外,例えばiso-2022-jpにしたいなら,出力直前に,文字列として全部まとめてから,&jcode'convert(*buf,'jis')などとすればよい。この作業には,Perlのライブラリであるjcode.plが必須であり,jcode.plをどこかのディレクトリ(普通はcgiと同じディレクトリか,/usr/local/lib/perlと思う)に置いておき,cgi内でそれをrequireしておく。URIアンエスケープするには,次の2点を行う。
一般の掲示板に比べて,書評に特化した掲示板が備えるべき条件としては,以下のものが考えられる。
これを可能にするためにはいろいろな困難があるのだが,まあ,ひとつひとつ片づけていくことにしよう。今のところ上から3つは概ね実現できていると思うがまだまだ改善の余地は大きい(右はver1.6のスクリーンショット)。
困ったことに,これまでロックを設定していなかったのだ。偶々アクセスが少なかったのが幸いしていたのだろう,クラッシュしなかったが,もし同時アクセスがあったらと思うとぞっとする。
設定は常道のsymlinkを使う。つまり,
sub lock { local($retry) = 6; while (!symlink(".",$lockfile)) { if (--$retry <= 0) { &error("BUSY!","他の誰かが書き込み中です。(Anyone else is writing now.)"); } sleep(5);}} sub unlock { unlink($lockfile); }
を定義して,ファイル書き込み部分を&lock;と&unlock;で挟むわけだ。たいていどんな掲示板でもこうしているようだ。$lockfileは,/tmp/_tmp.datに設定した。
これまでも,筑波大学の米澤さんをはじめ,何人もの方から,読むだけのためにcgiを起動するのは億劫であるという指摘をいただいてきたが,1ファイルから関連コメントだけを抜き出すにはcgiが必須であるという理由から対応しないできた。
しかし,ふと気がついた。逆に考えれば,コメントと書き込み以外ではcgiは要らないではないか。インデックスだけみて更新がなければ帰ることもできるのだ。その方がサーバへの負荷も軽い。
必要なことは,新規に本を追加するときにインデックスファイルに書き込むのと同時にhtmlファイルも作ってしまうことと,フレームの親ファイルからの参照名を変更することだけだ。
新規追加時のクッキーの扱いが問題だが,こうしよう。初めに呼ばれる「新規」のフォームはhtmlにしておく。もし自分のクッキーを使いたかったら,インデックスの方の「新規追加」を一度クリックしてもらう。つまり,book_bbs.cgiにもadd_newというサブルーチンは残しておいて,インデックスから呼ばれるところはそこを呼ぶようにする,ということだ。ヘッダに明記しておけば良かろう。
そう思って作成したが,書き込み時に問題があることがわかった。つまり,htmlからの書き込みでは,許可されないのである。説明書きが長くなったことでもあるし,最初に呼び出すファイルとして説明書きを独立させよう。そうすると,ヘッダファイルを常時表示させておく意味もないので,フレームを2つに減らした。これでrevision 1.4の完成である。
これまで,使い方のことばかり考えて著作権の断り書きをしなかったのは,大いに手落ちだったと思うので,「投稿内容の著作権は各投稿者に属しますので,投稿者及び掲示板設置者に無断での転載・引用はお断りします」と明示することにした。これではbk_head.htmlだけの更新になってしまうので,ついでにcgiの方もrevision表示のところをこのページへのリンクにした。プログラム内容は少しも変わっていないのだが更新には違いないので,revisionを1.5とした。
自分でコメントを追加したり,新しい本を追加したときに,本の数が増えてきたので,いちいち探すのが面倒になってきた。これを改善するには,インデックスファイルを作るときに更新されたインデックスを一番上にもってくるだけでよい。
まず思いついたのは,一回ごとにsort()を使って並べ替えてやることだが,これは処理が重くなることが容易に予想されるので却下した。次に思いついたのは,更新されたものを別の変数として保存し,それ以外のレコードはそのまま新しい配列にpush()しておいて,それを一回reverse()してからpush()で最後に付け加え,再びreverse()して書き込むことだが,これもどうも冗長である。考えてみれば何も一つのリストにしてから書き込む必要はないので,別の変数として保存したものを先に書き込み,次に新しい配列を書き込むだけで良さそうだということに気づいた。これまでにたまっているデータの並べ替えは手動でやるしかないが,まあインデックスだけだから大したことはなかろう。というわけで,revision 1.6となった。
長い間revision 1.6には満足して使ってきたが,登録された本の数が増えるにつれて全インデックスを表示するための待ち時間が鬱陶しくなってきた。とある方からご指摘いただいたこともあり,最新10件のみデフォルト表示して,後は別のリンクを呼ぶように変更することを思い立った。
しかし,そうした場合に問題になるのは,以前に登録した本へのアクセスの悪さである。現在の状態ならリストをみてクリック一発でよいが,上で考えたように変更すると最低でも3アクション必要になる。両者を天秤にかけた場合,うまい妥協案はないだろうかと悩むに至ったわけだ。
暫定的な解決策を2つ思いついたので実行しようというのが,revision 1.7である。具体的に書くと,
の2つである。こうして文章にできたということはアルゴリズムができたこととほぼ同値なので,コーディングは面倒くさいけれど論理的には簡単である。
まず$maindirと$mainhtmlという設定を新たに加える。$indexhtmlをファイル名だけにして$maindir配下に置くようにする。次に検索のため$FORM{'action'}での条件分岐を3つ増やす。それからsub writeindexの書き換えである。全インデックス用htmlを出力するのと同時にトップページも出力するようにする。最新10件だけというのは簡単である。Perlには「配列スライス」という機能があり,"@indexlines"という配列の先頭10件だけに操作を加えたいなら,
foreach $lines (@indexlines[0..10]) { [各$linesへの操作] }
のようにすればよい。
というのは真っ赤なウソで,[0..10]では要素は11個になってしまうので,当然[0..9]としなくてはいけない(以上,22日追加)。次は検索だが,これもどうということはなくて,foreachの中で条件にマッチした行だけ標準出力すればよいのだ。このバージョンから旧版と体裁が大違いなので旧版も残しておくことにした。データファイルのフォーマットは共通なのでどちらでも使える筈である。具体的なバージョンアップの手順としては,book_bbs.cgiの必要部分を修正して上書きし,画像ファイルを消してから環境変数QUERY_STRINGにaction=writeindexをセットして1回実行するだけである。
ダウンロードはこちらからできる(なお,最新版はcgiファイル1つだけになったので,gz圧縮ファイルにした。旧版はlzh圧縮のままである)。
ついでに,新規追加と書評表示画面でもスタイルファイルを導入して見やすくして,1.7bとした。(23日追加)
懸案であった,見るだけならcgiを使わないという状態をやっと実現した。ついでに西暦2000年(Y2K)問題にも対応した。Perlでlocaltimeを使うときは西暦から1900を引いた値が$yearに返るので,sprintf("19%02d",$year)ではなくて,$year+=1900としてからsprintf("%04d",$year)とすべきなのである。
変更のポイントはいろいろあるのだが,もっとも重要なことは,$resdirという変数を導入し,$maindir$resdirにコメントファイルをhtml化して放り込んでしまうことである。初回変更時を除けば,コメントをつける時のみ再html化するルーチンを呼べばよい。html化するルーチンをsub writecommentとし,これまでのsub commentから,$maindir$resdir$FORM{'number'}.htmlが存在しないときに呼び出すように設定する。存在すれば,print "Location: $maindir$resdir$FORM{'number'}.html\n\n";として直接htmlを呼べばよい。writeindexが呼ばれるときにも同じ条件判定をして,もしコメントファイルが存在すればフォームの代わりにアンカータグでリンクを張るようにした。
後は$resdirの実体bookresを作ってchmod 0777 bookresとし,1つずつ書評をクリックしていけばよいのだが,面倒なので一気にhtmlを作るサブルーチンrefreshhtmlsを作成した。このルーチンは,ファイルのオーナー属性の都合上,ブラウザからbook_bbs.cgi?action=refreshhtmlsという形で呼び出す必要がある。ファイルの存在チェックをしているので,一度htmlファイルができてしまえばこのルーチンは無効になるのだが,デバッグの際に重宝したので作って損は無かったと思う。最後にbook_bbs.cgi?action=writeindexを実行すれば,バージョンアップは完了である。
(16日追記)スタイルシートが各々のhtmlに分かれているのは,どう考えてもディスクスペースの無駄なので,スタイルファイルとして独立させることにした。スタイルファイルが無かったら自動で作れるようにスクリプトは書いたのだが,これには欠陥があって,スクリプトがnobody権限で実行される以上,$resdirでないディレクトリには勝手にファイルを作ったりできないのだ。仕方なくsetenv QUERY_STRING action=makestyleとしてからシェルでbook_bbs.cgiを実行するとスタイルファイルが生成されるようにし,できたスタイルファイルを$resdirに手動でコピーすることにしたが,この辺のパーミッションに何か抜け穴は無いものだろうか? 変に抜け穴があってファイルを勝手に書き換えられたりしても困るのだが,このスクリプトは信用するとかいう設定ができてもよさそうなものだ。setuidというのがこれかと思ってスクリプトの属性をchmod 6755とかもしてみたのだが,Perlスクリプトに関してはsetuidは効かないようで,apacheのeffective ownerであるnobodyでしか実行できないのである。この問題の解決にはスクリプト自体をCに書き換えるとかいう荒技や,Cのenvelopをかませるとかいう怪しい手段しか残されていないので,一度だけのことだし,シェルでcgiを実行してもらうことにした。このスクリプトのユーザーがいるかどうか知らないが,もしいたらそういうわけなので許されたい(なお,apacheの実効ownerをrootにしてしまうとかいう超荒技もないわけではないが,セキュリティ上お薦めできない)。
今回はバグフィックスである。タイトル中に半角コンマがあった場合に,インデックスとトップページの生成がおかしかったので,データファイルに書き込むときに,半角コンマはすべて全角に変換するようにした。スクリプトの圧縮ファイルは,tarでgzip圧縮したものなので,展開には,tar xvzf book_bbs.tgzとする必要があることに注意されたい。
文章中にURLが出てきたら自動でリンクにするようにした。データファイル上は操作せず,htmlファイルを生成するときに操作する。この関係で,htmlを再生成する必要が生じたので,ついでにeuc-jpでページを作っていたのをやめて,iso-2022-jpにして,HTMLも3.2にした。これは,Lynxは(コンパイルオプションの日本語文字コード設定にはないので誤解していたのだが)デフォルトでiso-2022-jpに対応していることがわかったためと,フレームを使わないことにしたのでHTMLが3.2でも十分だからである。これで,これまで見られなかった人にも見えるようになった筈である。
具体的には,自動リンクは,http://という文字列を検索して,URLを構成する文字列が続く間をURLと判断し,それ全体をアンカータグでくくればよい筈である。つまり,
$buf =~ s/(http:\/\/[\w\/\.\-_\?\=\~]+)/<a href="$1">$1<\/a>/g;
とするだけでよいのではないかと思われる。このステートメントは,新規登録でhtmlを作るルーチン内と,コメント追加でhtmlを作るルーチン内の2カ所に必要である。次に,euc-jpをiso-2022-jpに変えるには,原則として,printしていたのをすべて一旦文字列バッファに貯め,貯まったところでjcodeで変換するだけで良い。これもhtmlの出力だけ制御すればよく,datファイルは従来通りeucにしておく方が,検索などの際に便利である。
うまくできたので,revision 2.0でやったようにrefreshhtmlsとwriteindexをやり直した。これで,文中のWEB-URLは自動リンクされた筈である。ダウンロードはこれまでと同じである。なお,tgzが解凍できなくて他の形式が欲しい方はメールでご連絡いただきたい。
上記の置換ではファイルの途中へ飛ぶことができなかった。つまり#がURL文字列に含まれる可能性を考慮していなかった。これに対処するためには,
$buf =~ s/(http:\/\/[\w\/\.\-_\?\=\~#]+)/<a href="$1">$1<\/a>/g;
とするだけで良いはずだ。
書き忘れたが,revision 2.3から,このスクリプトを使ってくれる掲示板が登場した。今有無の棲息地の書評掲示板である。iso-2022-jp化は,実は今有無さんのご提案であった。なお,2.3cは自動リンクにバグがあったのを修正したものである。
名前は,edmf.cgiとする。mfはMy Favoriteである。まず,仕様を言葉で詰めてみる。メモ風だがご容赦願いたい。
何もつけずに起動すれば,mftxt.dat(絶対パス指定)を参照してインデックス一覧を表示,そこでインデックスをクリックすればedmf.cgi?number=xxという参照形態でその番号の記事が編集できるとする。編集は(1)全部通すか,(2)パラグラフ毎にリストと見なして加工するか,(3)タグを許さないで<p>と</p>でパラグラフ毎に加工するか,をモード指定で埋め込めるようにする。フィルタを3つ用意するだけで良い筈だ。
もちろん,平行して過去のmf_a??.htmをmftxt.datに取り込む作業が必要である。mftxt.datはEUCにする。htmlを出力する時点でiso-2022-jp化すればよい。なお,過去のシステムは併存させて,中の内部参照リンク(1つの話題へだから,個別ファイルへのリンクでよい)を漸次新しい体系の方に書き換えてゆく。
セキュリティ問題。mftxt.datは,CGIからは絶対パスでアクセスされるからWEB-unreachableな場所に置くのが望ましい。シェルが使えないサイトの場合,.htaccessで制限すればよい。mftxt.datを直接エディタなどで更新してアップデートした場合は,全htmlを作り直す必要があり,そのときはcgi実行でオーバーライトできるファンクションをつける。cgiで編集して更新する場合は関連テキストのみオーバーライトする。いずれにせよ,書き込みパスワードを指定して,それが合致した場合しか書き込みできないようにする。
カテゴリ指定が必要。カテゴリ定義はmftxt.datの先頭に。カテゴリ数が最初の行。sequentialに読めるはず。
htmlは自動生成。NMZ.headみたいな形でヘッダとフッタを分離(最終更新時刻=CGIを動作させた時刻を埋め込みたいので,$TIME_NOW$という置換用のSpecial Characterを設定),これらはiso-2022-jpにしておこうかと思ったが,置換する必要からEUC-JPにしておくことにした。最新テキスト(=メイン)+カレンダ一覧+カテゴリ別インデックスリスト+3ヶ月単位まとめファイル+カテゴリ別まとめファイル。個別ファイルにはそれぞれへ「戻る」アンカータグと「前へ」「次へ」を埋め込んでおく。個別ファイルの生成は,別々の,引数参照渡しのサブルーチン(呼び出し元の値をサブルーチン内で変更することはできない)によるようにする。カレンダーの形式は,javascriptで書いたやつみたいに1行が1週間になるのでもいいが,普通はブラウザの横幅はもっと広いと思うので,(1)1行が1週間のカレンダーを,3ヶ月分ほど横に並べる,(2)1行が1ヶ月,のどちらかにするのが美しいと思う。
リンクの扱い。内部はファイル名生成規則に依存してパス指定可(/~minato/...という形にすれば,myfavor.htmから呼んでもmyfavor/...から呼んでも問題ない=これはテキスト編集時に問題になることで,CGIの設計には無関係)。日付にするか連番にするか? カレンダー表示してファイルがあるところはリンクする,とか考えると,日付の方がよさそうだが,「次へ」リンクを考えると,連番の方が楽。でも一旦全部読んじゃえばいいのか。いずれにせよCGI先頭で選べるようにする?
さて,具体的な作業において,何をおいても決めなくてはいけないのはデータ構造である。ニクラス・ヴィルトの「アルゴリズム+データ構造=プログラム」を引くまでもなく,作ろうとしているのは一種のデータベースだから,データ構造が決定的に重要である。後々拡張しやすく作っておく必要もある。
最初は1行=1レコードにしようと思っていたが,日記の性質上,1レコードが異様に長くなってしまう場合が考えられ,それを普通のエディタでも編集できるようにするためには,レコード区切りを空行にして,複数行を1つのレコードとして扱えるようにした方がよいことに気づいて,以下のようにデータ構造を決めた(フィールド区切りはタブコード0x09である)。
[ファイル先頭] [カテゴリ数] [カテゴリ1] [カテゴリ1の名称] …… [カテゴリn] [カテゴリnの名称] [連番]#_# [変換モード]#_# [カテゴリ]#_# [ID]#_# [タイトル] [本文1行目] [本文2行目] [本文3行目] [本文4行目] (空行) [連番]#_# [変換モード]#_# [カテゴリ]#_# [ID]#_# [タイトル] [本文1行目] [本文2行目] [本文3行目] [本文4行目] (空行) [ファイル末尾]
これを読んで配列変数に格納するサブルーチンreaddbを書いておき,html化するときにはまずreaddbを呼び出すことにする。書き出しの3種類のフィルタは別々のルーチンにするが,気にしなくてはいけないのは,置換をするときに,オプションとしてg(見つけた全部について)だけでなく,m(改行コードを無視)も必要だということである。そこで大分悩んだのを除けば,フィルタ機能は問題なく完成した。
(1999年12月3日追加)まとめ読み機能(全文とカテゴリ別)を追加して,version 0.32とした。編集機能はまだ途中なので動作しない。気をつけなければいけないのは,ヘッダでファイル名だけの指定でスタイルファイルを呼んでいる場合,cgiがあるディレクトリにもスタイルファイルが必要となってしまうことである。これは鬱陶しいので,ヘッダでのスタイルファイル名の指定をhttpdのサーバルートからのパス指定にした。
(1999年12月7日追加)検索表示機能を追加して,version 0.33とした。
Cookieのexpireの設定で年を表す文字列が100になってしまっていたのを1900を足して直し,ついでなので無駄を省いた。
今までプログラムをつぎはぎしてきたために,あまりに無駄なコードが多く,バグもあったこと,日英二カ国語でメッセージを書いていたのは冗長だったと反省したことから,メッセージを最初に集めて全部配列要素にし,配列の添え字で言語を指定するように変更した。
具体的には,$langという変数を設定し,$msgd[$lang]によって,$msgd[1]なら書評だし,$msgd[0]ならBook Reviewと表示するようにした。つまり,$lang=0とするだけで,完全に英語環境でも使えるようになった筈である。本当は英語環境ならjcode.plも不要なのだが,条件文が増えるのは鬱陶しいので,現状ではjisに変換している。無意味だが。
(1月25日追加)コメント追加サブルーチンでフォームタグの閉じ位置を間違えたというか,</form>が1つ余計にあったため,コメント追加ができなくなっていたという恥ずかしいバグを修正。
$numtopsという変数を追加し,sub maketopの中で最新レコードだけ表示していたところをループにしただけなので簡単な変更。3月24日に「前へ」表示に整合性をもたせ,全般的に表示を改善する微妙な修正を付加して0.35とした。
xrefというサブルーチンを追加し,個別アーカイヴファイルの「トップへ」の前にフォローアップというリンクを追加。クリックすれば,後にそのアーカイヴを引用しているファイルがすべて表示されるという寸法。そのファイル自身とマッチする文字列を含むデータだったら表示する,というだけの単純なアルゴリズム。
編集サブルーチンと登録サブルーチンを作る必要がある。編集の方はサブルーチンでなくても,ローカルにフォームを使ったHTMLファイルを作ってもいいのだが,このアーカイヴを編集したい,という場合,それ自身の表示画面から,番号とともに呼び出せると便利と思うので,cgiにやらせるべきだろう。アルゴリズムとしては,そんなに難しくないと思う。
xrefのためにも,連番は表示されていた方が便利なので,1話ごとに連番も表示するようにした。表示形式は直接cgiを3箇所書き換えねばならないという,甚だ場当たり的な変更ではあるが,多少便利になったかもしれない。
URLになりうる文字列をいくつか忘れていたことに気づいたので追加した。
URLにポート指定がされている場合を含めるため,":"を自動リンクの文字列に含めた。さらに,ISBNが書かれた場合にオンライン書店の有名どころに自動リンクする機能をつけようと思った。森山和道さんのサイトの情報などを参照すると,書店によってISBNの指定方法はハイフンを含めた形式と含めない形式があることがわかった。そこで,書評内でも,どちらの形式で書かれても対応できるように,4つの部分文字列に分けてパターンマッチさせることにした。パターンマッチした結果をリンク形式にするのは長くなりそうなのでサブルーチンを書いて文字列としてreturnさせることにした。が,うまくいったかどうかはまだわからない。次に載せる書評でISBNを書いてみてうまく行ったかどうかチェックすることにしよう。
パターンマッチの結果を直接使って置換え文字列をサブルーチンで返そう(変な日本語で申し訳ない)としたら,文字列とみなされてサブルーチン名自体が返ってしまったので,ワンクッション置く形に切り替えたらうまくいったようだ。それにしても,bk1も自動リンクしたいのだが,ISBNでは直接呼び出すことができないようなのが残念だ。
(2001年2月13日追記)今のところうまく行っているようなのでパターンマッチの仕方を書いておこう。ISBNという文字の後にすぐ,あるいはスペースかイコールかコロンを挟んで,ハイフンで区切られた4つの数字列(最後はXの場合もある)が並ぶものにマッチさせれば良いのだから,$bufという文字列内を置換すると考えて,$buf =~ /ISBN[:=\s]*(\d+)\-*(\d+)\-*(\d+)\-*([\d|X]+)/i;とすれば,$1,$2,$3,$4にISBNの部分文字列が得られるので(仮にハイフンで区切られていなくてもそれなりにマッチするように,\-*としている),いったん$ref = &bslink($1, $2, $3, $4);などとして,bslinkというサブルーチン内でリンク文字列を作り,それを$refに代入しておいてから,$buf =~ s/ISBN[:=\s]*(\d+)\-*(\d+)\-*(\d+)\-*([\d|X]+)/ISBN $1-$2-$3-$4<BR>$ref/i;のようにして,改めて表示させたい形式で置換を行えばよい。
sub bslink内では,$1などとして与えられた引数は$_[0]など配列要素として参照できるので,サブルーチン先頭で一括して$d1=$_[0];のようにして短い文字列に代入しておき,自動リンク内ではこの短い文字列の方を使うようにした。なお,生成した自動リンク文字列,例えば$rrを返すためには,サブルーチンの最後にreturn $rr;を入れるだけでよいので簡単である。この部分だけ独立に使えるようにしたスクリプトをbslinks.tgz (1215 bytes)として公開しているので,自由に改変して使っていただいて構わない。
全登録件数をトップページに表示するようにした。ついでに,表示が乱れている部分があったので,htmlファイルの一括再生成をやろうとしたら,1つ再生成するたびにコメントデータファイルを読み直すという馬鹿なデザインにしていたおかげでタイムアウトすることに気づいた。そこで,こういう場合の常套手段だが,フラッグとなる変数を定義し,メインルーチンでゼロクリアしておいて,読み込みルーチンの最後にフラッグを立てるように変更し,html生成ルーチン内では,フラッグが立っていたらコメントデータファイルは読まないように変更した。これで1000件を超えるようになっても一括再生成に不安がなくなった。怪我の功名というべきか。
フォームメールとは,メールアドレスがない人でも,またメールソフトを使わなくても,WEBからメールが送れるという仕組みである。大抵のフォームメールスクリプトはsendmailなどのMTA (Mail Transfer Agent)を使ってメール送信を行うので,Perlのスクリプトの実効ユーザがデフォルトではnobodyになってしまうことから,最近のセキュリティの厳しいsendmail(8.9.3以降)では実行不可能な場合が多く,スクリプトから直接MTAを呼び出す代わりにCで書いたwrapperを噛ませて回避するか,Apacheの設定でsuEXECという大技を繰り出すか,あるいは,ここで紹介するPerl5のモジュールで直接メールを送ってしまうという方法を採らざるを得ない。
Perl5のモジュールを使う方法は,wrapperを書くより簡単だし,suEXECほどセキュリティ上の怖さがないが,あまり紹介されている場所が多くない。理由の1つは,たぶんモジュールそのもののインストールがルート権限でないとできないことだと思うが,自前でサーバを持っている人や,プロバイダのサイトのPerlに偶々JcodeとMail::Sendmailが組み込まれているといった幸運な人なら使えるので,試して損はないと思う。
日記で書いたことだが,以下に再掲する。
呼び出しフォームについては,アーカイヴを解凍すると得られるサンプルhtmlファイルをご覧いただけばわかるように,簡単である。cgi内ですることは,まず,前述のモジュールを使うという宣言である。これは,perlの存在位置を書いた直後に,use Jcode; use Mail::Sendmail;と書く。Jcodeの方の使い方は割と簡単で,jcode.plでは&jcode'convert(*value,'jis');と書いていたところを$value=Jcode->new($value)->jis;に変えればよい。Mail::Sendmailの使い方は,%mailという連想配列にメール内容を設定してsendmail(%mail);とするだけである。%mail = (To => "$adrs", From => "$youradrs", ...)のようにコンマで区切って一度に連想配列要素を渡せばいいのだが,区切り文字がコンマなので,配送先アドレスが複数の場合も考えて,$adrsなどをダブルクォートで括るのを忘れないようにすることが肝心である。詳細はそのうち追記するかもしれないが,取りあえずこの辺りを参照されたい。
400件を超えるような現状になってくると,旧来のカテゴリ別全インデックスは意味をなさなくなってきたので,最新のものから遡及的に指定件数ずつのインデックスを表示するように改良した。個々のカテゴリの全インデックスを表示するサブルーチンを別に書いて,その機能へのリンクを表示するようにした。これくらいの改良だと10分あればできる。
書評掲示板システムをちょっと直せば,スレッド独立型の掲示板システムができるであろうことは,以前からわかっていた。環境問題についてのディベートをする掲示板を作ろうと思い立って,ちょっと書き直してみたら,狙い通り簡単にできあがった。苦労したのは主に見栄えである。
討論をするのにコメント先が明示できないのは致命的欠陥だと思ったので,こんなことをしている場合じゃないと思いながら,新幹線の中でスレッド内参照機能をつけた。所要約20分。sub writecommentとしてデータファイルからのスレッド書き出しルーチンは独立しているので,その中でコメント番号をカウントして,コメント先頭にNAMEタグで番号を付け,>>番号という参照があったら自動的にリンクするように,s/>>(\d+)/<a href="#$1">>>$1<\/a>/gとした。うまくいくと思うのだが,試してみないとわからない。