Webページで提示したものに、閲覧者による投票によるランキングを作成してみよう。ランキングの作成の仕方は様々だが、順位の順番に並べるというタイプのwebページを作成しよう。
ユーザ(閲覧者)にどのように見せたいかを、この段階でしっかり検討する。その検討のなかで、別の機能のアイディアも浮かぶかもしれない。教材として、「そば投票」というテーマにしてみた。ここのサンプルのようにHTMLファイルを作成してみた。このファイルranking01.htmlのソースコードは、次のようである。
[ranking01.html]<!DOCTYPE html> <html lang="ja"> <head> <meta charset="Shift-JIS"> <title>ランキング(サンプル)</title> <meta name=viewport content="width=device-width, initial-scale=1"> <style type="text/css"> body{ background-color: lightgray; } .vote{ background-color: black; color: white; padding: 8px; } dt{ font-size: xx-large; text-align: left; margin-top: 3em; } dd p{ text-align: left; } form{ display: inline; } h5{ text-align: center; margin-top: 4em; } </style> </head> <body> <div style="width: 80%; margin: 0 auto; text-align: center;"> <h1>そば投票</h1> <p>もりかけ問題はさておき、寒い冬は、かけ蕎麦など温かい蕎麦が食べたくなります。 どのお蕎麦もおいしいけれども、売れないとお店のメニューからなくなってしまいます。 そうならないように、人気のないお蕎麦を積極的に注文する必要があります。 お蕎麦屋さんでつい選んでしまうお蕎麦に投票してください。 投票数の少ないお蕎麦が、メニューからなくなりそうなお蕎麦です。 それを意識して注文して、どのお蕎麦も楽しめる世の中にしましょう。 </p> <dl> <!-- かけそば 始まり --> <dt>1位(325ポイント) かけそば</dt> <dd> <img src="./img/kakesoba.png" width="200" align="center" alt="かけそば" > <form method="POST" action=""> <input type="submit" class="vote" value="コレをよく注文する!"> </form> <p>シンプルなそばです。ついついネギもいれてしまいます。</p> </dd> <!-- かけそば 終わり --> <!-- てんぷらそば 始まり --> <dt>2位(320ポイント) てんぷらそば</dt> <dd> <img src="./img/tenpurasoba.png" width="200" align="center" alt="てんぷらそば" > <form method="POST" action=""> <input type="submit" class="vote" value="コレをよく注文する!"> </form> <p>てんぷらと言えば海老天。</p> </dd> <!-- てんぷらそば 終わり --> <!-- 月見そば 始まり --> <dt>2位(320ポイント) 月見そば</dt> <dd> <img src="./img/tukimisoba.png" width="200" align="center" alt="月見そば" > <form method="POST" action=""> <input type="submit" class="vote" value="コレをよく注文する!"> </form> <p>黄身は汁に混ぜないように食べるのが好きな人は多いはず。</p> </dd> <!-- 月見そば 終わり --> <!-- きつねそば 始まり --> <dt>4位(250ポイント) きつねそば</dt> <dd> <img src="./img/kitunesoba.png" width="200" align="center" alt="きつねそば" > <form method="POST" action=""> <input type="submit" class="vote" value="コレをよく注文する!"> </form> <p>油揚げの味も悪くない。</p> </dd> <!-- きつねそば 終わり --> </dl> </div> <h5><a href="about.html">このページについて</a></h5> </body> </html>
全体をdiv要素の内容として、中央揃えにするためにインラインのCSSで、幅を80%、mariginプロパティの左右をautoにし、text-alignプロパティはcenterにしている。
各々のそばは、定義リスト(dl要素)で並べている。順位、ポイント、名前は、dt要素に書いている。そばの画像と投票ボタンとコメントは、dd要素に書いているので、字下げされて表示される。 dl要素の項目となっている各々のそばがわかりやすいように、始まりと終わりをコメントで示した。例えば、かけそばは、<!-- かけそば 始まり --> から <!-- かけそば 終わり -->までである。
投票ボタンは、input要素でtype属性を"submit"で表示している。
POSTメソッドを使いたかったので、CGIスクリプトにパラメータとしてデータを渡すためにform要素を投票ボタンごとに定義している。
form要素はデフォルトではブロックレベル要素なので、CSSでdisplayプロパティの値をinlineにしてインライン要素にして、インライン要素のとなりに表示されるように工夫した。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="Shift-JIS">
<title>ランキング(サンプル)</title>
<meta name=viewport content="width=device-width, initial-scale=1">
<style type="text/css">
body{
background-color: lightgray;
}
.vote{
background-color: black;
color: white;
padding: 8px;
}
dt{
font-size: xx-large;
text-align: left;
margin-top: 3em;
}
dd p{
text-align: left;
}
form{
display: inline;
}
h5{
text-align: center;
margin-top: 4em;
}
</style>
</head>
<body>
<div style="width: 80%; margin: 0 auto; text-align: center;">
<h1>そば投票</h1>
<p>もりかけ問題はさておき、寒い冬は、かけ蕎麦など温かい蕎麦が食べたくなります。
どのお蕎麦もおいしいけれども、売れないとお店のメニューからなくなってしまいます。
そうならないように、人気のないお蕎麦を積極的に注文する必要があります。
お蕎麦屋さんでつい選んでしまうお蕎麦に投票してください。
投票数の少ないお蕎麦が、メニューからなくなりそうなお蕎麦です。
それを意識して注文して、どのお蕎麦も楽しめる世の中にしましょう。
</p>
<dl>
<!-- かけそば 始まり -->
<dt>1位(325ポイント) かけそば</dt>
<dd>
<img src="./img/kakesoba.png" width="200" align="center" alt="かけそば" >
<form method="POST" action="$ENV{SCRIPT_NAME}">
<input type="hidden" name="soba" value="kake">
<input type="submit" class="vote" value="コレをよく注文する!">
</form>
<p>シンプルなそばです。ついついネギもいれてしまいます。</p>
</dd>
<!-- かけそば 終わり -->
<!-- てんぷらそば 始まり -->
<dt>2位(320ポイント) てんぷらそば</dt>
<dd>
<img src="./img/tenpurasoba.png" width="200" align="center" alt="てんぷらそば" >
<form method="POST" action="$ENV{SCRIPT_NAME}">
<input type="hidden" name="soba" value="tenpura">
<input type="submit" class="vote" value="コレをよく注文する!">
</form>
<p>てんぷらと言えば海老天。</p>
</dd>
<!-- てんぷらそば 終わり -->
<!-- 月見そば 始まり -->
<dt>2位(320ポイント) 月見そば</dt>
<dd>
<img src="./img/tukimisoba.png" width="200" align="center" alt="月見そば" >
<form method="POST" action="$ENV{SCRIPT_NAME}">
<input type="hidden" name="soba" value="tukimi">
<input type="submit" class="vote" value="コレをよく注文する!">
</form>
<p>黄身は汁に混ぜないように食べるのが好きな人は多いはず。</p>
</dd>
<!-- 月見そば 終わり -->
<!-- きつねそば 始まり -->
<dt>4位(250ポイント) きつねそば</dt>
<dd>
<img src="./img/kitunesoba.png" width="200" align="center" alt="きつねそば" >
<form method="POST" action="$ENV{SCRIPT_NAME}">
<input type="hidden" name="soba" value="kitune">
<input type="submit" class="vote" value="コレをよく注文する!">
</form>
<p>油揚げの味も悪くない。</p>
</dd>
<!-- きつねそば 終わり -->
</dl>
</div>
<h5><a href="about.html">このページについて</a></h5>
</body>
</html>
各々のそばを区別するためのキーワードを以下のように定めた。これは、画像ファイルにするために"soba.png"を追加するだけで済むように工夫した結果である。
soba | そばの名前 | 画像ファイル名 |
---|---|---|
kake | かけそば | kakesoba.png |
tenpura | てんぷらそば | tenpurasoba.png |
tukimi | 月見そば | tukimisoba.png |
kitune | きつねそば | kitunesoba.png |
name属性の値を"soba"としたinput要素で、type属性を"hidden"として、value属性をそばのsobaを値としたものを追加した。このフォームからの送信するとCGIスクリプトへsaba=そばのキーワードというパラメータが送られる。
フォームのaction属性は、環境変数を使って、$ENV{SCRIPT_NAME}とした。Perlのスクリプト内では、$ENV{SCRIPT_NAME}はCGIスクリプトファイル名に置き換えられる。
Webページの表示は、外部設計で作成したranking02.htmlの複製から作成したranking03.htmlを使用する。
ranking03.htmlは、ranking02.htmlのそばの項目のdt、dd要素をすべて削除して、そこに<!--TABLE_LIST-->と書き加えるだけである。
[ranking03.html]<!DOCTYPE html> <html lang="ja"> <head> <meta charset="Shift-JIS"> <title>ランキング(サンプル)</title> <meta name=viewport content="width=device-width, initial-scale=1"> <style type="text/css"> body{ background-color: lightgray; } .vote{ background-color: black; color: white; padding: 8px; } dt{ font-size: xx-large; text-align: left; margin-top: 3em; } dd p{ text-align: left; } form{ display: inline; } h5{ text-align: center; margin-top: 4em; } </style> </head> <body> <div style="width: 80%; margin: 0 auto; text-align: center;"> <h1>そば投票</h1> <p>もりかけ問題はさておき、寒い冬は、かけ蕎麦など温かい蕎麦が食べたくなります。 どのお蕎麦もおいしいけれども、売れないとお店のメニューからなくなってしまいます。 そうならないように、人気のないお蕎麦を積極的に注文する必要があります。 お蕎麦屋さんでつい選んでしまうお蕎麦に投票してください。 投票数の少ないお蕎麦が、メニューからなくなりそうなお蕎麦です。 それを意識して注文して、どのお蕎麦も楽しめる世の中にしましょう。 </p> <dl> <!--TABLE_LIST--> </dl> </div> <h5><a href="about.html">このページについて</a></h5> </body> </html>
投票結果や、webページの表示に必要なデータを保持するファイルを、point.datとする。point.datはタブ区切りファイルで、soba, ranking, point, name, image, commentの順番で各々のそばの情報を記録したものである。従って、初期状態として以下のように作成する。
[point.dat]kake 1 0 かけそば kakesoba.png シンプルなそばです。ついついネギもいれてしまいます。 tenpura 1 0 てんぷらそば tenpurasoba.png てんぷらと言えば海老天。 tukimi 1 0 月見そば tukimisoba.png 黄身は汁に混ぜないように食べるのが好きな人は多いはず。 kitune 1 0 きつねそば kitunesoba.png 油揚げの味も悪くない。
point.datを作成するときは、ranking02.htmlをブラウザで表示して該当箇所(特にコメント)をコピー&ペーストすると、間違いが少なくなる。
送られたパラメータの値を処理と、パラメータがない場合にwebページを表示するという二つの役割を持たせたCGIスクリプトを、ranking.cgiという名前で下のように作成する。
[ranking.cgi]
#! /usr/bin/perl
# 2019-12-16. ランキング サンプル by SU.
# use strict;
### use Encode;
### use utf8;
### my $encoder = find_encoding('utf8');
my $top_page = "index.html"; # エラーの後に戻るページ
my $datafile = "point.dat"; # 投票データを保持するファイル
my $show_list_tmpl = "ranking03.html"; # テンプレート化された表示されるwebページ
my %param;
my @data;
%param = get_param();
if( $param{soba} ne "" ){
read_records();
update_data($param{soba});
}
read_records();
show_list();
exit;
################### subroutines ##################
sub update_data
{
my $soba =$_[0];
$soba =~ s/\s+//g; # 空白は削除
my $flag = 0;
foreach (@data){
if( $_->{soba} eq $soba ){
$flag = 1;
$_->{point} += 1;
}
}
# $filenameに存在しない項目の場合は何もしない。
if( $flag == 0 ){ return; }
# 並べ替えてランキングを計算
ranking();
# 書き込みモードでファイルを開く
open(OUT, ">$datafile") || &error("Cannot open $datafile: $!");
foreach (@data){
if( $_->{soba} eq "" ){ next; }
print OUT "$_->{soba}\t$_->{ranking}\t$_->{point}\t$_->{name}\t$_->{image}\t$_->{comment}\n";
}
# ファイルを閉じる
close(OUT);
}
# ポイント順に並べ替えて、順位を計算
sub ranking
{
my( $rank, $prev_point );
# @dataをpointで降順で並べ替え
@data = sort{$b->{point} <=> $a->{point}} @data;
for(my $i = 0; $i < @data; $i++ ){
if( $i == 0 ){
$rank = 1;
$data[$i]->{ranking} = $rank;
$prev_point = $data[$i]->{point};
next;
}
if( $data[$i]->{point} < $prev_point ){
++$rank;
$prev_point = $data[$i]->{point};
}
$data[$i]->{ranking} = $rank;
}
}
sub read_records
{
my( $soba, $ranking, $point, $name, $image, $comment);
@data =(); # @dataを初期化
# 読み取りモードでファイルを開く
open(IN, "<$datafile") || &error("Cannot read $datafile: $!");
while(<IN>){
chomp;
( $soba, $ranking, $point, $name, $image, $comment ) = split(/\t/, $_);
# 無名ハッシュを配列にする
push @data,
{
'soba'=>$soba,
'ranking'=>$ranking,
'point'=>$point,
'name'=>$name,
'image'=>$image,
'comment'=>$comment
};
}
# ファイルを閉じる
close(IN);
}
sub show_list
{
my $table_list;
foreach (@data){
my $item = <<"_ITEM_";
<!-- $_->{name} 始まり -->
<dt>$_->{ranking}位($_->{point}ポイント) $_->{name}</dt>
<dd>
<img src="./img/$_->{image}" width="200" align="center" alt="$_->{name}" >
<form method="POST" action="$ENV{SCRIPT_NAME}">
<input type="hidden" name="soba" value="$_->{soba}">
<input type="submit" class="vote" value="コレをよく注文する!">
</form>
<p>$_->{comment}</p>
</dd>
<!-- $_->{name} 終わり -->
_ITEM_
$table_list .= $item;
}
# 読み取りモードでファイルを開く
open(IN, "<$show_list_tmpl") || &error("Cannot read $show_list_tmpl: $!");
print "Content-Type: text/html;charset=Shift_JIS;\n\n";
while(<IN>){
s/<!--TABLE_LIST-->/$table_list/;
print $_;
}
}
########### CGIの基本処理 #############
# 入力データの取得
sub get_param
{
my($query, $key, $value, %param);
if($ENV{REQUEST_METHOD} eq "GET"){
$query = $ENV{QUERY_STRING};
}else{
read(STDIN, $query, $ENV{CONTENT_LENGTH});
}
foreach(split(/&/, $query)){
($key, $value) = split(/=/, $_);
$value =~ s/\+/ /g;
$value =~ s/%([\da-f][\da-f])/pack("C", hex($1))/egi;
### $value = $encoder->decode($value);
$value =~ s/\r//g;
$param{$key} = $value;
}
return %param;
}
# エラー表示
sub error
{
print "Content-Type: text/html\n\n";
print <<"---EOF---";
<html><head><title>Information</title></head>
<body>
<span sylte="color: red;"> $_[0]</span>
<form method="GET" action="$top_page">
<input type=SUBMIT value="最初のページに戻る">
</form>
</body></html>
---EOF---
exit;
}
このCGIスクリプトのサブルーチン(ユーザ関数)は、以下のものである:
このCGIスクリプトのグローバル変数は、以下のものである:
上で作成したranking.cgi、point.dat、ranking03.htmlとそれに必要な画像ファイルを、ドキュメント・ルートにrankingというサブディレクトリを作成して、そこにアップロードする。ranking.cgiのパーミッションは、755に設定する。
ブラウザでranking.cgiのURLに接続して、動作を確認する。
サンプルはこちら。
不満足な点は、下位の項目に投票しても、ページの先頭が表示されてしまう点である。
不満足な点を解消するために工夫した。バージョンアップしたサンプルはこちら。
[ranking01.cgi]#! /usr/bin/perl # 2019-12-16. ランキング サンプル by SU. # 2019-12-16 v.1.1. 項目にアンカーを付けた。 # use strict; ### use Encode; ### use utf8; ### my $encoder = find_encoding('utf8'); my $top_page = "index.html"; # エラーの後に戻るページ my $datafile = "point.dat"; # 投票データを保持するファイル my $show_list_tmpl = "ranking03.html"; # テンプレート化された表示されるwebページ my %param; my @data; %param = get_param(); read_records(); if( $param{soba} ne "" ){ update_data($param{soba}); read_records(); print "Location:http://$ENV{SERVER_NAME}$ENV{SCRIPT_NAME}#$param{soba}\n\n" }else{ show_list(); } exit; ################### subroutines ################## sub update_data { my $soba =$_[0]; $soba =~ s/\s+//g; # 空白は削除 my $flag = 0; foreach (@data){ if( $_->{soba} eq $soba ){ $flag = 1; $_->{point} += 1; } } # $filenameに存在しない項目の場合は何もしない。 if( $flag == 0 ){ return; } # 並べ替えてランキングを計算 ranking(); # 書き込みモードでファイルを開く open(OUT, ">$datafile") || &error("Cannot open $datafile: $!"); foreach (@data){ if( $_->{soba} eq "" ){ next; } print OUT "$_->{soba}\t$_->{ranking}\t$_->{point}\t$_->{name}\t$_->{image}\t$_->{comment}\n"; } # ファイルを閉じる close(OUT); } # ポイント順に並べ替えて、順位を計算 sub ranking { my( $rank, $prev_point ); # @dataをpointで降順で並べ替え @data = sort{$b->{point} <=> $a->{point}} @data; for(my $i = 0; $i < @data; $i++ ){ if( $i == 0 ){ $rank = 1; $data[$i]->{ranking} = $rank; $prev_point = $data[$i]->{point}; next; } if( $data[$i]->{point} < $prev_point ){ ++$rank; $prev_point = $data[$i]->{point}; } $data[$i]->{ranking} = $rank; } } sub read_records { my( $soba, $ranking, $point, $name, $image, $comment); @data =(); # @dataを初期化 # 読み取りモードでファイルを開く open(IN, "<$datafile") || &error("Cannot read $datafile: $!"); while(<IN>){ chomp; ( $soba, $ranking, $point, $name, $image, $comment ) = split(/\t/, $_); # 無名ハッシュを配列にする push @data, { 'soba'=>$soba, 'ranking'=>$ranking, 'point'=>$point, 'name'=>$name, 'image'=>$image, 'comment'=>$comment }; } # ファイルを閉じる close(IN); } sub show_list { my $table_list; foreach (@data){ my $item = <<"_ITEM_"; <!-- $_->{name} 始まり --> <a name="$_->{soba}"></a> <dt>$_->{ranking}位($_->{point}ポイント) $_->{name}</dt> <dd> <img src="./img/$_->{image}" width="200" align="center" alt="$_->{name}" > <form method="POST" action="$ENV{SCRIPT_NAME}"> <input type="hidden" name="soba" value="$_->{soba}"> <input type="submit" class="vote" value="コレをよく注文する!"> </form> <p>$_->{comment}</p> </dd> <!-- $_->{name} 終わり --> _ITEM_ $table_list .= $item; } # 読み取りモードでファイルを開く open(IN, "<$show_list_tmpl") || &error("Cannot read $show_list_tmpl: $!"); print "Content-Type: text/html;charset=Shift_JIS;\n\n"; while(<IN>){ s/<!--TABLE_LIST-->/$table_list/; print $_; } } ########### CGIの基本処理 ############# # 入力データの取得 sub get_param { my($query, $key, $value, %param); if($ENV{REQUEST_METHOD} eq "GET"){ $query = $ENV{QUERY_STRING}; }else{ read(STDIN, $query, $ENV{CONTENT_LENGTH}); } foreach(split(/&/, $query)){ ($key, $value) = split(/=/, $_); $value =~ s/\+/ /g; $value =~ s/%([\da-f][\da-f])/pack("C", hex($1))/egi; ### $value = $encoder->decode($value); $value =~ s/\r//g; $param{$key} = $value; } return %param; } # エラー表示 sub error { print "Content-Type: text/html\n\n"; print <<"---EOF---"; <html><head><title>Information</title></head> <body> <span sylte="color: red;"> $_[0]</span> <form method="GET" action="$top_page"> <input type=SUBMIT value="最初のページに戻る"> </form> </body></html> ---EOF--- exit; }
「このページについて」として、参考文献や謝辞など、このページのメタ情報を、about.htmlに作成しよう。
about.htmlの工夫としては、「戻る」のリンクはjavascriptのhistory.back()関数で元のページに戻ることができるようにしている。
[about.html]<!DOCTYPE html> <html lang="ja"> <head> <meta charset="Shift-JIS"> <title>Thanks</title> <meta name=viewport content="width=device-width, initial-scale=1"> <style type="text/css"> body{ background-color: lightgray; } h5{ text-align: center; margin-top: 4em; } </style> </head> <body> <div style="width: 80%; margin: 0 auto; text-align: center;"> <h1>謝辞</h1> <p>このページを作成するのに、以下のサイトを参考にしたり利用したりしましたので、感謝します。 </p> <ul> <li><a href="http://www.ipc.hokusei.ac.jp/~z00328/swdesign/ranking/rankingdoc.html" target="_blank">そば投票サンプルの説明</a></li> <li><a href="http://www.ipc.hokusei.ac.jp/~z00328/swdesign/ranking/ranking01.cgi" target="_blank">そば投票サンプル</a></li> </ul> </div> <h5><a href="javascript:history.back()">戻る</a></h5> </body> </html>
URLで最後のファイル名を省略すると、index.htmlが省略されたものと解釈されてindex.htmlを表示しようとする。デフォルトの設定では、index.htmlが存在しない場合は、そのフォルダ内のファイルのリストをindex.htmlとして表示するようになっている。それは、あたかも舞台裏をさらけ出しているようで格好が悪いし、セキュリティー上も好ましくない。そこで、index.htmlをranking.cgiに自動的にジャンプするページにするという工夫をしてみよう。
[index.html]<!DOCTYPE html> <html lang="ja"> <head> <meta charset="Shift-JIS"> <title>そば投票(サンプル)</title> <meta name=viewport content="width=device-width, initial-scale=1"> <meta http-equiv="Refresh" content="1;URL=ranking.cgi"> <style type="text/css"> body{ background-color: lightgray; } </style> </head> <body> <div style="width: 80%; margin: 0 auto; text-align: center;"> <h1>そば投票</h1> <p>自動的にジャンプしない場合は、<a href="ranking.cgi">ここ</a>をクリックしてください。 </p> </div> </body> </html>