「掲示板」にしよう

 ここまでくれば、後は必要に応じてリファレンス本やウェブページで新しい関数を覚えたりしながら、これまでの事を応用していけば大抵のことは出来るでしょう。と言ってしまってもいいのですが、折角なので、1行掲示板ではなく、当サイトのMainBBSにあるようなレス機能付きの掲示板にしてみましょう。
 そのためにやることといえば、まず「1行」でなくすために複数行入力に対応して、いい加減何度も名前を入力するのが面倒になってきたでしょうからCookieを使ってみたり、レスを付けるために発言番号を管理してみたり、といたことです。やはりこれまでどおり段階を踏んで追加していきますので、まずはbbs1.cgi[ソース]をご覧ください。

複数行入力への対応

# 入力内容のデコード #
if($ENV{'REQUEST_METHOD'} eq "POST"){
	read(STDIN,$in,$ENV{'CONTENT_LENGTH'});
}else{ $in = $ENV{'QUERY_STRING'}; }
@in = split/&/,$in;
foreach(@in){
	($key,$val) = split/=/;
	$val =~ tr/+/ /;
	$val =~ s/%([0-9a-fA-F][0-9a-fA-F])/pack("C",hex($1))/eg;
	$val =~ s/</&lt;/g;
	$val =~ s/>/&gt;/g;
	$val =~ s/\r//g;
	if($key eq 'body'){
		$val =~ s/\n+$//g;
		$val =~ s/\n/<br>/g;
	}else{
		$val =~ s/\n//g;
	}
	$in{$key} = $val;
}

# 出力
print &header();
print <<"EOT";
<form method="POST" action="$mycgi">
<table>
<tr><td>お名前</td><td><input name=name size=16></td></tr>
<tr><td>タイトル</td><td><input name=title size=48></td></tr>
<tr><td>本文</td><td><textarea name=body cols=60 rows=4 wrap=soft></textarea></td></tr>
<tr><td>文字色</td><td>$colform</td></tr>
<tr><td></td><td><input type=submit name=submit value="送信"></td></tr>
</table>
</form>

 複数行の入力に対応するには、まず本文の入力フォームをINPUTタグからTEXTAREAタグにし、1発言1行のデータにするために、改行コードを別の文字列に置換する、という作業が必要になります。

 まずは改行コードの処理ですが、幸いHTMLにはBRタグがありますので、改行コードはBRタグに置換しましょう。実際INPUTタグで改行を入力することは普通出来ないので、常に改行をBRにしても問題は無いのですが、万が一いたずらをされないためにも、本文に対してのみBRタグへの置換を行います。
 BRタグに置換する前の行では、末尾にある余分な改行を切り飛ばしています。時々本文の後ろにやたらと空行が入る人を見かけるので、個人的にはこうして切り飛ばしてしまうのが好きです。ちなみに正規表現では「+」が「直前の文字1つ以上」、「$」が「文字列の末尾」という意味で、「\n+$」で「文字列の末尾まで続く1つ以上の改行」という意味になります。正規表現は、このように「こんな風な文字列と一致したら」という条件を付ける事が出来るのです。
 尚、これまでの処理順どおり、改行コードを処理した後にタグ記号の置換を行うと、改行がBRに置換された後、そのBRタグのタグ記号まで置換されてしまうので、処理の順番に気をつけましょう。

 そういえばそろそろこのデコード処理の意味も理解できてきたでしょうか?
 POSTメソッドの時には標準入力STDINに環境変数$ENV{'CONTENT_LENGTH'}バイト分のパラメータが渡されているので、read関数で$inに代入し、GETメソッドの時には環境変数$ENV{'QUERY_STRING'}にパラメータが渡されているのでそのまま$inに代入。
 まずはsplit関数を使って&で切り離し、「名前=値」という形の集まった配列に。
 その配列をforeachでループさせ、再びsplit関数を使って今度は=で切り離し、名前と値をそれぞれ$keyと$valに渡しながら処理します。split関数は、対象となる変数を省略すると自動的に$_を処理します。
 URLエンコーディングの時に半角スペースは+になっているので元に戻し、その他一部の記号や2バイト文字は「%??」という形式になっているので、正規表現とpack関数というものを使って元の文字に戻している。といった感じです。

 続いて発言フォームですが、タイトルを付けて本文をTEXTAREAにしただけです。TEXTAREAタグの属性の内、wrapの値を指定しなかった場合の解釈はブラウザによって異なる様なので、必ず指定するようにしましょう。一般的にはsoft(横幅が納まりきらない分は見た目に自動改行されるが、送信データ内では任意に改行した場所のみ有効)が使われます。

