職案人

求職・歴史・仏教などについて掲載するつもりだが、自分の思いつきが多いブログだよ。適当に付き合って下さい。

お御籤とお告げ/Perl/CGIの基礎講座

2018年05月30日 | perl
お御籤とお告げ/Perl/CGIの基礎講座


【環境条件】
Eclipse 4.4(ルナ)
XAMPP 1.8.3 
Perlは既にインストール済み
CGIを使用する時は、必ずApachを起動する

【おみくじの】
最初に表示される画面の左上にあるのは管理者用の仕掛けです。パスワードを入力して 「設定」 ボタンをクリックすると、設定画面が開かれて、お御籤の出る確率を、例えば、「大吉」 を 30%、「凶」 を 10% などと任意に設定することができます。

1.おみくじ画面

・「籤引き」ボタンを押す

2.お告げ画面
画面の 「籤引き」 ボタンを押すと、画面がめまぐるしく変わり、その後で 「お告げ」 の画面が現われます。バックグランドミュージックの変化や画面がめまぐるしく変わる動きは、最初の画面の中に仕掛けてある javascript で制御しています。画面が20回切り替わった後で、「お告げ」 の画面が表示されます。


【スクリプト】
この御神籤のスクリプトは、omikuji.cgiがメインで、そのスクリプトからlibrary.plとOmikuji.plを読み込んでる。
◆omikuji.cgi
1.#!/usr/local/bin/perl
2.
3.require 'library.pl';#ライブラリーの読み込み
4.require 'Omikuji.pl';#パッケージの読み込み
5.
6.$thisfile = 'omikuji.cgi';
7.$pass = '0123';
8.$bg_img = 'img/pur6b.gif';
9.$font_family = 'AR勘亭流H, HG創英角ポップ体';
10.$m_dir = 'mid';
11.$music_draw = 'babels.mid';
12.@music1 = ('yuuki.mid', 'bee.mid', 'bop_on.mid');
13.@music2 = ('avinion.mid', 'l_coucou.mid', 'fly_free.mid');
14.
15.#---- 基本プログラム ----
16.&decode;
17.if ($in{'mode'} eq 'otuge') { &otuge; }
18.elsif ($in{'mode'} eq 'admin') { &admin; }
19.elsif ($in{'mode'} eq 'write') { &write; }
20.else { &omikuji; }
21.exit;
22.
23.#---- お御籤を引く画面 ----
24.sub omikuji {
25. $n1 = int(rand 3);
26. $music = "$m_dir/$music1[$n1]";
#スタイルシート設定
27. $css = <<END;
28.#div0 { position:absolute; top:16px; left:16px; }
29.#div1 { font-size:20pt; font-weight:bold; margin-top:50px; }
30.#div2 { position:absolute; top:170px; width:100%; font-size:150pt; }
31.#div1, #div2 { text-align:center; font-family:$font_family; }
32.h1 { color:#006400; font-size:30pt; }
33.b { color:red; }
34..s1 { width:6em; height:32px; font-weight:bold; font-size:12pt;
35. background:#800000; color:white; margin-top:50px; }
36.END
# javascript の設定
37. $java = <」と出るか、「<b>大凶</b>」と出るか、
71. 今日の運勢を占います!!!</p>
72.<p>下のボタンを押してください</p>
73.<form action="$thisfile" method="post">
74.<input type="hidden" name="mode" value="otuge">
75.<input type="button" value="籤引き" class="s1" onclick="chng()">
76.</form>
77.</div>
78.<div id="div2"></div> ←囲った部分をブロックレベル要素としてグループ化している。
79.<p id="p1"><embed hidden="true" src="$music"></p>
80.</body>\n</html>
81.END
82.}
83.
84.#---- お告げの画面 ----
85.sub otuge {
86. &Omikuji::drawKuji($n1, $kuji, $text);
87. $n2 = int($n1/2); if ($n2>2) { $n2=2; }
88. $music = "$m_dir/$music2[$n2]";
89. $css = <<END;
90.div { text-align:center; font-family:$font_family;
91. font-size:20pt; font-weight:bold; margin-top:50px; }
92.h1, b { color:red; font-size:30pt; }
93.form { margin-top:50px; }
94.input { width:6em; height:32px; font-weight:bold; font-size:12pt;
95. background:#800000; color:white; }
96.END
97. &header('お告げ', $css, '');
98. print <<END;
99.<body background="$bg_img">
100.<div>
101.<h1>お告げ</h1>
102.<p>今日の運勢は <b>$kuji</b> です。</p>
103.<p>$text</p>
104.<form action="$thisfile">
105.<input type="submit" value="戻る">
106.</form>
107.</div>
108.<p><embed hidden="true" src="$music"></p>
109.</body>\n</html>
110.END
111.}
112.
113.#---- お告げの設定画面 ----
114.sub admin {
115. if ($in{'pw'} ne $pass) { &error('パスワードが違います'); }
116. @kuji = &Omikuji::getKuji;
117. @data = &Omikuji::readData;
118. $max = @kuji * 10;
119. $css = <<END;
120.div { text-align:center; font-family:$font_family;
121. font-size:20pt; font-weight:bold; margin-top:50px; }
122.h1 { color:#006400; font-size:30pt; }
123..s1 { width:6em; height:32px; font-weight:bold; font-size:12pt;
124. background:#800000; color:white; margin-top:50px; }
125.END
126. &header('お告げの設定', $css, '');
127. print <<END;
128.<body background="$bg_img">
129.<div>
130.<h1>お告げの設定</h1>
131.<p>合計が $max になるように設定してください</p>
132.<form action="$thisfile" method="post">
133.<input type="hidden" name="mode" value="write">
134.<table>
135.END
136. for ($i=0; $i<@kuji; $i++) {
137. print <<END;
138.<tr><td>$kuji[$i]</td>
139.<td><input type="text" size=6 name="k$i" value="$data[$i]">
140.</td></tr>
141.END
142. }
143. print <<END;
144.</table>
145.<input type="submit" value="設定" class="s1">
146.</form>
147.</div>\n</body>\n</html>
148.END
149.}
150.
151.#---- 設定の保存処理 ----
152.sub write {
153. $max = 0;
154. foreach (sort keys %in) {
155. if (/k([0-9])/) { $data[$1] = $in{$_}; $max += 10; }
156. }
157. $ret = &Omikuji::writeData(@data);
158. if ($ret==0) { &error("合計が $max ではありません"); }
159. &omikuji;
160.}
161.
162.#---- エラー表示画面 ----
163.sub error {
164. $css = <<END;
165.div { text-align:center; font-family:$font_family;
166. font-size:20pt; font-weight:bold; margin-top:50px; }
167.h1 { color:#006400; font-size:30pt; }
168.input { width:6em; height:32px; font-weight:bold; font-size:12pt;
169. background:#800000; color:white; margin-top:50px; }
170.END
171. &header('設定エラー', $css, '');
172. print <<END;
173.<body background="$bg_img">
174.<div>
175.<h1>ERROR !</h1>
176.<hr width=600 size=2 noshade>
177.<p>$_[0]</p>
178.<hr width=600 size=2 noshade>
179.<input type="button" value="戻る" onclick="history.back();">
180.</div>
181.</body>\n</html>
182.END
183. exit;
184.}

●●● 解説 ●●●


・外部ファイルの呼び込み
【omikuji.cgi】
1.#!/usr/local/bin/perl
2.
3.require 'library.pl';#ライブラー
4.require 'Omikuji.pl';#パッケージ

ライブラリーとパッケージを呼び込んでる。ところで、ライブラリとパッケージの違いは、変数の扱い方が違います。ライブラリと呼び出し側とで、互いの変数をグローバルに扱えるのに対して、パッケージの場合は独立性が強く、変数は別々に管理されるため、呼び出し側では相手の変数を気にする必要はありません。

6.$thisfile = 'omikuji.cgi';
7.$pass = '0123';
8.$bg_img = 'img/pur6b.gif';
9.$font_family = 'AR勘亭流H, HG創英角ポップ体';
10.$m_dir = 'mid';
11.$music_draw = 'babels.mid';
→初期設定用の変数と配列です。 $pass は管理者用のパスワードです。 $bg_img は背景画像、$font_family は表示用字体です。

12.@music1 = ('yuuki.mid', 'bee.mid', 'bop_on.mid');
13.@music2 = ('avinion.mid', 'l_coucou.mid', 'fly_free.mid');
→楽曲の指定、$m_dir はこれらの楽曲ファイルの所在を示すディレクトリです。これらの楽曲ファイルを omikuji.cgi と同じディレクトリに置く場合は、カレントディレクトリを示す 「. 」 を使い $m_dir = '.' と指定します

【library.pl】
1.#---- フォームデータ取得 ----
2.sub decode {
・・・
16.}
・・・
41.#---- HTMLヘッダー部の出力 ----
42.sub header {
・・・
66.}
67.1; ←ライブラリーの場合

【Omikuji.pl】
package とは
「名前空間」 を宣言する言葉です。「この名前で、次から記述する変数名、サブルーチン名などを管理せよ」 と宣言しているのです。このため、パッケージ内で使われる変数名などは、呼び出し側とは異なる別な 名前空間 で管理されることなります。

1.package Omikuji; #パッケージ名の宣言(名前空間)をする
#コンストラクタ設定
2.sub BEGIN {
3. $datafile = 'omikuji.dat';
4. @kuji = ('大吉', '中吉', '小吉', '吉', '末吉', '凶');←6要素の配列
5. @text = (
6. '何をしても絶好調な一日。新しいことにチャレンジを!',
7. '良いことが起きます。早朝東の空にUFOが見られるかも?',
8. 'ちょっとした幸運あり。道で小銭が拾えるかも?',
9. 'ツキが廻ってきます。但しギャンブルに手をだすと転落。',
10. '運気上昇の兆しあり。トイレで読書すると吉に転ずる?',
11. '災厄の日。入浴すると吉、湯船で眠るとそのまま沈む。');←6要素の配列
12. $max = 6;
13.}

14.sub getKuji { return @kuji; }
15.sub writeData {
16. $sum=0;
17. foreach (@_) { $sum += $_; }
18. if ($sum != $max * 10) { return 0; }
19. else {
20. $data = join(",", @_);←区切り文字列を挟みながら、文字列を連結し、連結した文字列をスカラー変数
21. open(OUT, ">$datafile");
22. print OUT "$data\n";
23. close(OUT);
24. return 1;
25. }
26.}
27.sub readData {
28. if (-e $datafile) {
29. open(IN, $datafile);
30. $data = <IN>;
31. close(IN);
32. chomp $data;←改行を取り除く関数
33. @val = split(/,/, $data);←指定した区切り文字で文字列を分解して配列を作成する
34. }
35. return @val;
36.}
37.sub drawKuji {
38. @val = &readData;
39. if (@val) {
40. $rr = int(rand($max * 10));
41. for ($n1=0, $dd=0; $n1<$max; $n1++) {
42. $dd += $val[$n1];
43. if ($rr < $dd) { last; }
44. }
45. } else {
46. $n1 = int(rand $max);
47. }
48. $_[0] = $n1;
49. $_[1] = $kuji[$n1];
50. $_[2] = $text[$n1];
51.}
52.return 1;← パッケージの場合


・パッケージ内サブルーチンの呼び出し
→スクリプトomikuji.cgiからパッケージ内のサブルーチンを呼び出すには、86行目のように書く&Omikuji::drawKuji($n1, $kuji, $text);→ &パッケージ名::サブルーチン名; として呼び出さなければなりません。この書き方はPerl5での書き方で、Perl4では &パッケージ名'サブルーチン名; となっています。

■ パッケージ Omikuji.plについて
1.package Omikuji;←パッケージ宣言
2.sub BEGIN {
$datafile = 'omikuji.dat';
・・・・・・・
13.}
までは、コンストラクタと呼ばれる初期処理の部分、$datafile として管理者が 「お告げ」 の出る確率を設定するデータファイルを、また @kuji と @text という夫々6つの要素を持つ2つの配列を定義しています。

14.sub getKuji { return @kuji; }
パッケージ内で定義した配列 @kuji を返します。呼び出し側で、これを配列、または変数で受け取れば、配列 @kuji そのもの、または @kuji の要素数が渡されます。

15.sub writeData {・・・}のサブルーチンは
管理人が設定したデータをデータファイルに書き込むための処理ルーチンです。渡された引数の合計を計算して、$max の10倍とならない場合は、エラーを示すために 0 を返して終了します。合計が正しい場合には、join 関数を使って引数を区切り記号 「, 」 で繋ぎ合わせて(20行目)、それを $datafile に書き出し、成功を示す 1 を返します。ファイルに書き込まれるデータは "20,10,10,5,5,10" のような形の1行の文字列です

27.sub readData {・・・}のサブルーチンは
$datafile が存在すれば、これを読み取りモードで開きます。このファイルには1行しか書いてないので、これを $data という変数に代入してファイルを閉じます。末尾の改行記号を取り去った後、split 関数で、「, 」 で区切られたデータを分解して配列 @val に取り込み、この配列を返します

37.sub drawKuji{・・・}のサブルーチンは
お御籤を引いた人への 「お告げ」 を演算するルーチンです
先ず、readData サブルーチンを呼び出し、データファイルに書かれているデータを配列 @val で受け取ります

データが存在する場合には、
$max を10倍した数以下の乱数($max が 6 のときは 0~59)を $rr にセットします(40行目)。41~44行目の for ループでは、まわる度に、配列 @val の要素を加算した $dd と 乱数 $rr とを比較して、$rr < $dd の条件が成り立てば、last; で for ループを抜け出します。ループを抜けたときの $n1 は、このような処理の結果、単なる乱数ではなく重み付けされた値になっています

45.} else {
46. $n1 = int(rand $max);
47. }
→データが存在しなければ @val は偽となり、46行目の演算で、乱数による均等確率で $n1 の値をセットします。

48. $_[0] = $n1;
49. $_[1] = $kuji[$n1];
50. $_[2] = $text[$n1];
51.}
この $n1 と、配列 @kuji と @text の夫々から $n1 番目の文字列を取り出して、第1~第3引数にセットします(48~50行目)。

◆omikuji.cgi の基本プログラム
15.#---- 基本プログラム ----
16.&decode;
→decode サブルーチンが生成するハッシュ %in にある筈の キー mode の値 $in{'mode'} を調べている。次のif文で、値にあった処理をする。

17.if ($in{'mode'} eq 'otuge') { &otuge; }
18.elsif ($in{'mode'} eq 'admin') { &admin; }
19.elsif ($in{'mode'} eq 'write') { &write; }
20.else { &omikuji; }←最初の画面に成る
21.exit;
→最初に表示される 「お御籤を引く画面」 は、omikuji サブルーチンで作られます。

■ お御籤を引く画面
23.#---- お御籤を引く画面 ----
24.sub omikuji {
25. $n1 = int(rand 3);
26. $music = "$m_dir/$music1[$n1]";
27. $css = <<END;
 ・・・
36.END
37. $java = <<END;
 ・・・
57.END
58. &header('おみくじ', $css, $java);
59. print <<END;
60.<body id="bg" background="$bg_img">
 ・・・
80.</body>\n</html>
81.END
→25行目の $n1 = int(rand 3); で、$n1 に 0~2 の乱数が代入されます。
26行目で、この$n1を使って変数 $music に楽曲ファイルをセットします。 $m_dir と @music1 は、前節で説明したように、このスクリプトの冒頭で定義してあり、$n1 が 0 であれば、$music は "mid/yuuki.mid" というような文字列となります。

→27.$css = <<END;から36.ENDはスタイルシートの文字列を $css に設定。
→37.$java = <<END;から57.ENDは javascript の記述を $java に設定。
これらの変数を引数にして、58行目で library.pl 内の header サブルーチンを使って HTML のヘッダー部分を出力する。

→59.print <このHTML は、javascript を使って 「籤引き」 というボタンを押すと、画面がめまぐるしく変わり、せきたてるようなバックグランドミュージックが鳴り出し、20回表示を変えた後に、「お告げ」 の画面に切り替わるようになっています

【JavaScriptについて】
getElementById→任意のHTMLタグで指定したIDにマッチするドキュメント要素を取得するメソッドです。


javascript の仕掛けの説明
21.function chng() {
22. if (n1==0) {
DynamicHTML(<a href="https://eng-entrance.com/what-is-dom">DOM</a>)
23.document.getElementById("div0").style.visibility = "hidden";#可視属性の変更
24.document.getElementById("div1").style.visibility = "hidden";#可視属性の変更
25.document.getElementById("bg").style.backgroundImage = "none";
26. document.getElementById("p1").innerHTML =
27. '<embed hidden="true" src="mid/babels.mid">';
28. }
29. var i = n1++ % 4;
30. document.getElementById("div2").innerText = mj[i];
31. document.fgColor = fc[i];
32. document.bgColor = bc[i];
33. if (n1chng()関数が起動されます。n1が0であるため、先ず23~27行目が順に実行されます。ここの処理は1回だけ行われます。

→23行目にある。getElementById("div0").style.visibility とは id="div0" と設定した40~46行目のdiv要素を特定する記述で、このstyle要素のvisibilityプロパティを "hidden" と設定して、見えないように隠している。ここは、管理者が設定画面を開くためのフォームを含んでいる部分です。
40.<div id="div0">
フォーム0
41.<form action="omikuji.cgi" method="post">
42.<input type="hidden" name="mode" value="admin">
43.<input type="password" size=12 name="pw">
44.<input type="submit" value="設定">
45.</form>
46.</div>

→24行のdocument.getElementById("div1").style.visibility = "hidden";も同様で、idに設定された"div1"要素をスタイルシートに現れないようにしている。
47.<div id="div1">
48.<h1>おみくじ</h1>
49.<p>「<b>大吉</b>」と出るか、「<b>大凶</b>」と出るか、
50. 今日の運勢を占います!!!</p>
51.<p>下のボタンを押してください</p>
フォーム1
52.<form action="omikuji.cgi" method="post">
53.<input type="hidden" name="mode" value="otuge">
54.<input type="button" value="籤引き" class="s1" onclick="chng()">
55.</form>
56.</div>

→25行のdocument.getElementById("bg").style.backgroundImage = "none";はbody 要素の背景画像を非表示に変更している。

→26行のdocument.getElementById("p1").innerHTML = '<embed hidden="true" src="mid/babels.mid">';→.innerHTMLプロパティを使って、58行の

<embed hidden="true" src="mid/yuuki.mid">

で、最初に鳴り出す楽曲を設定されたものを、別な楽曲に変更している。
ただし、「babels.mid」「yuuki.mid」ファイルが無いのでならない。

→29.var i = n1++ % 4;は、n1%4の演算結果を i に代入した後で、変数 n1 はインクレメントされます。この演算では、n1 を4で割った剰余が変数 i に代入されるので、i は 0~3 の何れかの値となります。

→30行目では、id="div2" と設定した57行目の
内の文字を、配列 mj の添え字としてこの i を使って、mj[i] とします。同様にして、31, 32行目では、画面の文字色と背景色を、fc[i] と bc[i] に変更しています。

→33行の if (n1<20) { timer = setTimeout("chng()", 250); } は、変数 n1 が20に達するまで、このchng()関数自身が 250msec 毎に再帰的に呼び出されます。呼び出されるたびに、30~32行目で画面の文字、文字色、背景色が変更され、めまぐるしく変化する画面が表示されることになります。

→n1が20に達すると、34行目の clearTimeout(timer); でタイマーがクリアされ、 document.forms[1].submit(); が実行されます。HTML内のフォームは、forms というコレクション(配列)で管理されています。この HTML には2つのフォームがあり、現われた順に forms[0]、forms[1] ・・・ として識別されます。従って、この記述は、2番目に現われる 52~55行目の 「籤引き」 ボタンのあるフォームを指定して、このフォームのデータを submit() メソッドで送信するように命じていることになります。送信先は、52行目の form タグの action 属性で指定されている omikuji.cgi で、53行目の隠し input タグの記述で mode=otuge というデータが送られます。従って、この命令で 「お告げの画面」 に切り替わります。

■ お告げの画面
84.#---- お告げの画面 ----
85.sub otuge {
86. &Omikuji::drawKuji($n1, $kuji, $text);
87. $n2 = int($n1/2); if ($n2>2) { $n2=2; }
88. $music = "$m_dir/$music2[$n2]";
89. $css = < 90.div { text-align:center; font-family:$font_family;
91. font-size:20pt; font-weight:bold; margin-top:50px; }
92.h1, b { color:red; font-size:30pt; }
93.form { margin-top:50px; }
94.input { width:6em; height:32px; font-weight:bold; font-size:12pt;
95. background:#800000; color:white; }
96.END
97. &header('お告げ', $css, '');
98. print <<END;
99.<body background="$bg_img">
100.<div>
101.<h1>お告げ</h1>
102.<p>今日の運勢は <b>$kuji</b>です。</p>
103.<p>$text</p>
104.<form action="$thisfile">
105.<input type="submit" value="戻る">
106.</form>
107.</div>
108.<p><embed hidden="true" src="$music"></p>
109.</body>\n</html>
110.END
111.}
→86行目で Omikuji.pl の drawKuji サブルーチンを &Omikuji::drawKuji($n1, $kuji, $text); で呼び出し、ありがたいお告げを頂いています。お告げが 「大吉」 の場合には、$n1=0、$kuji='大吉'、$text='何をしても絶好調な ・・・ ' とセットされます

→87行目では、6種類のお告げを3種類の楽曲に割り当てるため、$n2 = int($n1/2); の処理をしています。 if ($n2>2) { $n2=2; } の処理は、Omikuji.pl が6種類以上のお告げを出せるように修正される場合もあり得ることを配慮した処理です

→88行目では、冒頭で定義された $m_dir と @music を使って、この画面で使う楽曲ファイルを $music という変数にセットしています。

あとは一気呵成に HTML 文を出力します。 89~96行目でスタイルシートを $css に設定して、97行目で &header('お告げ', $css, ''); として HTML のヘッダー部分を出力し、98~110行目で HTML の body 部分を出力しています。

◆「お告げの設定画面」 を表示する admin サブルーチンのスクリプト
113.#---- お告げの設定画面 ----
114.sub admin {
115. if ($in{'pw'} ne $pass) { &error('パスワードが違います'); }
116. @kuji = &Omikuji::getKuji;
117. @data = &Omikuji::readData;
118. $max = @kuji * 10;

スタイルシート
119. $css = <<END;
120.div { text-align:center; font-family:$font_family;
121. font-size:20pt; font-weight:bold; margin-top:50px; }
122.h1 { color:#006400; font-size:30pt; }
123..s1 { width:6em; height:32px; font-weight:bold; font-size:12pt;
124. background:#800000; color:white; margin-top:50px; }
125.END

126. &header('お告げの設定', $css, '');

<HTML>を出力する
127. print <<END;
128.<body background="$bg_img">
129.<div>
130.<h1>お告げの設定</h1>
131.<p>合計が $max になるように設定してください</p>
フォームを作る
132.<form action="$thisfile" method="post">
133.<input type="hidden" name="mode" value="write">
134.<table>
135.END

136. for ($i=0; $i<@kuji; $i++) {
137. print <<END;
138.<tr><td>$kuji[$i]</td>
139.<td><input type="text" size=6 name="k$i" value="$data[$i]">
140.</td></tr>
141.END
142. }

143. print <<END;
144.</table>
145.<input type="submit" value="設定" class="s1">
146.</form>
147.</div>\n</body>\n</html>
148.END
149.}
→115行目で、「お御籤を引く画面」 から送られてくるパスワードをチェックして、正しくない場合は error サブルーチンに飛ばします。正しければ、116行目に進みます。

→116行目の @kuji = &Omikuji::getKuji; で、Omikuji.pl 内の配列 @kuji を受け取ります。

→117行目では @data = &Omikuji::readData; で、データファイルから読み出したデータを受け取ります。データファイルが存在しなければ、@data は空の配列となります。

→118行目の$max = @kuji * 10;では、配列 @kuji の要素数を10倍した値が $max に代入されます。6種類のお告げのときは $max = 6 * 10 の演算です

→119~125行目でスタイルシートの文字列を $css に設定し、126行目でこの $css を使って HTML のヘッダー部分を出力します。127行目以降で HTML の body 部分を出力しています。

→136~142行目のfor ループを使って、テーブルの各行2列を出力しています。ここでも @kuji が要素の数として扱われていることに注意してください。このようにスクリプトを記述しておけば、Omikuji.pl でお御籤の種類を増やしたとしても影響を受けなくなります。

→この画面のフォームは、132~146行目で生成する form 要素です。管理者が入力したデータは、139行目の生成する input タグから送られ、k0=20、k1=10 ・・・ のような形になります

■ 設定の保存処理
管理者の入力を保存処理する write サブルーチンを下に示します。「お告げの設定画面」 から送られてきた k0=20、k1=10 ・・・ のようなデータは、decode サブルーチンによって、ハッシュ %in に、キー 'k0' の値として 20、キー 'k1' の値として 10 ・・・ などと格納されています

151.#---- 設定の保存処理 ----
152.sub write {
153. $max = 0;
154. foreach (sort keys %in) {
155. if (/k([0-9])/) { $data[$1] = $in{$_}; $max += 10; }
156. }
157. $ret = &Omikuji::writeData(@data);
158. if ($ret==0) { &error("合計が $max ではありません"); }
159. &omikuji;
160.}

→これらのデータは、154~156行目の foreach ループで処理されます。154行目の (sort keys %in) は、ハッシュ%in からキーだけを配列として取り出し、それをソートした配列です。 foreach ループでは、この配列から1つずつ要素を取り出し、155行目で処理します。

→155行目では、取り出した$_ を対象に、正規表現 での /k([0-9])/ パターンにマッチするかどうかを調べます。 'k0'、'k1' のようなキーだけがマッチして、( ) で囲まれた部分は、特殊な変数 $1 に格納されます。

→従って、キー 'k0' が取り出されたときには、$data[$1] = $in{$_}; は、$data[0] = $in{'k0'}; となり、このようにして配列 @data に管理者が入力したデータが格納されます。また、該当するキーが見付かるたびに、$max には10 が加算され、キーが6個見付かれば、ループを抜けたときには、60になっています。

→157行目の $ret = &Omikuji::writeData(@data); で、Omikuji.pl 内の writeData サブルーチンを使ってデータファイルへの書き込みを行います。戻り値 $ret が 0 となるのは、データの合計が正しくなかったためで、この場合は158行目で error 処理を行います。データの合計値が正しければ書き込み成功の 1 が返り、この場合は omikuji サブルーチンに処理を移します。

■ エラー表示画面
エラー画面を表示する error サブルーチンのスクリプト
162.#---- エラー表示画面 ----
163.sub error {
164. $css = <<END;
165.div { text-align:center; font-family:$font_family;
166. font-size:20pt; font-weight:bold; margin-top:50px; }
167.h1 { color:#006400; font-size:30pt; }
168.input { width:6em; height:32px; font-weight:bold; font-size:12pt;
169. background:#800000; color:white; margin-top:50px; }
170.END
171. &header('設定エラー', $css, '');
172. print <

$_[0]

で出力されます。

→179行目の 「戻る」 ボタンをクリックすると、onclick="history.back();" で、この画面の前に表示されていた画面に戻ります。 history とは javascript のオブジェクトで、このオブジェクトのメソッド back() は、前の画面に戻る動作を引き起こします。

この画面を表示させたあとは、呼び出し元に戻らないように、サブルーチンの最後に exit; を実行して、プログラムを終結させます。





コメント

シンプルな掲示板/Perl.CGIの基礎講座

2018年05月08日 | perl
シンプルな掲示板/Perl.CGIの基礎講座

【環境条件】
Eclipse 4.4(ルナ)
XAMPP 1.8.3 
Perlは既にインストール済み
CGIを使用する時は、必ずApachを起動する

参照サイト→例題9: シンプルな掲示板

【シンプルな掲示板】
1)入力画面

画面上部に投稿用のフォームが表示され、画面下部にこれまでに投稿のあったログが表示されます。投稿用フォームには 「編集キー」 の入力用テキストボックスがあります。投稿する際にこの 「編集キー」 を入力しておけば、この記事を投稿した後で削除したり、修正したりすることができます。また、この掲示板の 「使い方」 のページを表示させることもできます。


■スクリプト「bbs.cgi」
1.#!/usr/local/bin/perl
2.
3.#---- ライブラリの読み込み ----
4.require 'library.pl';
5.
6.#---- グローバル変数の設定 ----
7.$thisfile = 'bbs.cgi';
8.$datafile = 'bbs.dat';
9.$title = 'この講座へのご感想';
10.$maxdata = 20;
11.$maxview = 5;
12.$pass = '0123';
13.$home = 'http://homepage2.nifty.com/Shouji/';
14.$kiji_bg = '#f0e68c';
15.
16.$default_css = < 17.body {background:#eeeeee;}
18.a:link, a:visited {color:blue;}
19.a:hover, a:active {color:red;}
20.END
21.
22.#---- 基本プログラム ----
23.&decode;
24.if ($in{'mode'} eq 'help') {&help;}
25.&dataRead;
26.if ($in{'mode'} eq 'write') {&write;}
27.elsif ($in{'mode'} eq 'edit' || $in{'mode'} eq 'del') {&edit;}
28.&display;
29.
30.#---- データファイル読み取り ----
31.sub dataRead {
32. if (-e $datafile) {
33. open(IN, $datafile);
34. @data = <IN>;
35. close(IN);
36. }
37.}
38.
39.#---- データファイル書き込み ----
40.sub dataWrite {
41. open(OUT, ">$datafile");
42. eval {flock(OUT, 2)} ; # 排他ロック
43. print OUT @data;
44. close(OUT);
45.}
46.
47.#---- 投稿記事の保存処理 ----
48.sub write {
49. $name=$in{'name'}; $ttl=$in{'ttl'}; $kiji=$in{'kiji'}; $pwd=$in{'pwd'};
50. if ($in{'num'}) {
51. # 記事の修正書き込み
52. for ($i=0; $i<@data; $i++) {
53. ($num, $date) = split(/<>/,$data[$i]);
54. if ($in{'num'}==$num) {
55. $data[$i] = "$num<>$date<>$name<>$ttl<>$kiji<>$pwd\n";
56. $in{'page'} = int($i/$maxview) + 1;
57. last;
58. }
59. }
60. } else {
61. # 記事の新規書き込み
62. @youbi = ('日', '月', '火', '水', '木', '金', '土');
63. ($sec, $min, $hour, $day, $mon, $year, $wkday) = localtime(time);
64. $date = sprintf("%4d/%02d/%02d(%s) %02d:%02d:%02d",
65. $year+1900, ++$mon, $day, $youbi[$wkday], $hour, $min, $sec);
66. ($num) = split(/<>/, $data[0]);
67. $num++;
68. unshift(@data, "$num<>$date<>$name<>$ttl<>$kiji<>$pwd\n");
69. while (@data>$maxdata) { pop(@data);}
70. }
71. &dataWrite;
72.}
73.
74.#---- 投稿記事の修正、削除 ----
75.sub edit {
76. for ($i=0; $i<@data; $i++) {
77. ($num, $date, $name, $ttl, $kiji, $pwd) = split(/<>/, $data[$i]);
78. chomp $pwd;
79. if ($num==$in{'num'}) {
80. if ($in{'pwd'} eq $pass || $in{'pwd'} eq $pwd) {
81. if ($in{'mode'} eq 'edit') {
82. # 記事の修正フォーム表示
83. $v0=$num; $v1=$name; $v2=$ttl; $v3=$kiji; $v4=$pwd;
84. &CodeToMj($v1, $v2, $v3, $v4);
85. } else {
86. # 記事の削除
87. splice(@data,$i,1);
88. &dataWrite;
89. }
90. } else {
91. $msg = ($pwd) ? '編集キーが違います':
92. 'この記事には編集キーが設定されていません';
93. &error($msg);
94. }
95. last;
96. }
97. }
98. if ($i==@data) {&error("記事番号 $in{'num'} が見つかりません");}
99. $in{'page'} = int($i/$maxview) + 1;
100.}
101.
102.#---- 投稿フォーム、過去ログの表示 ----
103.sub display {
104. $css = <<END;
105.form {margin:0;}#マージン
106.hr {width:800px;}#水平の罫線
107..tbl {border-collapse:collapse; width:800px;}
108..t1 {background:$kiji_bg; font-weight:bold;}
109..t2 {padding:6px 2em;}
110..t3 {border:1px solid black; text-align:center;}
111.END
112. $java = <<END;
113.function f1() {
114. if (document.forms[0].name.value=="" ||
document.forms[0].kiji.value=="") {
115. alert('名前と記事に入力が必要です'); return false;
116. } else { return true; }
117.}
118.function f2(p) {
119. document.forms[1].page.value = p;
120. document.forms[1].submit();
121.}
122.function f3() {
123. if (document.forms[2].num.value=="" ||
document.forms[2].pwd.value=="") {
124. alert('記事番号と編集キーに入力が必要です'); return false;
125. } else { return true; }
126.}
127.END
128.&header($title, $default_css.$css, $java);
129. print <<END;
130.<body>
131.<p> <a href="$home">HOMEへ戻る</a> 
132.<a href="$thisfile?mode=help">使い方</a></p>
133.<h1 align="center">$title</h1>
134.<form action="$thisfile" method="post" onsubmit="return f1()">
135.<input type="hidden" name="mode" value="write">
136.<input type="hidden" name="num" value="$v0">
137.<table align="center">
138.<tr><td>名前:</td>
139.<td><input type="text" name="name" value="$v1"></td></tr>
140.<tr><td>題名:</td>
141.<td><input type="text" size="40" name="ttl" value="$v2"></td></tr>
142.<tr><td>記事:</td>
143.<td><textarea cols="60" rows="4" name="kiji">$v3</textarea></td></tr>
144.<tr><td>編集キー(記事の修正、削除用):
145.<input type="password" name="pwd" value="$v4">  
146.<input type="submit" value="投稿する"></td></tr>
147.</table>
148.</form>
149.<form action="$thisfile" method="post">
150.<input type="hidden" name="page" value="">
151.</form>
152.END
153. $p = $in{'page'} || 1;
154. $top = --$p * $maxview;
155. for ($i=$top; $i<$top+$maxview; $i++) {
156. if ($i==@data) {last;}
157. ($num, $date, $name, $ttl, $kiji) = split(/<>/, $data[$i]);
158. print <<hr>
160.<table align="center" class="tbl">
161.<tr class="t1"><th width="6%">$num:</th>
162.<td width="20%">$name</td><td width="40%">$ttl</td>
163.<td align="center"><small>$date</small></td></tr>
164.<tr><td class="t2">$kiji</td></tr>
165.</table>
166.END
167. }
168. print qq(<hr>\n<table align="center" cellpadding=4 class="tbl">\n);
169. print qq(<t><td width="45%">【ページ移動】);
170. for ($i=0, $p=1; $i<@data; $i+=$maxview, $p++) {
171. if ($i==$top) {print qq(  $p);}
172. else {print qq(  <a href="javascript:f2($p)">$p</a>);}
173. }
174. print <<END;
175.</td>\n<td class="t3">
176.<form action="$thisfile" method="post" onsubmit="return f3()">
177.記事番号<input type="text" name="num" size="3">
178.編集キー<input type="password" name="pwd">
179.<select name="mode">
180.<option value="edit" selected>修正
181.<option value="del">削除
182.</select> 
183.<input type="submit" value="実行">
184.</form>\n</td></tr>\n</table>
185.</body>\n</html>
186.END
187. exit;
188.}
189.
190.#---- 使い方の表示 ----
191.sub help {
192.&header($title, $default_css, '');
193. print <<END;
194.<body>
195.<p> <a>戻る</a></p>
196.<h1 align="center">使い方</h1>
197.<table align="center"><tr><td>
198.<ul>
199.<li>投稿するには、「名前」と「記事」の入力が必須です。
200.<li>編集キーを入力しておけば、自分の投稿記事を修正したり、削除することができます。
201.<li>記事は最大で$maxdata件保存され、これを超えた場合は古い記事から削除されます。
202.<li>半角のカタカナは使わないでください。文字化けの原因になります。
203.<li>この掲示板に投稿された記事は、管理者が不都合と見なした場合に断りなく
削除または訂正することがあります。
204.<li>管理者は、編集キーの代わりに管理用パスワードを使用して、全ての記事の
修正や削除を行うことが出来ます。
205.</ul>
206.</td></tr></table>\n</body>\n</html>
207.END
208. exit;
209.}
210.
211.#---- ERRORの表示 ----
212.sub error {
213. $css ="h1 {color:red;}\nhr {width:50%;}\n";
214. &header($title, $default_css.$css, '');
215. print <<END;
216.<body>
217.<p> <a>戻る</a></p>
218.<div align="center">
219.<h1>ERROR !</h1>
220.<hr>\n<p><b>$_[0]</b></p>\n<hr>
221.</div>\n</body>\n</html>
222.END
223. exit;
224.}
225.__END__

ライブラリ「library.pl 」

1.#---- フォームデータ取得 ----
2.sub decode {
3. my $buf;
4. if ($ENV{'REQUEST_METHOD'} eq 'POST') {
5. read(STDIN, $buf, $ENV{'CONTENT_LENGTH'});
6. } else {
7. $buf = $ENV{'QUERY_STRING'};
8. }
9. foreach (split(/&/, $buf)) {
10. my ($key, $val) = split(/=/);
11. $val =~ tr/+/ /;
12. $val =~ s/%([0-9a-fA-F][0-9a-fA-F])/chr(hex($1))/eg;
13. &MjToCode($val);
14. $in{$key} = $val;
15. }
16.}
17.
18.#---- 特定の文字をコードに変換 ----
19.sub MjToCode {
20. foreach (@_) {
21. s/&/&/g;
22. s/ 23. s/>/>/g;
24. s/"/"/g;
25. s/\r\n//g;
26. s/\n//g;
27. }
28.}
29.
30.#---- 特定のコードを文字に変換 ----
31.sub CodeToMj {
32. foreach (@_) {
33. s//\n/g;
34. s/ 35. s/>/>/g;
36. s/"/\"/g;
37. s/&/\&/g;
38. }
39.}
40.
41.#---- HTMLヘッダー部の出力 ----
42.sub header {
43. print <<END;
44.Content-type: text/html
45.
46.<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
47.<html>
48.<head>
49.<meta content="text/html; charset=UTF-8">←自分の環境に合わせる
50.<title>$_[0]</title>
51.END

52. if ($_[1]) {
53. print <<END;
<--スタイルシート -->
54.<style type="text/css"></style>
56.END
57. }

58. if ($_[2]) {
59. print <<END;
<--ジャバスクリプト-->
60.<script type="text/javascript"><!--
61.$_[2]//-->
62.</script>
63.END
64. }
65. print qq(</head>\n);
66.}
67.1;


●●●解説●●●

スクリプト「bbs.cgi」の解説

3.#---- ライブラリの読み込み ----
4.require 'library.pl';#ライブラリの読み込み

→この4行目ではrequire関数で外部ファイルであるライブラリを呼び込んでいる。慣例として、ライブラリの拡張子には、.pl を付けることが多いようですが、.cgi を使っても構いません。

【ライブラリについて】

ライブラリ「library.pl」
1.#---- フォームデータ取得 ----
2.sub decode {
 ・・・
16.}
17.
18.#---- 特定の文字をコードに変換 ----
19.sub MjToCode {
 ・・・
28.}
29.
30.#---- 特定のコードを文字に変換 ----
31.sub CodeToMj {
 ・・・
39.}
40.
41.#---- HTMLヘッダー部の出力 ----
42.sub header {
 ・・・
66.}
67.1;

注1)殆どのスクリプトで1行目に書いていた #!/usr/local/bin/perl という記述しない事
注2)ライブラリの最後に行に 1; と記述する事

【全体の流れ】
このスクリプト bbs.cgi の構造はライブラリ library.pl を読み込んだ後、「グローバル変数の設定」 を行い、次の23~28行目の 「基本プログラム」 の部分から、各サブルーチンを呼び出している。

・グローバル変数の設定
このスクリプトで使う変数を定義しているもので、このスクリプトを使うユーザーが任意に設定変更を行えるように、最初にまとめて定義している。
7.$maxdata →投稿記事の最大保存数、
8.$maxview →1ページに表示する投稿記事数
9.$pass →管理者用パスワード、
10.$home →この掲示板の閲覧後に戻すURL
11.$kiji_bg →投稿記事の題名などを表示する行の背景色
など

・スタイルシートの定義
どのページを表示する場合にも使うスタイルシートを定義している。

16.$default_css = <<END;
17.body {background:#eeeeee;}←ブラウザ全体のスタイル調整
18.a:link, a:visited {color:blue;}→
19.a:hover, a:active {color:red;}
20.END

・リンクの4つの状態に対して文字色を指定するには
a:link { color: #0000ff; } … 未訪問のリンク
a:visited { color: #000080; } … 訪問済みのリンク
a:hover { color: #ff0000; } … ポイント時のリンク
a:active { color: #ff8000; } … 選択中のリンク


・基本プログラム
23.&decode;
24.if ($in{'mode'} eq 'help') {&help;}
25.&dataRead;
26.if ($in{'mode'} eq 'write') {&write;}
27.elsif ($in{'mode'} eq 'edit' || $in{'mode'} eq 'del') {&edit;}
28.&display;

プログラムの全体の流れを記述しています。この CGIスクリプトは、処理内容毎にサブルーチンにしておいて、この基本プログラム部分で全体の流れを示し、スクリプトを見直し易くしている。
単純にこの bbs.cgi が呼び出された場合には、mode というキーのハッシュは存在しないため、decode の後で dataRead が行われ、次に display サブルーチンが呼び出されて HTML の画面が生成されることに成る。

初めに、
23行目の&decodeでdecode サブルーチンを呼び出し、フォームから送られてくるデータをハッシュ %in() に格納します。このスクリプトの動作モードを指定するキーワードとして、mode という言葉を使っているので、$in{'mode'} の値を調べれば、処理の分岐を行えるようになっています。

処理の分岐
1)$in{'mode'}== 'help' の場合→「使い方」 の画面を表示して終了します(24行目)。
2)'help' 以外→&dataReadでデータファイルを読み込む(25行目)。
3)$in{'mode'} =='write' の場合→write サブルーチンの処理をする(26行目)。
4)値が'write'以外で、'edit'または'del'ならば→editサブルーチンを呼び出して、投稿済記事の 「修正」あるは「削除」の処理をする(27行目)。

基本プログラムの終わりは、28.&displayより、displayサブルーチンを呼び出し、画面上部に投稿用のフォーム、その下に過去ログを表示するHTML文書を生成して終わる。

・特殊リテラル
スクリプト「bbs.cgi」の最終行(231行目)にある __END__ 特殊リテラルと呼ばれるもので、これ以降はコメントとして無視されるという働きをします。

ライブラリlibrary.plの解説
1.#---- フォームデータ取得 ----
2.sub decode {
3. my $buf;
4. if ($ENV{'REQUEST_METHOD'} eq 'POST') {
5. read(STDIN, $buf, $ENV{'CONTENT_LENGTH'});
6. } else {
7. $buf = $ENV{'QUERY_STRING'};
8. }
9. foreach (split(/&/, $buf)) {
10. my ($key, $val) = split(/=/);
11. $val =~ tr/+/ /;
12. $val =~ s/%([0-9a-fA-F][0-9a-fA-F])/chr(hex($1))/eg;
13. &MjToCode($val);
14. $in{$key} = $val;
15. }
16.}
17.

decodeサブルーチン内の3行目と10行目にあるmyは、
変数の局所化を意味し、ローカル宣言したブロック内でのみ有効、ブロックの外に出ると変数は消滅する。この為、library.pl を利用するスクリプトの中で、同じ名前の変数が使われても問題は起こらない。
また、decodeサブルーチンは、以前にも説明した様にフォームデータの取得の処理をしているが、これまでのスクリプトと違うのは、13行目で MjToCode サブルーチンを呼び出しと、CodeToMj サブルーチンについて説明します

・MjToCode サブルーチンや CodeToMj サブルーチンの解説
18.#---- 特定の文字をコードに変換 ----
19.sub MjToCode {
20. foreach (@_) {
21. s/&/&/g;
22. s/ 23. s/>/>/g;
24. s/"/"/g;
25. s/\r\n//g;
26. s/\n//g;
27. }
28.}
29.
MjToCode サブルーチンは、
投稿された記事の中にあるかもしれない 「<」、「>」、「"」 というような文字を、特殊な書き方に変換するためのものです。即ち、これらの文字はHTMLタグ用の文字であるため、このような文字そのままではHTML文書に書くことはできません。これらを文字として表示するには、「<」、「>」、「"」 と書く、また 「&」 は 「&」 と書くというのが HTML文書のルールなのです。このための変換が、21~24行目で行われています。
また、
記事に書かれているかも知れない改行文字を、HTML文書での に変換する処理を、25、26行目で行っています。

30.#---- 特定のコードを文字に変換 ----
31.sub CodeToMj {
32. foreach (@_) {
33. s//\n/g;
34. s/ 35. s/>/>/g;
36. s/"/\"/g;
37. s/&/\&/g;
38. }
39.}
CodeToMj サブルーチンは、
MjToCode サブルーチンで変換された特殊な書き方を元に戻す逆変換を行います。

■ HTMLヘッダー部の出力
41.#---- HTMLヘッダー部の出力 ----
42.sub header {
43. print <<END;
44.Content-type: text/html
45.
46.<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
47.<html>
48.<head>
49.<meta http-equiv="content-type" content="text/html; charset=UTF-8">
50.<title>$_[0]</title>
51.END
52. if ($_[1]) {
<!-- CSSを埋め込む-->
53. print <<END;
54.<style type="text/css"><!--
55.$_[1]--></style>
56.END
57. }
58. if ($_[2]) {
<!-- JavaScriptを埋め込む-->
59. print <<END;
60.<script type="text/javascript"><!--
61.$_[2]//-->
62.</script>
63.END
64. }
65. print qq(</head>\n);
66.}
67.1;
このサブルーチンは、サブルーチンdisplay、help、error から、&header($title, $default_css.$css, $java)の様に、3つの引数を付けて呼び出されます。第1引数はタイトル、第2引数はスタイルシートの内容、第3引数は javascript の関数定義。

引数は @_ という特殊な配列に積み上げられてサブルーチンに渡されます。第1引数は $_[0] で、第2引数は $_[1] で、第3引数は $_[2] で参照することができます。

43~51行目で、CGIヘッダー と HTML の <title>~</title> までを出力します。
52~57行目で、第2引数 $_[1] が空文字でない場合に、スタイルシートの文字列を出力します。
58~64行目で、第3引数 $_[2] が空文字でない場合に、javascriptの関数定義の文字列を出力します。
最後に65行目で </head> を出力します。このようにして、このサブルーチンを呼び出した側では、<body> タグから書き始めればよいことになります

■複数人によるファイルの同時読み書きするには
複数の人が同じCGIスクリプトファイルを同時に実行する場合は
CGIスクリプトファイルが同じでも、全く別々の環境で実行されますから、何の問題も起きません。しかし、データファイルを同時に操作する場合は、問題が起こる可能性があります。例えば、ファイルを書き込みモードで開くと、ファイルの中身は空にされるため、その直後に他の人が読み込みモードで同じファイルを開けば、空のファイルを読み出すことに成る。

30.#---- データファイル読み取り ----
31.sub dataRead {
32. if (-e $datafile) {
33. open(IN, $datafile);
34. @data = <IN>;
35. close(IN);
36. }
37.}
32行目にある if 文で、ファイルの存在を確認してからファイルを開くようにしています。ファイルが存在しない場合には、@data は空の配列となります。

39.#---- データファイル書き込み ----
40.sub dataWrite {
41. open(OUT, ">$datafile");
42. eval {flock(OUT, 2)} ; #排他ロック
43. print OUT @data;
44. close(OUT);
45.}
dataWrite サブルーチンでは、flock 関数を使ってファイルを排他ロックして、書き込みモードで開いている間は、他人が同じファイルを開けないようにしている。

eval { } は、ブロックの中のプログラムを実行、評価する関数で、ブロック内でエラーが発生してもプログラムは終了しません。そのため、使えない可能性のある関数を実行するときには、eval { } を使うようにします。但し、flock 関数が使えない場合には、42行目の記述は無意味になるため、上述のような不幸なタイミングに遭遇したら諦めてもらうことになります。また、flock 関数の第2引数に指定する番号2は、排他ロックを意味する。

■ 記事の投稿フォーム
102.#---- 投稿フォーム、過去ログの表示 ----
103.sub display {
変数$css にスタイルシートの文字列を格納している
104. $css = <<END;←ヒアドキュメント
 ・・・・・・・
111.END
javascriptの関数定義を変数 $java に格納する
112. $java = <<END;←ヒアドキュメント
113.function f1() {
114. if (document.forms[0].name.value=="" ||
document.forms[0].kiji.value=="") {
115. alert('名前と記事に入力が必要です'); return false;
116. } else { return true; }
117.}
118.function f2(p){
 ・・・・・・
}
122.function f3() {
・・・・・・・
}
127.END
128.&header($title, $default_css.$css, $java);
129. print <<END;
130.<body>
131.<p> <a href="$home">HOMEへ戻る</a> 
132.<a href="$thisfile?mode=help">使い方</a></p>
133.<h1 align="center">$title</h1>

134.<form action="$thisfile" method="post" onsubmit="return f1()">
135.<input type="hidden" name="mode" value="write">
136.<input type="hidden" name="num" value="$v0">
137.<table align="center">
138.<tr><td>名前:</td>
139.<td><input type="text" name="name" value="$v1"></td></tr>
140.<tr><td>題名:</td>
141.<td><input type="text" size="40" name="ttl" value="$v2"></td></tr>
142.<tr><td>記事:</td>
143.<td><textarea cols="60" rows="4" name="kiji">$v3</textarea>
</td></tr>
144.<tr><td>編集キー(記事の修正、削除用):
145.<input type="password" name="pwd" value="$v4">  
146.<input type="submit" value="投稿する"></td></tr>
147.</table>
148.</form>

149.<form action="$thisfile" method="post">
150.<input type="hidden" name="page" value="">
151.</form>
152.END
・・・・
176.<form action="$thisfile" method="post" onsubmit="return f3()">
177.記事番号<input type="text" name="num" size=3>
178.編集キー<input type="password" name="pwd">
179.<select name="mode">
180.<option value="edit" selected>修正
181.<option value="del">削除
182.</select> 
183.<input type="submit" value="実行">
184.</form>\n</td></tr>\n</table>
185.</body>\n</html>
186.END
187. exit;
【解説】
→112行の$java = <<END;~127行のEND7までのjavascriptの関数を、変数 $javaに格納している。これらは、128行目の&header() 呼び出しの引数に使われ、HTML のヘッダー部に記述されます。第2引数の指定に使われている 「. 」 は、文字列結合演算子です。

→このスクリプトが出力する HTML文書には、 <form・・・>~</form> で表される3つのフォームがあります。最初に現われるのが134~148行目で、この最初のフォームが 「投稿フォーム」 を形作っています。 javascriptでは、フォームは forms という配列で表現することができ、現われる順に forms[0]、forms[1] として識別できます。

→146行目の 「投稿する」 と表示された submit ボタンを押すと、データの送信を開始しようとするイベントが発生し、134行目に書かれている onsubmit イベントハンドラ が、113~117行目の f1() 関数を起動します。

→この関数は、「名前」と「記事」の入力状況を調べ、入力がなければ警告を出して false を返し、入力がある場合には true を返します。従って、onsubmit="return f1()" としているため、onsubmit には f1() の戻り値が返され、true の場合のみデータの送信が実行され、false の場合には submit ボタンの操作が無効になります。

→f1() 関数による入力の有無を確認する処理などは、javascript を使わずに、CGIスクリプトで行っても良さそうですが、このように javascript で制御すれば僅かでもサーバーの負荷を減らすことになります。 JavaScript

→132行目のリンクの貼り方(<a href="$thisfile?mode=help">)はGET でデータを渡すことに成る。

→変数$v0, $v1, $v2, $v3, $v4 は、投稿済記事の修正の場合に値がセットされる変数で、新規投稿の場合にはこれらには空文字がセットされます

■ 過去ログの表示
102.#---- 投稿フォーム、過去ログの表示 ----
113.function f1() {
 ・・・・・・
117.}
118.function f2(p){
118. document.forms[1].page.value = p;
120. document.forms[1].submit();
 
}
122.function f3() {
if (document.forms[2].num.value=="" ||
document.forms[2].pwd.value=="") {
124. alert('記事番号と編集キーに入力が必要です'); return false;
125. } else { return true; }
}

153~167行目が過去ログ表示の部分
153. $p = $in{'page'} || 1;
154. $top = --$p * $maxview;
155. for ($i=$top; $i<$top+$maxview; $i++) {
156. if ($i==@data) {last;}
157. ($num, $date, $name, $ttl, $kiji) = split(/<>/, $data[$i]);
158. print <<hr>
160.<table align="center" class="tbl">
161.<tr class="t1"><th width="6%">$num:</th>
162.<td width="20%">$name</td><td width="40%">$ttl</td>
163.<td align="center"><small>$date</small></td></tr>
164.<tr><td class="t2">$kiji</td></tr>
165.</table>
166.END
167. }
・・・・・
ページ移動
168. print qq(<hr>\n<table align="center" cellpadding=4 class="tbl">\n);
169. print qq(<t><td width="45%">【ページ移動】);
170. for ($i=0, $p=1; $i<@data; $i+=$maxview, $p++) {
171. if ($i==$top) {print qq(  $p);}
172. else {print qq(  <a href="javascript:f2($p)">$p</a>);}← JavaScript擬似URL
173. }

174. print <<END;
175.</td>\n<td class="t3">
176.<form action="$thisfile" method="post" onsubmit="return f3()">
177.記事番号<input type="text" name="num" size="3">
178.編集キー<input type="password" name="pwd">
179.<select name="mode">
180.<option value="edit" selected>修正
181.<option value="del">削除
182.</select> 
183.<input type="submit" value="実行">
184.</form>\n</td></tr>\n</table>
185.</body>\n</html>
186.END


【解説】
→$maxviewには1ページに表示する件数が設定してある(11.$maxview = 5;)。なぜなら、保存してあるログの件数が多くなると、それらを1ページに全て表示したのでは見難くなるため

→153行目の$pは、最初のページを1とするページ番号。
154行目の$topは、そのページの最初に表示するログが @data の何番目かを表す変数。
よって、次のfor文は、@dataの$top 番目から$maxview 件数を読み出して表示している。

→172行目のelse {print qq( <a href="javascript:f2($p)">$p</a>);}のリンク部分はjavascriptのf2(p)関数を起動させる。この関数は、forms[1] 即ち 149~151行目のフォームの中にあるpage 名付けられた input タグの属性 value を、引数の値 p に設定した後、submit メソッドでデータ送信を実行します。この様なリンクの貼り方をJavaScript擬似URLと言う

→176~184行目のフォームは、「記事番号」と「編集キー」を入力させてから、投稿済記事の 「修正」 または「削除」の 「実行」を行うためのものです。
「投稿フォーム」の「投稿する」 ボタンの場合と同様に、javascript の関数を使って、「記事番号」 と「編集キー」 が入力済かどうかを確認した後で、データの送出が行われます。

→183行目のsubmit ボタンを押すと、176行目に書かれているonsubmitイベントハンドラが、122~126行目の f3() 関数を起動します。この関数は、「記事番号」 と 「編集キー」 の入力状況を調べ、true または false を返します。その戻り値が、onsubmit イベントハンドラに返され、true の場合のみデータの送信が実行され、false の場合には submit ボタンの操作が無効になります

■ 投稿記事の保存処理
投稿用フォームから送られてくるデータは、名前、題名、記事、編集キーの4つの入力データと、mode および num の隠しデータです。 新規投稿の場合はnum の値は空文字、修正投稿の場合は num の値は修正しようとする記事の番号を表すようにしています。50行目のif文で、新規投稿か、修正投稿かを判断して処理を分岐しています。

尚、投稿記事をデータファイルに保存するときの形式は、「番号<>日付<>名前<>題名<>記事<>編集キー\n」 とし、新しい投稿記事はファイルの先頭に追加するようにします。区切りの文字列として 「<>」 を使っているのは、若しこれと同じ文字列が投稿記事の中に現れても、それは 「<>」 と変換され、この文字列 「<>」 がデータの一部として存在することは有り得ないからです。

47.#---- 投稿記事の保存処理 ----
48.sub write {
49. $name=$in{'name'}; $ttl=$in{'ttl'}; $kiji=$in{'kiji'}; $pwd=$in{'pwd'};
50. if ($in{'num'}) {
51. # 記事の修正書き込み
52. for ($i=0; $i<@data; $i++) {
53. ($num, $date) = split(/<>/,$data[$i]);
54. if ($in{'num'}==$num) {
55. $data[$i] = "$num<>$date<>$name<>$ttl<>$kiji<>$pwd\n";
56. $in{'page'} = int($i/$maxview) + 1;
57. last;
58. }
59. }
60. } else {
61. # 記事の新規書き込み
62. @youbi = ('日', '月', '火', '水', '木', '金', '土');
63. ($sec, $min, $hour, $day, $mon, $year, $wkday) = localtime(time);
64. $date = sprintf("%4d/%02d/%02d(%s) %02d:%02d:%02d",
65. $year+1900, ++$mon, $day, $youbi[$wkday], $hour, $min, $sec);
66. ($num) = split(/<>/, $data[0]);
67. $num++;
68. unshift(@data, "$num<>$date<>$name<>$ttl<>$kiji<>$pwd\n");
69. while (@data>$maxdata) { pop(@data);}
70. }
71. &dataWrite;
72.}
→52~59行目が修正投稿の処理で、ここでは、保存データの配列 @data を調べて一致する記事番号を探し出し、名前、題名、記事、編集キー を書き換えれば良いわけです。

実際にはデータ数だけ回す for ループを作り、$data[$i] が該当することが分かったなら、この $data[$i] を書き替えて for ループを抜け、71行目でデータファイルを書き替えています。

56行目は、「過去ログの表示」 でこのデータが表示されるようにするページ番号処理です。

62~69行目が新規投稿の処理で、ここでは、記事番号の採番と日付の取得が必要です。

日付と時刻は、例題2の 「時間処理の関数」 で説明したように、time 関数で取得した1970年1月1日の0時0分0秒から現在の時刻までの秒数を、localtime 関数に引数として渡して取得します。


63行目で localtime(time) の演算結果を、($sec, $min, $hour, $day, $mon, $year, $wkday) で受け取っているため、秒、分、時、日、月、年、曜日 の値が、夫々の変数に代入されます。64行目では、sprintf 関数を使って、フォーマットした文字列を $date に代入しています。sprintf 関数の第1引数 "%4d/%02d/%02d(%s) %02d:%02d:%02d" でフォーマット形式を指定して、「2007/11/04(日) 10:05:02」 というような文字列を作り出しています。

新しい投稿記事はファイルの先頭に追加するようにします。従って、$data[0] が最新の過去ログですから、66行目でこのデータの記事番号を調べ、67行目でそれに 1 を加えて、新規投稿記事の番号としています。

68行目の unshift(@data,"$num<>$date<>$name<>$ttl<>$kiji<>$pwd\n"); は、unshift 関数で、配列 @data の先頭に新規の投稿記事データを追加しています。69行目は、データの数が最大保存数を超えた場合に、古いデータを削除するための記述で、ここでは pop 関数を使っています

■ 投稿記事の修正、削除
74.#---- 投稿記事の修正、削除 ----
75.sub edit {
76. for ($i=0; $i<@data; $i++) {
77. ($num, $date, $name, $ttl, $kiji, $pwd) = split(/<>/, $data[$i]);
78. chomp $pwd;
79. if ($num==$in{'num'}) {
80. if ($in{'pwd'} eq $pass || $in{'pwd'} eq $pwd) {
81. if ($in{'mode'} eq 'edit') {
82. # 記事の修正フォーム表示
83. $v0=$num; $v1=$name; $v2=$ttl; $v3=$kiji; $v4=$pwd;
84. &CodeToMj($v1, $v2, $v3, $v4);
85. } else {
86. # 記事の削除
87. splice(@data,$i,1);
88. &dataWrite;
89. }
90. } else {
91. $msg = ($pwd) ? '編集キーが違います':
92. 'この記事には編集キーが設定されていません';
93. &error($msg);
94. }
95. last;
96. }
97. }
98. if ($i==@data) {&error("記事番号 $in{'num'} が見つかりません");}
99. $in{'page'} = int($i/$maxview) + 1;
100.}

 フォームからのデータは、修正の場合は mode=edit、削除の場合は mode=del として、記事番号 num と編集キー pwd が送られてきます。記事番号と編集キーの確認の後、修正の場合は投稿用フォームに該当記事の内容を表示して修正の入力を求めるようにします。削除の場合は、保存用データファイルから該当記事を削除します。

for ループ内の最初の77行目で、保存データの配列 @data の各要素を、split 関数を使って $numや $date などに分解し、78行目で $pwd の後ろに付いている改行記号を取り除きます。79行目で、この $num がフォームから送られてきた $in{'num'} に一致するかが調べられ、一致するものが見付かるまで for ループが繰り返されます。

一致するものが見つからなかった場合は、$i が配列 @data の要素数となってループを抜けますから、98行目でエラー表示のサブルーチン error に飛ばします。

該当する記事番号が見つかれば、80行目で編集キーの確認を行います。フォームから送られてきた $in{'pwd'} が、管理者用のパスワードか、データファイルに保存されていた編集キーと一致するか否かで分岐しています。

一致しない場合には、93行目で error サブルーチンに飛ばします。一致した場合には、修正または削除の処理を行います。87, 88行目が削除の処理です。

87行目の splice 関数は、@data の $i 番目の要素から1つの要素を削除します。88行目で、この配列 @data をデータファイルに書き込み、95行目の last; で for ループを抜け、98行目に移ります。

83, 84行目が修正のための処理です。 83行目の $v0 ~ $v4 は、「投稿フォーム」 に該当記事の内容を表示させるための変数です。データファイルには、& < > " というような文字は & < > " に、また改行文字は に変換されて保存されています。このため、84行目の処理で、$v1 ~ $v4 に対して、この逆変換の処理を library.pl の中にある CodeToMj サブルーチンに行わせています。
コメント

リンクカウンター/perlのCGI講座

2018年04月25日 | perl
アクセスカウンター

【環境条件】
Eclipse 4.4(ルナ)
XAMPP 1.8.3 
Perlは既にインストール済み
CGIを使用する時は、必ずApachを起動する

■リンクカウンター
これまでに勉強した 例題1~例題4 へのリンクが張られています。夫々のページへジャンプする度に link.cgi が介在して、夫々のページへのアクセス回数がカウントアップされた後に、夫々のページが表示されます。リンクカウンターの仕掛けは、link.cgi にによって実現されていますが、link.html を見ている人には、このような裏の仕掛けは見えません。

では、最初の画面「link.html」を表示させて見る。

→リンクが貼られた例題1~例題4が表示されたら、どれでも好きな所を選んで見る。
1)例題1


2)例題2


3)例題3


4)例題4


次にリンクカウンターが仕掛けられた閲覧記録画面「link.cgi」を起動する

閲覧記録画面
パス入力


閲覧記録


■スクリプト
リンクカウンターでは、2つのファイルを使う。一つは、リンクを貼ったlink.htmlと、リンクカウンターの仕掛けがあるlink.cgiである。

スクリプト「link.html」
1.<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
2.<html>
3.<head>
4.<meta http-equiv="content-type" content="text/html; charset=UTF-8">
5.<title>リンクのページ</title>
6.</head>
7.<body link="blue" vlink="blue">
8.<h1>リンクのページ</h1>
9.<ul>
10.<li><a href="link.cgi?url=html.cgi">例題1</a>
11.<li><a href="link.cgi?url=time.cgi">例題2</a>
12.<li><a href="link.cgi?url=dice.cgi">例題3</a>
13.<li><a href="link.cgi?url=env.cgi">例題4</a>
14.</ul>
15.</body>
16.</html>

スクリプト「link.cgi」
1.#!/usr/local/bin/perl
2.
3.$thisfile = 'link.cgi';
4.$datafile = 'link.dat';
5.$pass = '0123';#パスワード
6.
#---- 基本プログラム ----
7.&decode;
8.if ($in{'url'}) { &url; }
9.elsif (exists $in{'pw'}) { &admin; }
10.else { &main; }
11.exit;
12.
13.#---- フォームデータ取得 ----
14.sub decode {
15. if ($ENV{'REQUEST_METHOD'} eq 'POST') {
16. read(STDIN, $buf, $ENV{'CONTENT_LENGTH'});
17. } else {
18. $buf = $ENV{'QUERY_STRING'};
19. }
20. foreach (split(/&/, $buf)) {
21. ($key, $val) = split(/=/);
22. $val =~ tr/+/ /;
23. $val =~ s/%([0-9a-fA-F][0-9a-fA-F])/chr(hex($1))/eg;
24. $in{$key} = $val;
25. }
26.}
27.
28.#---- リンク先の計数と表示 ----
29.sub url {
30. $file = -e $datafile;
31. if ($file) {
32. open(IN, $datafile);
33. while (<IN>) {
34. chomp;
35. ($url, $count) = split(/,/);
36. if ($url eq $in{'url'}) { $count++; $flag = 1; }
37. push(@data, "$url,$count\n");
38. }
39. close(IN);
40. }
41. if ($flag != 1) { push(@data, "$in{'url'},1\n"); }
42. open(OUT, ">$datafile");
43. print OUT @data;
44. close(OUT);
45. if (!$file) { chmod(0666, $datafile); }
46. # リンク先を表示
47. print "location: $in{'url'}\n\n";
48.}
49.
50.#---- 管理画面への入り口 ----
51.sub main {
52. &html('この先は管理人しかは入れません');
53. print <<END;
54.<form action="$thisfile" method="post">
55.パスワード <input type="password" size="10" name="pw">
56.<input type="submit" value="管理画面へ">\n</form>
57.</body>\n</html>
58.END
59.}
60.
61.#---- 管理画面 ----
62.sub admin {
63. if (!$in{'pw'}) {
64. $s1 = 'パスワードが入力されていません';
65. } elsif ($in{'pw'} ne $pass) {
66. $s1 = 'パスワードが違います';
67. } elsif (! -e $datafile) {
68. $s1 = '閲覧記録はありません';
69. } elsif ($in{'del'}) {
70. unlink $datafile;
71. $s1 = '閲覧記録を削除しました';
72. }
73. if ($s1) {
74. &html($s1);
75. } else {
76. open(IN, $datafile);
77. @data = <IN>;
78. close(IN);
79. &html('閲覧記録');
80. print "<table border="1">\n";
81. print "<tr><th>URL</th><th>回数</th></tr>\n";
82. foreach (sort @data) {
83. chomp;
84. ($url, $count) = split(/,/);
85. print "<tr><td>$url</td><th>$count</th></tr>\n";
86. $total += $count;
87. print <<END;
88.<tr><th>Total</th><th>$total</th></tr>\</table>
89.<form action="$thisfile" method="post">
90.<input type="hidden" name="pw" value="$pass">
91.<input type="hidden" name="del" value="1">
92.<input type="submit" value="記録削除">\n</form>
93.END
94. }
95. }
96. print <<END;
97.<form action="$thisfile">
98.<input type="submit" value=" 戻る ">\n</form>
99.</body>\n</html>
100.END
101.}
102.
103.#---- HTML 書き出し ----
104.sub html {
105. print <<END;
106.Content-type: text/html\n
107.<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
108.<html>\n<head>
109.<meta content="text/html;">
110.<title>閲覧記録管理</title>
111.</head>\n<body>
112.<h1>閲覧記録管理</h1>
113.<p><b>$_[0]</b></p>
114.END
115.}
●●● 解説 ●●●

1)スクリプト「link.html」
■ リンクデータ
先ず、link.html の中でどのようにリンクデータをlink.cgiへ送られるか?
→10~13行目の
  • 例題✗はlink.cgi を起動し、データとして url=html.cgi を渡している。また、データの渡し方を指定できない時は、デフォルトの GET でデータが渡される。更に、データのパス指定はlink.cgi からの 「相対パス」 で指定するか、url=http://localhost/samples/html.cgi と 「絶対パス」 で指定する。

    2)スクリプト「link.cgi」
    ■ リンクカウンタの動作原理
    1)link.html の中で、"link.cgi?url=html.cgi" としてリンクが張られ、link.cgi url=html.cgi というデータが送られてきたときの link.cgi での処理を見てみよう。

    基本プログラムの7行目の&decodeで、
    decodeサブルーチンを呼び出し、link.htmlから送られてきたデータをハッシュ %in に格納する。今回は、キー url の値 $in{'url'} が 'html.cgi' という文字列としてセットされる。

    基本プログラムの8行目のif ($in{'url'}){&url;}で、キー値が真に成り、urlサブルーチンが呼び出される。
    【urlサブルーチン】
    28.#---- リンク先の計数と表示 ----
    29.sub url {
    30. $file = -e $datafile;#ファイルテスト演算子
    31. if ($file) {
    32. open(IN, $datafile);
    33. while (<IN>) {
    34. chomp;
    35. ($url, $count) = split(/,/);
    36. if ($url eq $in{'url'}) { $count++; $flag = 1; }
    37. push(@data, "$url,$count\n");
    38. }
    39. close(IN);
    40. }
    41. if ($flag != 1) { push(@data, "$in{'url'},1\n"); }
    42. open(OUT, ">$datafile");
    43. print OUT @data;
    44. close(OUT);
    45. if (!$file) { chmod(0666, $datafile); }
    46. # リンク先を表示
    47. print "location: $in{'url'}\n\n";
    48.}
    urlサブルーチンの処理は、
    変数$datafile に代入した html.cgi へのリンク回数を 1 だけ加算して書き直すための処理。
    $datafile には、リンクされた順に 「リンク先」 と 「リンク回数」 をコンマで区切って、次のようにリンク先毎に1行に記録することに決めてあります。


    33~38行目の while ループでは
    $_ に各行が読み出されます。$_ の最後にある改行文字を chomp 関数で取り除き、split 関数 で $_ を区切り文字 「, 」 で分割して $url と $count に取り出しています

    37行目の push 関数 は、
    第1引数で指定した配列の末尾に、第2引数で指定した値を付け加える関数です。結局、この33~38行目の while ループでは、36行目でアクセス回数をインクレメントすることにより、ファイルの内容を書き換えるためのデータを配列 @data として準備したことになります。

    41行目の処理は、
    初めてのリンク先は $datafile の中に該当するデータが無いため、このリンク先をデータとして追加する処理です。

    42~44行目の処理は、
    @data を $datafile に書き込み、$datafile を新規に作成したときには、chmod 関数 で パーミッションを 666 に変更しています。

    47行目の print "location: $in{'url'}\n\n"; は、
    サーバーに対して 「この URL を表示してください」 と指示しています。この記述で改行記号が2つ付いていることに注意してください。CGI ヘッダーの出力と同様に2行目を空行にして出力しなければなりません。

    ■ 管理画面への入り口
    「link.cgi」を動作させると、基本プログラムより、「管理画面」 への入り口が表示されます。
    3.$thisfile = 'link.cgi';
    4.$datafile = 'link.dat';
    5.$pass = '0123';#パスワード
    #---- 基本プログラム ----
    7.&decode;
    8.if ($in{'url'}) { &url; }
    9.elsif (exists $in{'pw'}) { &admin; }
    10.else { &main; }
    11.exit;
    12.
    5行目に $pass として、管理者用のパスワードを設定しています。'Open Sesame' と設定しようと、'開けゴマ' と指定しようと、任意の文字列を設定します。

    →link.cgi が単純に起動されれば、url=xxx とか pw=xxx というようなデータは送られてきてはいないため、main サブルーチンが呼び出されます。
    50.#---- 管理画面への入り口 ----
    51.sub main {
    52. &html('この先は管理人しかは入れません');
    53. print <<END;
    54.<form action="$thisfile" method="post">
    55.パスワード <input type="password" size="10" name="pw">
    56.<input type="submit" value="管理画面へ">\n</form>
    57.</body>\n</html>
    58.END
    59.}

    52行目で html サブルーチンを呼び出し、'この先は管理人しかは入れません' というメッセージを表示する HTML の前半を出力し、ヒアドキュメントで 54~57行目のフォームを出力しています。このフォームで、パスワードの入力枠と 「管理画面へ」 という submit ボタンが作られます
    ■ 管理画面
    「管理画面」 を表示しているのは、この admin サブルーチンです。このサブルーチンは、9行目で (exists $in{'pw'}) が真の場合に呼び出されます。即ち、exists 関数を使って、ハッシュ %in の中に pw というキーが存在するかどうかを調べ、存在する場合にのみこのサブルーチンが呼び出されるようになっています。この管理画面では、「閲覧回数の集計」 を見たり、「閲覧記録をクリア」 することもできるようにしています
    61.#---- 管理画面 ----
    62.sub admin {
    63. if (!$in{'pw'}) {
    64. $s1 = 'パスワードが入力されていません';
    65. } elsif ($in{'pw'} ne $pass) {
    66. $s1 = 'パスワードが違います';
    67. } elsif (! -e $datafile) {
    68. $s1 = '閲覧記録はありません';
    69. } elsif ($in{'del'}) {
    70. unlink $datafile;
    71. $s1 = '閲覧記録を削除しました';
    72. }
    73. if ($s1) {
    74. &html($s1);←htmlサブルーチンの呼び出し
    75. } else {
    76. open(IN, $datafile);
    77. @data = <IN>;
    78. close(IN);
    79. &html('閲覧記録');←htmlサブルーチンの呼び出し
    80. print "<table border="1">\n";
    81. print "<tr><th>URL</th><th>回数</th></tr>\n";
    82. foreach (sort @data) {
    83. chomp;
    84. ($url, $count) = split(/,/);
    85. print "<tr><td>$url</td><th>$count</th></tr>\n";
    86. $total += $count;
    87. print <<END;
    88.<tr><th>Total</th><th>$total</th></tr>\</table>
    89.<form action="$thisfile" method="post">
    90.<input type="hidden" name="pw" value="$pass">
    91.<input type="hidden" name="del" value="1">
    92.<input type="submit" value="記録削除">\n</form>
    93.END
    94. }
    95. }
    96. print <<END;
    97.<form action="$thisfile">
    98.<input type="submit" value=" 戻る ">\n</form>
    99.</body>\n</html>
    100.END
    101.}
    63行目の (! $in{'pw'}) は、キー pw の値が空白なら真となる条件式です。空白は偽と見做されます。これに否定演算子 ! を付けて、「空白なら」 という条件式にしています。

    67行目でも否定演算子 ! を使っています。-e はファイルの存在を調べるファイルテスト演算子で、ファイルが存在すれば真を返します。これを否定して、「ファイルが無ければ」 という条件式にしています。

    67行目の条件式は、キー del の値が真ならば、という式です。「記録削除」 ボタンが押されると、del=1 というデータが送られるようにしてあるため、このデータを受けたときには unlink 関数で $datafile を削除してしまいます。

    63~72行目の何れかの条件が真の場合には、変数 $s1 にメッセージをセットします。従って、73行目の条件式が真となり、html サブルーチンで HTML の前半を出力して、96行目以降の処理を行って終了します。
    【htmlサブルーチン】
    103.#---- HTML 書き出し ----
    104.sub html {
    105. print <<END;
    106.Content-type: text/html\n
    107.<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    108.<html>\n<head>
    109.<meta content="text/html;">
    110.<title>閲覧記録管理</title>
    111.</head>\n<body>
    112.<h1>閲覧記録管理</h1>
    #----引数表示----
    113.<p><b>$_[0]</b></p>
    114.END
    115.}


    上記以外の場合には、76行目以降の処理を行います。先ず、76~78行目でデータを @data に読み取り、html サブルーチンで HTML の前半を出力します。80~88行目はデータを表の形で出力しています。出力に際しては、URL をアルファベット順にソートして(82行目の sort 関数による)、合計回数を計算して(86行目)表の最後に書いています。

    89~92行目の出力で、「記録削除」 ボタンのあるフォームをを作っています。このフォームには、pw と del のコントロールを隠しておきます。

    96~100行目のヒアドキュメントによる出力は、「管理画面への入り口」 に戻すボタンを作っています。「戻す」 ボタンを押したときには、単純に $thisfile(link.cgi)にリンクを張ったときと同様の動作となり、次には main サブルーチンが呼び出されることになります。

  • コメント

    アンケート2-(サブルーチン編)

    2018年04月08日 | perl
    アンケート2-(サブルーチン編)

    【環境条件】
    Eclipse 4.4(ルナ)
    XAMPP 1.8.3 
    Perlは既にインストール済み
    CGIを使用する時は、必ずApachを起動する

    ---アンケート2編---

    【操作画面】
    1)アンケートの質問画面

    2)回答画面

    3)アンケートの管理画面
    パスワードを入れないと表示されない。
    ここでは集計結果表示と集計リセットが出来る。


    【アンケート2】
    前回作ったシンプルなアンケートはenquete-1.html、enquete-2.cgi、enquete-3.cgi の3つのファイルを使っていましたが、今回のアンケート2では1つのファイル CGI で作成し直した。

    ■スクリプト「enquete.cgi 」
    1.#!/usr/local/bin/perl
    2.
    3.$thisfile = 'enquete.cgi';
    4.$datafile = 'enquete.dat';
    #パスワード設定
    5.$pass = '0123';
    6.@ans = ('犬', '猫', 'どちらも嫌い');#配列
    7.$ttl = '犬と猫のどちらが好き?';
    8.
    9.&decode;←サブルーチンを実行する命令
    10.if ($in{'mode'} eq 'kaitou') { &kaitou; }
    11.elsif ($in{'mode'} eq 'admin') { &admin; }
    12.else { &situmon; }
    13.exit;
    14.
    15.#---- フォームデータ取得 ----
    16.sub decode {
    17. if ($ENV{'REQUEST_METHOD'} eq 'POST') {
    18. read(STDIN, $buf, $ENV{'CONTENT_LENGTH'});
    19. } else {
    20. $buf = $ENV{'QUERY_STRING'};
    21. }
    22. foreach (split(/&/, $buf)) {
    23. ($key, $val) = split(/=/);
    24. $in{$key} = $val;
    25. }
    26.}
    27.
    28.#---- 質問の表示 ----
    29.sub situmon {
    30. &html('質問', $ttl, '下から選んでご回答ください');
    31. print <<END;
    32.<form action="$thisfile" method="post">
    33.<input type="hidden" name="mode" value="kaitou">
    34.<ul>
    35.END
    36. for ($i=0; $i<@ans; $i++) {
    37. print qq(<li><input type="radio" name="ans" value="$i">$ans[$i]\n);
    38. }
    39. print <<END;
    40.</ul>
    41.<input type="submit" value="回答する">\n</form>
    42.<form action="$thisfile" method="post">
    43.<input type="hidden" name="mode" value="admin">
    44.パスワード <input type="password" size="10" name="pw">
    45.<input type="submit" value="管理画面">\n</form>
    46.</body>\n</html>
    47.END
    48.}
    49.
    50.#---- 回答の計数と表示 ----
    51.sub kaitou {
    52. if (exists $in{'ans'}) {
    53. @n = &readData($data);
    54. $i = $in{'ans'};
    55. $n[$i]++;
    56. open(OUT, ">$datafile");
    57. foreach (@n) { print OUT "$_\n"; }
    58. close(OUT);
    59. if (!$data) { chmod(0666, $datafile); }
    60. $msg = "あなたは 「$ans[$i]」 の$n[$i]人目です";
    61. } else {
    62. $msg = "どれかを選んでからお答えください";
    63. }
    64. &html('回答', 'ご回答の表\示', $msg);
    65. &back;
    66.}
    67.
    68.#---- 管理画面 ----
    69.sub admin {
    70. if (!$in{'pw'}) {
    71. $msg = 'パスワードが入力されていません'; $flag=0;
    72. } elsif ($in{'pw'} ne $pass) {
    73. $msg = 'パスワードが違います'; $flag=0;
    74. } elsif ($in{'del'}) {
    75. unlink $datafile;
    76. $msg = '集計ファイルを削除しました'; $flag=1;
    77. } else {
    78. $msg = '集計結果'; $flag=1;
    79. }
    80. &html('集計', '集計結果', $msg);
    81. if ($flag == 1) {
    82. @n = &readData($data);
    83. print "<ul>\n";
    84. $i = 0;
    85. foreach (@ans) { print "<li>$_ : $n[$i++]\n"; }
    86. print "</ul>\n";
    87. if ($data) { print <
    ●●● 解説 ●●●

    ■サブルーチンについて
    ・サブルーチン定義
    sub サブルーチン名 { ・・・ }

    ・サブルーチンの呼び出し
    &サブルーチン名;

    ・サブルーチンに引数を渡すには
    &サブルーチン名( 引数1, 引数2, ・・・ 引数n );
    但し、引数は 「@_ 」 という名前の配列でサブルーチンに渡されます。配列 @_ の添え字は 0 から始まり、最後の添え字は 「$#_ 」 という変数に格納されます。従って、サブルーチンの中では $_[0]、$_[1]、 ・・・ $_[$#_] で、各引数の値を参照したり、逆に各引数の値を設定したりすることができます。また サブルーチンからの戻り値は、サブルーチンの中で return 関数を使って返す
    Perlのサブルーチン
    例文
    # (1)引数に渡す値
    my $num1 = 1;
    my $num2 = 2;

    # (2)サブルーチンの呼び出し
    my $total = total($num1, $num2);値は3になる

    # (3)サブルーチンの定義
    sub total {
    my ($num1, $num2) = @_;
    my $total = $num1 + $num2;
    return $total;
    }
    戻り値は$totalに代入される。

    ●●●解説●●●

    1)「基本プログラム」

    9.&decode;←ルーチンの呼び出し
    10.if ($in{'mode'} eq 'kaitou') { &kaitou; }←ルーチンkaitouの呼び出し
    11.elsif ($in{'mode'} eq 'admin') { &admin; }←ルーチンadminの呼び出し
    12.else { &situmon; }←ルーチンsitumonの呼び出し
    13.exit;
    14.

    9~14行に書かれたプログラムを「基本プログラム」と呼びます。この「基本プログラム」は、いろいろなサブルーチンを呼び出してプログラム全体を進行させ、13行目の exit; でプログラムは終了する。また、いろいろな処理をサブルーチン化しているため、非常に簡潔になっている。

    ★サブルーチンdecode

    #---- フォームデータ取得 ----
    16.sub decode {
    17. if ($ENV{'REQUEST_METHOD'} eq 'POST') {
    #read関数は環境変数 'CONTENT_LENGTH' にセットされた長さの文字列を、STDIN から 変数 $buf に読み込む。
    18. read(STDIN, $buf, $ENV{'CONTENT_LENGTH'});
    19. } else {
    20. $buf = $ENV{'QUERY_STRING'};
    21. }
    22. foreach (split(/&/, $buf)) {
    23. ($key, $val) = split(/=/);
    24. $in{$key} = $val;
    25. }
    26.}
    例題の「フォームデータの取得」 の部分をサブルーチンとして纏めたもの。最初に、このサブルーチンを実行し、他のサブルーチンからデータが送られて来た時、そのデータをハッシュ %in に格納する処理をしている。

    #---- 質問の表示 ----
    sub situmon {
     ・・・
    }
    #---- 回答の集計と表示 ----
    sub kaitou {
     ・・・
    }

    #---- 管理画面 ----
    sub admin {
     ・・・
    }

    始めはデータ送信されない為、ハッシュ%inは作られない。よって、パスされる。次の10~12行目では、$in{'mode'}=kaitouの場合は、サブルーチンkaitouを呼び出し、$in{'mode'}=adminの場合はサブルーチンadminを呼び出す。それ以外はサブルーチンsitumonを呼び出して、次のexit命令で終了する。

    ■アンケートの質問画面
    サブルーチンsitumonのスクリプト
    #---- 質問の表示 ----
    29.sub situmon {

    サブルーチンhtmlの呼び出し
    30. &html('質問', $ttl, '下から選んでご回答ください');

    31. print <<END;←ドキュメント
    32.<form action="$thisfile" method="post">
    33.<input type="hidden" name="mode" value="kaitou">←隠しキー
    34.<ul>
    35.END
    36. for ($i=0; $i<@ans; $i++) {
    37. print qq(<li><input type="radio" name="ans" value="$i">$ans[$i]\n);
    38. }
    39. print </ul>
    41.<input type="submit" value="回答する">\n</form>
    42.<form action="$thisfile" method="post">
    43.<input type="hidden" name="mode" value="admin">
    44.パスワード<input type="password" size="10" name="pw">←パスワード入力欄が作成される
    45.<input type="submit" value="管理画面">\n</form>
    46.</body>\n</html>
    47.END
    48.}
    →→→サブルーチンsitumonの30行で、サブルーチンhtmlを引数を付けて呼び出し、HTMLの前半部分を出力し、31行目以降で2つのフォームを含む HTML の後半を出力している。
    32行目の<form action="$thisfile" method="post">で、自分自身を呼び出すように設定している。
    36~37行目の for ループで質問に対する回答項目を出力しています。ループ条件を $i<@ans として $ans[$i] を出力するようにしていますが、これは、配列 @ans の内容、要素数を変更しても、ここのスクリプトを変更しないで済むようにしているためです。

    以上、実行すると
    「犬と猫のどちらが好き?」と言う質問画面を表示される。

    ★サブルーチンhtml
    #---- HTML 書き出し ----
    117.sub html {
    118.print <<END;←ヒアドキュメント
    119.Content-type: text/html\n
    120.<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
    121.<html>\n<head>
    122.<meta content="text/html;">
    123.<title>アンケートの$_[0]</title>
    124.</head>\n
      <body>
    125.<h1>$_[1]</h1>
    126.<p><b>$_[2]</b></p>
    127.END
    128.}
    →→→ このサブルーチンsitumonは、CGIヘッダーの2行と HTML の head 部 および body 部の途中までを出力している。また、渡される3つの引数を($_[0]→質問、$_[1]→犬と猫のどちらが好き?、$_[2]→下から選んでご回答ください)で参照して、それらの文字列を 123, 125, 126行目で使用している。

    ■ 回答画面
    このサブルーチンは、回答された数を計数してから、回答内容を表示する画面を出力します。また、このサブルーチン内で、「readData サブルーチン」、「html サブルーチン」、「back サブルーチン」 の3つのサブルーチンを呼び出してる。

    サブルーチンkaitouのスクリプト
    #---- 回答の計数と表示 ----
    51.sub kaitou {
    52. if (exists $in{'ans'}) {
    53. @n = &readData($data);
    54. $i = $in{'ans'};
    55. $n[$i]++;
    56. open(OUT, ">$datafile");#書き込みモード
    57. foreach (@n) { print OUT "$_\n"; }
    58. close(OUT);
    59. if (!$data) { chmod(0666, $datafile); }
    60. $msg = "あなたは 「$ans[$i]」 の$n[$i]人目です";
    61. } else {
    62. $msg = "どれかを選んでからお答えください";
    63. }
    64. <font color="red">&html('回答', 'ご回答の表\示', $msg); </font>
    65. &back;
    66.}
    →→→53行目の@n = &readData($data); で、データファイルを読み取り、回答数を@n で受け取っています。引数 $data には、データファイルが存在すれば真、存在しなければ偽がセットされます。

    ★サブルーチンreadData
    99.#---- 集計データ読み取り ----
    100.sub readData {
    101. my @num; #myはローカルでのみ有効
    102. $_[0] = -e $datafile;
    103. if ($_[0]) {
    104. open(IN, $datafile); #読み込みモード
    105. while (<IN>) {
    106. chomp;
    107. push(@num, $_); #配列にデータを代入
    108. }
    109. close(IN);
    110. } else {
    111. for (my $i=0; $i<@ans; $i++) { $num[$i] = 0; }
    112. }
    113. return @num;
    114.}
    →→→102行目でファイルテスト演算子 -e を使って $datafile が存在するかどうかを調べる。データファイルが存在すれば真を、存在しなければ偽をセットする。
    ・ファイルが存在した場合
    このファイルの内容を読み取り、これまでのデータを配列numに格納します
    ・データファイルが存在しない場合
    111行目の for ループで @ans と同じ要素数をもつ @num の各要素を0に設定して、これを戻り値とする。
    ★サブルーチンback
    130.#---- 戻るボタン ----
    131.sub back {
    132. print <<END;
    133.<form action="$thisfile">
    134.<input type="submit" value=" 戻る ">\n</form>
    135.</body>\n</html>
    136.END
    137.}

    ■ 管理画面
    管理画面は、前回の「集計結果の表示画面」 に類似の画面ですが、管理者だけがアンケートの集計を見ることができ、必要なときには集計をリセットすることができるようにした画面です。集計をリセットする機能は前回にはありませんでしたが、この例題で追加しました。「集計リセット」 というボタンを設け、これが押されたときにデータファイルを削除してしまいます。

    #---- 管理画面 ----
    69.sub admin {
    70. if (!$in{'pw'}) {
    71. $msg = 'パスワードが入力されていません'; $flag=0;
    72. } elsif ($in{'pw'} ne $pass) {
    73. $msg = 'パスワードが違います'; $flag=0;
    74. } elsif ($in{'del'}) {
    75. unlink $datafile; #ファイルの削除
    76. $msg = '集計ファイルを削除しました'; $flag=1;
    77. } else {
    78. $msg = '集計結果'; $flag=1;
    79. }
    80. &html('集計', '集計結果', $msg);

    81. if ($flag == 1) {
    82. @n = &readData($data);
    83. print "<ul>\n";
    84. $i = 0;
    85. foreach (@ans) { print "<li>$_ : $n[$i++]\n"; }
    86. print "</ul>\n";
    87. if ($data) { print <

    93.END
    94. }
    95. }
    96. &back;#サブルーチンbackの呼び出し
    97.}
    →→→70~79行目で、この admin サブルーチンがどのような状態で呼び出されたのかを調べています。パスワードが正しく入力されていなければ、何らかのメッセージを表示して 「戻る」 ボタンで最初の画面に戻ってもらいます。この状態を $flag=0; として82~94行目の処理がスキップされる。

    →→→「集計リセット」 ボタンが押された直後でなければ、78行目が行われた後に82~94行目の処理に進みます。82行目でデータが読み出され、83~86行目の処理でデータを表示します。若しデータファイルが存在すれば、ヒアドキュメントで88~92行目で 「集計リセット」 ボタンのあるフォームを出力します。

    このフォームには隠しコントロールを3つ設け、「集計リセット」ボタンが押されたときに enquete.cgi が mode=admin、pw=$pass、del=1 というデータを伴って起動されるようにしておきます。従って、「集計リセット」 ボタンが押された直後には、再びこの admin サブルーチンが呼び出されて75行目にある unlink $datafile; が実行されます。

    以上









    コメント

    シンプルなアンケートを作る

    2018年03月17日 | perl
    Perlでシンプルなアンケート作る

    【環境条件】
    Eclipse 4.4(ルナ)
    XAMPP 1.8.3 
    Perlは既にインストール済み
    CGIを使用する時は、必ずApachを起動する

    【アンケート】
    仕様
    1.質問「犬と猫のどちらが好き?」に対して、相手に回答してもらう。
    2.回答は「犬」、「猫」、「どちらも嫌い」 の何れかをチェックする形式。
    3.各項目の集結が出来るようにする。

    画面操作
    1.初期画面

    2.選択し、「回答」ボタンを押す

    3.回答画面
    「戻る」ボタンを押して、初期画面に戻る

    4.初期画面から、「集計の表示」ボタンを押す

    5.アンケート集計が表示される


    【アンケートスクリプト】
    1)アンケートの質問を表示する画面「enquete-1.html」

    ⅰ.最初のフォーム<form action="enquete-2.cgi" method="post">~</form>は、質問に対する回答データを「enquete-2.cgi」に対して送り出すフォーム

    ii.このフォームには3つのラジオボタンがあり、16行目の「回答する」と表示された submitボタンを押すと、「犬」を選んだときは ans=0 というデータがpostで送信される。

    iii.リスト<ul><li type="disc"><li> ~ </li><ul>は、先頭に「・」を付け、チェックボタンとデータを縦に並べる。

    ⅳ.<form action="enquete-3.cgi" >~</form>は「集計の表示」 ボタンと、その処理を委ねるスクリプトを指し示している。

    2)回答画面「enquete-2.cgi」

    ⅰ.「$datafile = 'enquete.dat';」はデータを入れるファイルの指定。
    「@ans =('犬', '猫', 'どちらも嫌い'); は質問項目を要素の初期化。

    ⅱ.$in{$key} = $valの解説
    #---- フォームデータ取得 ----
    if ($ENV{'REQUEST_METHOD'} eq 'POST') {
     ・・・・・・
    }else{
     ・・・・・・・
    }
    以上までは前回説明済み
    foreach (split(/&/, $buf)) {
    ・・・・
    $in{$key} = $val;→ハッシュの生成
    }
    例えば、「犬」 が選ばれ「 ans=0 」というデータが送られてきた場合、ハッシュ %in にキーを ans、その値 $in{'ans'} を0とする要素が設定される。

    ⅲ.exists 関数
    #---- 回答の計数 ----
    if (exists $in{'ans'}) {
      ・・・・
    }else{
    $msg = "どれかを選んでからお答えください";
    }
    exists 関数でハッシュ値$in{'ans'}の有無を調べる。→なぜなら、アンケートの質問画面でどの項目も選ばないと、ans データ無しでこのCGI が起動しないからである。そして、選択されて無い時は、"どれかを選んでからお答えください"の表示を出す。

    ⅳ.ファイルテスト演算子 -e
    if (-e $datafile) {

    ・・・・・・・・
    }

    ファイルテスト演算子 -e を使って $datafile が存在するかどうかを調べています。$datafile が存在すれば、このファイルの内容を読み取む。

    ⅴ.open関数
    if (-e $datafile) {
    pen(IN, $datafile);
    ・・・・
    }
    標準ファイルハンドル以外を使う場合には、open関数を使用します。そして、open関数を使って、読み込みモードでファイルを開かせる。
    構文
    →open(ファイルハンドル名, "ファイル名")


    ⅵ.ファイル読み込みwhile文
    ファイルからテキストデータを読み込む時、while (<IN>) を使うのが便利である。また、while (<IN>)は while(defined($_ = <TXT>)) と同じで、$_ にファイルハンドル<IN> から1行読み込んだ値がセットされる。行入力演算子<>はファイルの終端に来ると、未定義を返す。

    while (<IN>) {
    chomp;→改行文字だけを削除する。
    push(@n, $_);→$_に入ってるファイルデータを配列(@n)に入れる
    }
      #クローズ
      close(IN);
    }else{ @n = (0, 0, 0); $new=1; }→ファイルが無い時、配列@nに0を入れる

    ⅶ.データ
    $i = $in{'ans'};#変数←値
    $n[$i]++;
    回答された ans の値 $in{'ans'} を $i に代入し、データの配列 @n の $i 番目の要素 $n[$i] をインクレメント(1を加算)する

    ⅷ.ファイルへの書き出し
    open(OUT, ">$datafile");
    →「enquete.dat」ファイルを出力モードでオープンする。
    foreach (@n){print OUT "$_\n";}
    →更新された配列 @n のデータが変数$_に入り、ファイルハンドルOUTが示す$datafileに書き出す。
    close(OUT);→ファイルを閉じる

    ■オープンモード


    ⅸ.ファイルのパーミッション変更
    if ($new) { chmod(0666, $datafile); }
    →この書き出しで新規にファイルが作られたときは、パーミッションですべてのユーザーにファイルの書込権限を与えている。

    $msg = "あなたは 「$ans[$i]」 の$n[$i]人目です";
    →例えば、 "あなたは 「犬」 の5番目です" というようなメッセージを設定している。
    } else {
    $msg = "どれかを選んでからお答えください";
    }

    Ⅹ.結果表示
    #---- 回答の表示 ----
    print <

    ご回答の表\示


    $msg


    <form action="enquete-1.html"> ←フォームを使って入力画面に戻る
    <input type="submit" value=" 戻る ">
    </form>\n</body>\n</html>
    END


    3)アンケート集計画面「enquete-3.cgi 」

    #!/usr/local/bin/perl

    $datafile = 'enquete.dat';←データファイル名の設定
    @ans = ('犬', '猫', 'どちらも嫌い');←質問項目を要素とする配列の設定

    #---- データ読み取り ----
    if (-e $datafile) {
    open(IN, $datafile);
    while (<IN>) {
    chomp;
    push(@n, $_);
    }
    close(IN);
    } else { @n = (0, 0, 0); }
    ここまでは、回答画面の処理と同じで、$datafileを読み取る処理をしている。
    #---- 集計の表示 ----
    print <

    集計結果



      END
      $i = 0;
      foreach (@ans) { print "
    • $_ : $n[$i++]\n"; } ←リストの項目を出力しています。
      print <

    <form action="enquete-1.html">
    <input type="submit" value=" 戻る ">
    </form>\n</body>\n</html>
    END

    以上




    コメント