時刻の扱い

# 発言日時の取得
($ss,$mm,$hh,$d,$m,$y,$w) = localtime();
$y += 1900;
$m++;
@wday = ('日','月','火','水','木','金','土');
$date = sprintf("%d/%02d/%02d(%s)%02d:%02d:%02d",$y,$m,$d,$wday[$w],$hh,$mm,$ss);

# 追加するログデータの作成
$newlog = "<hr><font color=\"$in{'color'}\"><b>$in{'title'}</b> <small><b>$in{'name'} </b>$date</small><br>$in{'body'}</font>\n";

 掲示板らしい機能の追加として、発言日時を取り出してみました。
 localtime関数は時刻を取り出す関数で、引数には1970年1月1日から何秒経過したかという値を入れるのですが、省略すると自動的に現在時刻を出してくれます。現在の経過秒数はtimeという関数で取得できます。ちなみに時刻はあくまでサーバの時計ですので、自分のパソコンの時刻とずれていても慌てないでください。
 localtime関数を「$date = localtime();」の様に変数で受け取ると、「Tue May 10 13:37:19 2005」のような形式の文字列が受け取れますが、これはあまり使いません。配列で受け取った場合、「秒,分,時,日,月,年,曜日」の順になります。曜日は0〜6の数字が渡され、0が日曜です。但し、月は0〜11、年は西暦から1900を引いた値が 渡されますので、それぞれ数値を足してやらないと正しい値になりません。「$y += 1900;」は「$y = $y + 1900;」、「$m++;」は「$m += 1;」(つまり「$m = $m + 1;」)と同じ意味です。
 日時の値を受け取ったら、表示用の形式に揃えるのですが、ここで活躍するのがsprintf関数です。「変数 = sprintf(形式文字列,値の配列)」で、変数に値の配列を形式文字列に変換した文字列が代入されます。形式文字列には%で始まる型を指定いし、後にその数だけの値を配列で並べます。ここで使用している型は「%d(整数)」「%02d(0で埋めた2桁揃え)」「%s(文字列)」です。数値の桁揃えに使うことが多いでしょう。
 これに合わせて、ログデータの方もタイトル、名前、日時で一旦改行されるようなデザインにしました。名前のすぐ後に全角スペースを置いているのは、SHIFT_JISで全角スペースの後に半角文字が続くと、文字コードの都合で配列と間違われた上文字化けしてしまうからです。SHIFT_JISのソース上に書かれた全角文字は、全角スペース以外にも「ソ」や「能」などで文字化けを引き起こすことが結構あるので注意しましょう。

Cookieを使う

 続いては、Cookieを利用してお名前と文字色をいちいち選ばなくてもいいようにしましょう。
 Cookieは、発行時に指定されたURLごとに、各自のクライアントマシン内に情報を保存するもので、リクエストしたURLに該当するCookieの内容が、環境変数$ENV{'HTTP_COOKIE'}としてリクエスト時に自動的に送られるので、CGIはフォームの入力とは別に、以前Cookieに保存した内容を参照して処理することが出来るのです。
 今回のようにフォームの内容を記憶して再入力の手間を省いたり、会員向けサイトでのログインに利用されるなど、非常に便利ではありますが、各自のマシンに保存されるので、自由に閲覧・編集出来てしまいますし、基本的に一つのファイルにつき4KBまでしか保存できませんので、ゲームのセーブデータなどに利用するのは避けるべきです。

Cookieの発行

# Cookie発行(30日有効)
sub setcookie{

my ($name,$color) = @_;
my $gmt = &getgmt(time+86400*30);
return "Set-Cookie:SAMPLEBBS=name<>$name<>color<>$color; expires=$gmt;\n";

}
# GMT日時文字列取得
sub getgmt{

my ($sec,$min,$hour,$mday,$mon,$year,$wday) = gmtime($_[0]);
my @month=('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec');
my @week = ('Sun','Mon','Tue','Wed','Thu','Fri','Sat');
my $gmt = sprintf("%s, %02d-%s-%04d %02d:%02d:%02d GMT",
	$week[$wday],$mday,$month[$mon],$year+1900,$hour,$min,$sec);
return $gmt;

}

# 出力処理 #
sub view{

〜〜〜

%c and $http_header .= &setcookie($c{'name'},$c{'color'});

〜〜〜

# 出力
print &header($http_header);

〜〜〜

}

# HTMLヘッダ #
sub header{

my ($http_header) = @_;
return <<"EOT";
Content-Type: text/html; charset:SHIFT_JIS;
$http_header
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=SHIFT_JIS">
<title>掲示板</title>
</head>
<body bgcolor=white text=black>
EOT

}

 Cookieは、Content-typeやLocationと同様の、Set-CookieというHTTPヘッダとして出力します。
 これまでHTTPヘッダは一行しか記述していませんでしたが、今回はContent-typeとSet-Cookieの2行を出力し、表面上はContent-typeで通常のHTML出力をした場合と違いはありません。

 まず最初に、Set-Cookieの内容をsetcookieサブルーチンで作成します。パラメータとしてお名前と文字色を渡し、それを保存するようにしました。
 Cookieで保存するデータ本体は、取得時に特定する為に"Set-Cookie:名称=内容;"の様に、キーとなる名称の文字列と組にして記述します。保存する項目が1つであればそれだけですが、複数の項目を保存したい場合は何かしら特定の法則によってエンコードしてやる必要があります。これは取得時に扱う形式に戻す(デコードする)ことが出来ればどのような内容でも構いません。ここでは「項目名1<>内容1<>項目名2<>内容2」の様に「<>」で項目名と内容を交互に区切って並べています。
 「<>」は通称空タグといって、タグ記号は入力内容のデコード時に変換していますのでお名前などユーザが入力する文字列に含まれていることもありませんし、正規表現で処理する時にも特に影響が無いので、この様なデータの区切り記号として非常に有効です。
 尚、ここでは省いており、実際ほとんど問題なく動作するのですが、厳密には全角文字はURLエンコードしなければなりません。

 また、Cookieには有効期限というものが存在し、これが指定されていると、例えその他の内容が記述されていても適用されず、自動的に削除されるようになっています。指定されていない場合、ワンタイムCookieといって、そのブラウザが閉じるまでの間有効なCookieが発行されます。(ブラウザによって多少動作が異なります)
 Cookieの有効期限は、GMT(グリニッジ標準時)で記述します。ここではそのための文字列を作成する関数をサブルーチンで作成しています。30日有効とする場合、「time+86400*30」で30日後の秒数を取得し、それをgmtime関数に渡してlocaltime関数と同様に、秒,分,時,日,月,年,曜日を受け取り、月と曜日の文字列を配列で定義して、sprintf関数で「Wdy, DD-Mon-YYYY HH:MM:SS GMT」という形に成型しています。

 そして、これらを合わせて「Set-Cookie:名称=内容; expires=Wdy, DD-Mon-YYYY HH:MM:SS GMT;\n」という文字列にします。HTTPヘッダは1行で1項目ですので、有効期限は、Set-Cookieの後に「expires=Wdy, DD-Mon-YYYY HH:MM:SS GMT;」という風に並べ、その後で改行します。

 最後にこの文字列をContent-typeに続けて出力しますが、HTMLヘッダの出力用サブルーチンを、Cookieを発行する時としない時と共通で利用できるように、Content-type以外のHTTPヘッダをパラメータで受け取るようにしています。パラメータとして渡すHTTPヘッダが1行ごとにきちんと改行をしてあれば、この書き方でHTML本体との間に必ず1行の空行が出来るので問題ありません。

Cookieの取得

# Cookie取得
sub getcookie{

foreach(split/;/,$ENV{'HTTP_COOKIE'}){
	my ($key,$val) = split/=/;
	$key =~ s/\s//g;
	if($key eq "SAMPLEBBS"){
		return split/<>/,$val;
	}
}

}

# 発言処理 #
sub insert{

〜〜〜

# Cookieに入力内容を上書き
$c{'name'} = $in{'name'};
$c{'color'} = $in{'color'};

}

# 出力処理 #
sub view{

# Cookieの取得
%c or %c = &getcookie();

%c and $http_header .= &setcookie($c{'name'},$c{'color'});

〜〜〜

# 色選択フォーム作成
foreach(@msgcol){
	if($c{'color'} eq $_){
		$colform .= <<"EOT";
<input type=radio name=color value="$_" checked><font color="$_">●</font> 
EOT
	}else{
		$colform .= <<"EOT";
<input type=radio name=color value="$_"><font color="$_">●</font> 
EOT
	}
}

# 出力
print &header($http_header);
print <<"EOT";
<form method="POST" action="$mycgi">
<table>
<tr><td>お名前</td><td><input name=name size=16 value="$c{'name'}"></td></tr>
<tr><td>タイトル</td><td><input name=title size=48></td></tr>
<tr><td>本文</td><td><textarea name=body cols=60 rows=4 wrap=soft></textarea></td></tr>
<tr><td>文字色</td><td>$colform</td></tr>
<tr><td></td><td><input type=submit name=submit value="送信"></td></tr>
</table>
</form>

<center>$pagelink</center>

$log

</body>
</html>
EOT

}

 あなたのパソコンのOSがWindowsXPであれば、「C:\Documents and Settings\ユーザー名\Cookies」というフォルダにInternetExplorerのCookieが保存されています。他のブラウザでも、それぞれ特定のフォルダに同様の形式で保存されたファイルがあるはずです。
 ブラウザは、アクセスしたURLに一致するもの全てを環境変数$ENV{'HTTP_COOKIE'}として送信します。一つのURLに対して複数のCookieが発行される場合もありますので、で送られた中から必要なデータだけを受け取る為に、まず「;」切り分けてループし、保存時に付けた名称をチェックします。

 一つのURLにCookieが複数保存されていた場合、「;」の後に半角スペースが入ったりするので、それを取り除いてから比較し、一致した時の内容を返します。(「\s」は正規表現で空白文字を表します)ここでは内容が「項目名1<>内容1<>項目名2<>内容2」という形式になっていますので、切り分けてから返すのですが、いちいち配列やハッシュに代入せず、直接returnで返してしまいます。
 returnを行うと自動的にサブルーチンを終了するので、ループの途中でも名称の一致したデータがあれば、そこで終了するということになります。

 今回の場合、項目名と内容を交互に並べていますので、%cというハッシュとして受け取っていますが、この形式は自分で決めているのですから、「名称=お名前<>文字色」という形で保存し、「($c_name,$c_color) = &getcookie();」の様にしても構いません。

 それから出力の際に、%cというハッシュで受け取ったCookieの内容をフォームに反映させていますが、その前の発言処理で、入力内容を同じハッシュに代入し、その場合はCookieの取得を行っていません。そして、どちらの場合でも%cが存在していたらCookieを発行しています。
 発言していなくとも、既にCookieが保存されていた場合は、ページをリロードする度にCookieが発行されることになりますが、これは、ページを閲覧するだけで、再びその瞬間からCookieが30日有効になるようにするためです。こうしないと、閲覧だけしているとその内Cookieが消えてしまうことになるので注意しましょう。

レス機能の実装

 最後にレス機能の実装ですが、これを行うために必須となることはなんでしょうか?
 それは、「発言番号の管理」です。  どの発言に対してレスを付けるかという指定をするためには、少なくともスレッドごとに番号を振っておく必要があります。また、発言内容の編集機能など、発言単位での処理を行おうと思ったら、発言ごとに番号を振っておかなければなりません。また、各レスがどの発言に対するレスなのかを区別する情報も必要でしょう。
 ここまでログの内容はHTMLがそのまま書かれていましたが、これを管理するためには、記事番号と親記事番号を発言内容と別に持っておかなければいけません。HTMLの形式を崩さないままそのような情報を持つことも出来ますが、ここでは区切り記号を使ったデータ形式にする、汎用性の高い方法を紹介します。

 それではbbs3.cgi[ソース]をご覧ください。

発言処理の修正

# 入力内容のデコード #

〜〜〜
	$val =~ s/\t//g;
〜〜〜

# 処理モード判定 #
if($in{'submit'}){ &comment(); }
elsif($in{'res'}){ &resform(); }
else{ &view(); }
exit;

# 発言処理(新規発言・レス共通) #
sub comment{

〜〜〜

# ロック開始
&lock() or &error('只今混雑しているようです。','Lock ERROR!');

# ログファイル読込
open FILE,$logfile;
# 発言番号は別に取得
$count = <FILE>;
chomp $count;
@logdat = <FILE>;
close FILE;

# スレッド単位でログを格納
%log = ();
foreach(@logdat){
	my ($num,$res) = split/\t/;
	$log{$res} .= $_;
}

# カウントアップ
$count++;

# 新規発言は発言番号を親記事番号に
$in{'res'} or $in{'res'} = $count;

# そのスレッドのデータに追加
$log{$res} .= join("\t",$count,$in{'res'},$date,$in{'color'},$in{'name'},$in{'title'},$in{'body'},"\n");

# 保存スレッド数の分保存
open FILE,">$logfile";
print FILE "$count\n";
$l = 0;
foreach$res(sort { $b <=> $a } keys %log){
	$l++;
	$l > $maxlog and last;
	print FILE $log{$res};
}
close FILE;

# ロック終了
&unlock();

# Cookieに入力内容を上書き
$http_header .= &setcookie($in{'name'},$in{'color'});

# 完了画面の転送用METAタグ
$html_header .= "<meta http-equiv=\"Refresh\" content=\"3;URL=$mycgi\">\n";

# 出力
print &header($http_header,$html_header);
print <<"EOT";
<p align=center>
<b>送信完了</b><br>
コメントありがとうございます。<br>
ページが切り替わらない場合は下のリンクをクリックしてください。<br>
<br>
[<a href="$mycgi">戻る</a>]<br>
</p>

</body>
</html>
EOT

}

 データの保存形式を大幅に変更するということは、データの読み書き両方に関わることなので、かなり大きな修正となります。実際部分的に修正しても他の部分が対応できていないと動作確認もままならないのですが、データの書き込み処理の部分から説明することにします。

 まず、入力内容のデコード処理で、\t(TAB記号)も削除しています。これは、1データ1行という形を保つために改行を削除しているのと同様に、TAB記号を区切り記号として使用するために、ユーザの入力によってデータが壊れるのを防いでいます。
 ちなみにTAB記号を区切り記号として使うのは、ブラウザでTABキーを押すと、次のリンクやフォームの部品へ移動するようになっているため、フォームにTAB記号を入力して送信するのは難しいといった理由で、区切り記号として扱い易いからです。それに、TAB区切りテキストはExcel等のツールで展開することができるので、手動でデータを直接書き換える場合にも便利だったりします。

 次の処理モード判定では、発言処理が新規発言とレスの両方を兼ねているので、insertサブルーチンの名前をcommentに変更し、返信フォーム用のサブルーチンresformが増えています。
 また、送信ボタンが押されていたら発言処理、押されていなくて親記事番号が指定されていたら返信フォーム、どちらでもなければログの表示、という風に処理を振り分けており、複数のサブルーチンが同時に呼ばれることもなくなっています。この様になっている理由については後で解説します。

 そして発言処理になりますが、先ほどいったとおり、新規発言とレスの両方を兼ねた処理になっています。似ているとはいえ異なる処理を一つにまとめるのはちょっとややこしいことかもしれませんが、これも共通する処理を1箇所にまとめてメンテナンス性を向上させるための工夫です。
 また、この様な組み方が難しいという場合でも、サブルーチンで部分ごとに処理を細切れにして組み立てるという方法があります。

 それでは本題ですが、まず、ファイルの読み込み処理が1行増えています。これは、発言番号を管理するために、最初の1行をカウンタとして機能させているのです。
 カウンタはもちろん数値ですが、1行のデータなので改行が含まれます。すると、「数値\n」という文字列として扱われてしまうので、改行を取り除きます。chomp関数は、文字列の末尾の改行のみを取り除く、便利な関数です。
 尚、「<ファイルハンドル>」というのは、一回ごとに先頭の1行を返すという書き方で、「変数 = <ファイルハンドル>;」と書けば最初の1行だけが変数に代入されます。その後、そのままファイルハンドルを閉じずに同じように書けば、1回ごとに2行目、3行目となり、配列に代入すると残りの分が1行1項目で全て代入されるのです。

 「スレッド単位でログを格納」というのは、スレッド数をカウントするのと、スレッド内の順序を保存するために行っています。
 各行を\tでsplitして親記事番号を取り出していますが、ここでは2番目の項目である親記事番号しか必要ないので、1つ目は未定義値であるundefと書き、2つ目に変数名を書いてそこで終わっています。
 また、ここでmyを使っていますが、こうした場合、ローカライズされた変数は、ループの外に干渉しません。ありがちな変数名を使う時は、こうしておいた方が、処理を追加する時に変数名が被ってもおかしなことにならず安心です。

 次にカウントを1つ進め、親記事番号が指定されない新規の発言の場合には、発言番号を親記事番号にしています。
 親記事番号が空のものを親記事、と判断するような場合もありますが、こうしておけば、親記事番号だけを見て同じスレッドの発言をチェックできるので、処理の都合がいいと思います。
 また、この場合、親記事であることは、発言番号と親記事番号が一緒かどうかで判断します。

 そして発言データを追加していますが、join関数を使って、今回区切り記号に使用した「\t」で発言データを繋げています。「$count\t$in{'res'}\t〜\t\n」の様に書いてもいいのですが、こうすると区切り記号を変えたい時に書き換える箇所が減りますし、データの並びも見た目に分かりやすく、少し楽だと思います。  ちなみに、この書き方をする場合、最後の「"\n"」を忘れないようにしましょう。
 それから、「.=」でハッシュの項目に追加するようにしていますが、この方法だと、新規発言の場合はハッシュに新たな項目を作り、返信の場合は先ほどハッシュにまとめた内容の後に追加されるので、新規発言時と返信時の処理が、1つの書き方でうまくまとまります。

 続いてデータの保存です。書き込み時にprint文を複数書けば、その分が順に書き込まれます。
 カウンタは改行を改めて付けて書き込みます。
 次にハッシュに分けた内容を、親記事番号の大きい順に書き込んでいるのですが、ここで使用するのがsort関数です。
 「keys ハッシュ」とすると、ハッシュのキーの配列が取り出せるのですが、この時出来上がる配列の順番は決まっていません。
 sort関数は、配列の並び順を決定する関数で、「sort 配列」とだけ書くと文字コードの小さい順に並びますが、「sort {並び替え処理} 配列」とすることで、一定条件での並び替えが可能です。
 { $b <=> $a }というのは、数値の大きい順に並べる時の書き方で、$aと$bはこの時特殊変数として振舞います。ちなみに、$aと$bを入れ替えれば数値の小さい順です。
 「sort 配列」でも数値順に並びそうですが、あくまで文字コード順なので、1から100までの数値を「sort 配列」で並べ替えると、1の次は10になってしまいます。
 それから、これまで発言単位(行単位)で保存するデータの量を制限していましたが、今回発言の古い順に削除してしまうと、スレッドの親記事だけが消えてしまったりすることになりますので、スレッド単位で保存数を制限しています。こうすると1つのスレッドにレスが大量についても、無制限に記録されてしまいますので、1スレッド当たりのレス数制限を付けてもいいかもしれません。

 最後にHTMLの出力です。
 これまで、発言処理後直接ログ表示を行ったり、Locationで転送を行ったりする方法を紹介しましたが、今度はHTMLのMETAタグによる転送処理です。この処理の為に、headerサブルーチンの内容が多少変更されているので、そちらもチェックしてください。

 入力内容をCookieを出力するためには、Locationを行うとCookieに保存する内容をURLエンコードする必要があるなど面倒なので、そのままHTML出力を行うのが一般的ですが、今回の処理でそのままログ表示に移ろうとすると、少々厄介な事態に陥ります。(どうなるかは秘密です。自分でそのように作って試してみましょう)  そこで、単純な返信完了画面を出力してとりあえずCookieを発行し、METAタグのRefresh処理を使用してログ表示画面に転送しています。
 こうするとその問題も解決する他、書き込み処理という手間のかかる処理と表示処理を同時に行わないので、ある意味サーバに優しいと言えますし、ログ表示画面に行ってしまえばリロードしても大丈夫なので、案外悪くもないのです。

 尚、ここでHTMLの出力まで行ってしまっているので、この後出力処理&view()を呼ぶ必要もありません。

出力処理の修正

 発言内容を新形式で保存する用意が出来たので、そういうデータが保存されているものとして出力処理を修正しましょう。

# 出力処理 #
sub view{

〜〜〜

# ログファイル読込(発言時は不要)
if(!@logdat){
	open FILE,$logfile;
	# 発言番号は別に取得
	$count = <FILE>;
	@logdat = <FILE>;
	close FILE;
}

〜〜〜

# 表示するページのログ取得
$log = '';
$l = -1;
foreach(@logdat){
	my ($num,$res,$date,$col,$name,$title,$body) = split/\t/;
	if($num == $res){
		$l++;
	}
	$l < ($in{'page'} - 1) * $pageline and next;
	$l >= $in{'page'} * $pageline and last;
	if($num == $res){
$log .= <<"EOT";
<dt><hr><b>$title</b> <b>$name</b> <small>$date</small> [<a href="$mycgi?res=$num">返信</a>]<br>
<font color="$col">$body</font></dt>
EOT
	}else{
$log .= <<"EOT";
<dd><hr><b>$title</b> <b>$name</b> <small>$date</small><br>
<font color="$col">$body</font></dd>
EOT
	}
}

〜〜〜

# 出力
print &header($http_header);
print &form($c{'name'},$c{'color'});
print <<"EOT";

<center>$pagelink</center>

<dl>
$log</dl>

</body>
</html>
EOT

}

 まず、ログファイルの読み込み処理で、1行目を分けるようにします。

 表示するページのログ取得処理では、変数$lでスレッド数をカウントし、指定行数より小さかったらnextで次へ進み、指定行数を超えたらlastですぐにループを抜けるようにしています。
 元の処理が1行目を0番として行っているので、ここでも$lの初期値を-1として、処理の仕方を変えずに修正しています。
 また、データがHTMLに成型されていないので、わずかに処理が重くなるものの(split関数も処理としては重い方なのです)、この処理を修正すると即座に全てのログに反映されるので、模様替えをするならこの方式は有効ですし、ログに対して色々処理をすることを考えれば、何かと都合が良いので、多機能な掲示板ほどこの様なデータの持ち方をすることが多いです。扱い易さと処理の軽さのバランスを考えてデータの形式を決めるということも大事です。

 そして、親記事と子記事で表示を切り替え、親記事には返信フォームへのリンクを付け、子記事はインデント(字下げ)を行っています。
 この様な場合、blockquoteタグで字下げを行う人もいますが、blockquoteタグは本来インデントのためのタグではないですし、左側だけでなく右側まで字下げが行われてしまうのでお勧めしません。それくらいなら、面倒でもTABLEタグを使って成型することをお勧めします。
 個人的には、一般的にあまり使われていない気がしますが、DL,DT,DDというタグを使うのが簡単で良いと思っています。

 出力部分では、次に説明する返信フォームとほぼ内容が同じ発言フォームを、文字色の選択フォーム作成処理ごとサブルーチンにしています。
 formサブルーチンでは、隠し属性のフォームに返信番号を入れ、返信フォームへのリンクで指定された親記事番号を、返信時にも送信できるようにしています。

返信フォームの作成

# 返信フォーム
sub resform{

# Cookieの取得
%c or %c = &getcookie();

%c and $http_header .= &setcookie($c{'name'},$c{'color'});

# ログファイル読込(発言時は不要)
open FILE,$logfile;
# 発言番号は別に取得
$count = <FILE>;
# 表示するスレッドのログ取得
$log = '';
while(<FILE>){
	my ($num,$res,$date,$col,$name,$title,$body) = split/\t/;
	$res == $in{'res'} or next;
	# 返信番号の発言が親記事でなかったらエラー
	$num == $in{'res'} and $num != $res and &error('その発言にレスをつけることはできません。');
	if($num == $res){
$log .= <<"EOT";
<dt><hr><b>$title</b> <b>$name</b> <small>$date</small><br>
<font color="$col">$body</font></dt>
EOT
	}else{
$log .= <<"EOT";
<dd><hr><b>$title</b> <b>$name</b> <small>$date</small><br>
<font color="$col">$body</font></dd>
EOT
	}
}
close FILE;

# 出力
print &header($http_header);
print <<"EOT";
[<a href="$mycgi">戻る</a>]<br>

<dl>
$log</dl>

<hr>

EOT
print &form($c{'name'},$c{'color'},$in{'res'});
print <<"EOT";

</body>
</html>
EOT

}

 ログのページ全体をフォームとしてスレッドごとに選択ボタンを置き、返信したいスレッドのボタンをチェックして発言、という形式であれば、ページを増やす必要もなくて簡単なのですが、あまり使い勝手が良いとも思えないので、返信用ページを作ってしまいましょう。

 Cookieの扱いはログ表示の時と同様ですが、ログファイルの読み込みはページ単位でなく指定されたスレッド分だけでいいので、ファイルの中身を全て配列で受け取ってしまわずに、必要な分だけを取り出しています。
 後はサブルーチン化されたフォームを出力するだけですので、それほど手間ではありません。ログ成型処理をサブルーチン化するなどすれば、更に効率化が図れるかもしれませんね。

終わりに

 見た目には少々寂しいものの、これで一通り掲示板としての機能が整いました。
 ここから更に機能を追加して、アイコン掲示板にしてみたり、修正・削除フォームや管理機能を作っても良いでしょう。ですが、それらはほとんどこれまでやってきた事の繰り返しのはずです。

 また、ブラウザゲームを作る場合でも、やることはほとんど変わりません。ユーザの入力内容に対して処理を行い、ファイルを読み書きして結果を表示する。結局CGIがやっていることはそれだけに過ぎないのです。

 ただ、ここでは分かり易さを重視したが為に、抜けていたり間違っている内容もあるかもしれません。
 このサイトで基本的なことを理解し、本格的にプログラミングをやりたいと思うならば、詳しく解説された専門書――最終的には「ラクダ本」と呼ばれるもの――を読み、より深く正しい知識を身につけることをお勧めします。

 最後に、今回は成り行きでこの様な形式を紹介しましたが、他の人が同じようなPerlCGIを書いたら、きっと別のプログラムかと思うようなソースになるでしょう。ですが、それも正解に違いはありません。
 プログラムの書き方に明確な正解などなく、結果的に同じ動作をするなら、自分のいじりやすいと思う方法でやればいいのです。Perlはそれだけ多彩な書き方が出来る言語なのですから。
 最初は習うがままで構いませんが、自分なりの書き方のルールを身に付ける事が、「プログラマ」になる事なのだと思います。


≪Index  ≪Back