前へ << CGI プログラムのはじめの一歩 | 掲示板を作ろう (2) >> 次へ |
CGI プログラムの習作として、掲示板を作りましょう。1: #!/usr/local/bin/perl 2: 3: # 自分自身のファイル名を取得 4: $script_name = $ENV{SCRIPT_NAME}; 5: $script_name =~ s|.*/([^/]+)$|$1|; 6: 7: print "Content-type: text/html\n\n"; 8: 9: print <<END; 10: <HTML> 11: <HEAD><TITLE>掲示板</TITLE></HEAD> 12: <BODY BGCOLOR="#DDDDDD"> 13: <H1>掲示板</H1><HR> 14: <FORM METHOD=GET ACTION="$script_name"> 15: <TABLE BORDER=1> 16: <TR><TD>ハンドルネ―ム: 17: <TD><INPUT TYPE=text NAME=FROM SIZE=54> 18: <TR><TD COLSPAN=2><TEXTAREA ROWS=6 COLS=60 NAME=MESSAGE></TEXTAREA> 19: </TABLE> 20: <P><INPUT TYPE=submit VALUE="送信"> 21: </FORM><HR></BODY></HTML> 22: ENDまずはフォームを出力します。 print 文でヘッダを出力し、さらに HTML を出力しているだけです。print <<END; ...... ENDというのは「ヒアドキュメント」という形式です。これはprint "<HTML>\n"; print "<HEAD><TITLE>掲示板</TITLE></HEAD>\n"; print "<BODY BGCOLOR=\"#DDDDDD\">\n"; print "<H1>掲示板</H1><HR>\n"; …と全く同じですが、という利点があります。比較的長い文章を出力したい場合は できるだけヒアドキュメントを使いましょう。
- いちいち "" で括る必要がない
- いちいち \n (改行コード) を記述する必要がない
- ダブルクォート " 自体を出力する場合、いちいち \" とエスケープする必要がない
一つ工夫をしたのは、先頭部分で自分自身のファイル名を 取得し、<FORM ACTION="..." の部分に変数を使っていることです。 $ENV{SCRIPT_NAME} というのは環境変数 SCRIPT_NAME のことです。 WWW サーバが CGI スクリプトを実行する前に、SCRIPT_NAME に /~68user/webcgi/sample/bbs-start.cgi という値をセットします。
こうすることで、もしファイル名を bbs-start.cgi から変更したとしても、 いちいち ACTION="..." の部分をいじる必要がなくなります。
何も問題がないように見えるこのスクリプトですが、 実行結果を見てみるとおかしなことに気がつくでしょうか。 スクリプトには確かに「ハンドルネ―ム」と書いてあるのに、 出力される HTML は「ハンドルネ<」と化けてしまっています。
これは、CGI スクリプトを Shift-JIS (SJIS) で書いたからです。
回避方法はいくつかあります。
print "ハンドルネ―ム"; # これは化ける print "ハンドルネ―\ム"; # \ の後にもう一つ \ を入れると \ そのものとして扱われる print 'ハンドルネ―ム'; # '' で括れば大丈夫 print <<'END'; # これも OK ハンドルネ―ム END open(IN,"file"); # 事前に「ハンドルネ―ム」と書いたファイルを用意しておき、 print <IN>; # そこから読み込めば OKしかし、いずれも不便です。結論から言うと、perl スクリプトを SJIS で書くのは愚かなことです。 EUC-JP で書きましょう。もしエディタ (メモ帳など) が EUC-JP に対応していないなら、 EUC-JP に対応しているエディタを入手しましょう。
ISO-2022-JP (いわゆる JIS コード) は問題外です。 ISO-2022-JP は通信用のコードであって、 プログラムに埋め込むのには不向きです。文字コードを EUC-JP に変えたものが以下のスクリプトです。1: #!/usr/local/bin/perl 2: 3: # 自分自身のファイル名を取得 4: $script_name = $ENV{SCRIPT_NAME}; 5: $script_name =~ s|.*/([^/]+)$|$1|; 6: 7: print "Content-type: text/html\n\n"; 8: 9: print <<END; 10: <HTML> 11: <HEAD><TITLE>掲示板</TITLE></HEAD> 12: <BODY BGCOLOR="#DDDDDD"> 13: <H1>掲示板</H1><HR> 14: <FORM METHOD=GET ACTION="$script_name"> 15: <TABLE BORDER=1> 16: <TR><TD>ハンドルネ―ム: 17: <TD><INPUT TYPE=text NAME=FROM SIZE=54> 18: <TR><TD COLSPAN=2><TEXTAREA ROWS=6 COLS=60 NAME=MESSAGE></TEXTAREA> 19: </TABLE> 20: <P><INPUT TYPE=submit VALUE="送信"> 21: </FORM><HR></BODY></HTML> 22: ENDスクリプトの見た目は全く変わりませんが :-) 、 実行結果を見ると「ハンドルネ―ム」が正しく出力されています。これ以降のスクリプトは、全て EUC-JP コードを使います。
さて、適当にフォームに書き込んで「送信」ボタンを押してみて下さい。 同じページが表示されるだけで、何も起こりません。 しかし、ブラウザの URL の項はhttp://X68000.q-e-d.net/~68user/webcgi/sample/bbs-euc.cgi?FROM=abc&MESSAGE=defなどと、最後に ?FROM=...&MESSAGE=... という文字列が表示されているはずです。 CGI プログラムはこれらの文字列を解析し、それに応じた動作をすればよいのです。この文字列は WWW サーバによって、環境変数 QUERY_STRING に格納されています。 掲示板プログラムは、読み込み (閲覧) と書き込み (発言) の場合で動作を変えないといけませんが、 それを決めるときも環境変数 QUERY_STRING を参照すればよいわけです。
1: #!/usr/local/bin/perl 2: 3: # 自分自身のファイル名を取得 4: $script_name = $ENV{SCRIPT_NAME}; 5: $script_name =~ s|.*/([^/]+)$|$1|; 6: 7: # 引数解析 8: foreach ( split('&',$ENV{QUERY_STRING}) ){ 9: ($key,$value) = split('=',$_); 10: if ( $key eq 'FROM' ){ 11: $from = $value; 12: } elsif ( $key eq 'MESSAGE' ){ 13: $message = $value; 14: } 15: } 16: 17: print "Content-type: text/html\n\n"; 18: 19: print <<END; 20: <HTML> 21: <HEAD><TITLE>掲示板</TITLE></HEAD> 22: <BODY BGCOLOR="#DDDDDD"> 23: <H1>掲示板</H1><HR> 24: END 25: 26: # 引数が設定されていたら、それを表示 27: if ( $from ne '' ){ print "FROM=$from<P>\n" } 28: if ( $message ne '' ){ print "MESSAGE=$message<P>\n" } 29: 30: print <<END; 31: <FORM METHOD=GET ACTION="$script_name"> 32: <TABLE BORDER=1> 33: <TR><TD>ハンドルネ―ム: 34: <TD><INPUT TYPE=text NAME=FROM SIZE=54> 35: <TR><TD COLSPAN=2><TEXTAREA ROWS=6 COLS=60 NAME=MESSAGE></TEXTAREA> 36: </TABLE> 37: <P><INPUT TYPE=submit VALUE="送信"> 38: </FORM><HR></BODY></HTML> 39: ENDフォームに書き込むと、環境変数 QUERY_STRING に値が入ります。それを8: foreach ( split('&',$ENV{QUERY_STRING}) ){ 9: ($key,$value) = split('=',$_); 10: if ( $key eq 'FROM' ){ 11: $from = $value; 12: } elsif ( $key eq 'MESSAGE' ){ 13: $message = $value; 14: } 15: }の部分で解析しています。例えば QUERY_STRING が FROM=abc&MESSAGE=def ならば、 最初の split で FROM=abc と MESSAGE=def に分解されます。 それを 2番目の split で FROM と abc、MESSAGE と def に分けているのです。 その結果 $from='abc'、$message='def' と変数に代入されます。
27: if ( $from ne '' ){ print "FROM=$from<P>\n" } 28: if ( $message ne '' ){ print "MESSAGE=$message<P>\n" }ここで、$from と $message に値が入っていたらそれを表示しています。 大事なのは、「$from と $message が空 (eq '') なら、書き込み (発言) ではなく、読み込み (閲覧) である」ということです。
abc や def などと英数字を入力するだけなら問題ないのですが、 フォームに日本語や記号を入力してみてください。 例えばハンドルネームに「あいうえお」、メッセージに「~!@#$」を 入力すると、FROM=%A4%A2%A4%A4%A4%A6%A4%A8%A4%AA MESSAGE=%7E%21%40%23%24と出力されてしまいます。これは、フォームに入力された文字列をブラウザが変換してから送信したからです。 このブラウザが行う変換を「URL エンコード」といいます。 これは RFC という規格で決まっており、ブラウザは a〜z、A-Z、0-9、-、_、* は そのまま送信してもよいのですが、それ以外の文字は (ブラウザが) URL エンコード しなくてはいけません。
URL エンコードは `%' の後に文字コードを 16進数表記したものです。 `あ'は EUC-JP では 0xA4 0xA2 なので %A4%A2 となり、 `~' は 0x7E なので %7E となったわけです。 URL エンコードの正確な定義は、
です。例えば「aあ +~*」は、
- [-a-zA-Z0-9_\* ] 以外の文字列を %XX という形式に変換
- ` ' (半角スペース) を `+' に変換
となります。
- 1の変換で「a%A4%A2 %2B%7E*」
- 続いて 2の変換で「a%A4%A2+%2B%7E*」
CGI プログラム側では、これとは逆の変換である「URL デコード」を行わなければいけません。 URL デコードを perl で記述すると、
$str =~ tr/+/ /; $str =~ s/%([0-9a-fA-F][0-9a-fA-F])/pack("C",hex($1))/eg;だけで済みます。ちなみに URL エンコードはURL デコードに対応した CGI スクリプトは以下の通りです。$str =~ s/([^-a-zA-Z0-9_\* ])/sprintf("%%%02lX",unpack("C",$1))/eg; $str =~ tr/ /+/;です。1: #!/usr/local/bin/perl 2: 3: # 自分自身のファイル名を取得 4: $script_name = $ENV{SCRIPT_NAME}; 5: $script_name =~ s|.*/([^/]+)$|$1|; 6: 7: # 引数解析 8: foreach ( split('&',$ENV{QUERY_STRING}) ){ 9: ($key,$value) = split('=',$_); 10: $value =~ tr/+/ /; 11: $value =~ s/%([0-9a-fA-F][0-9a-fA-F])/pack("C",hex($1))/eg; 12: 13: if ( $key eq 'FROM' ){ 14: $from = $value; 15: } elsif ( $key eq 'MESSAGE' ){ 16: $message = $value; 17: } 18: } 19: 20: print "Content-type: text/html\n\n"; 21: 22: print <<END; 23: <HTML> 24: <HEAD><TITLE>掲示板</TITLE></HEAD> 25: <BODY BGCOLOR="#DDDDDD"> 26: <H1>掲示板</H1><HR> 27: END 28: 29: # 引数が設定されていたら、それを表示 30: if ( $from ne '' ){ print "FROM=$from<P>\n" } 31: if ( $message ne '' ){ print "MESSAGE=$message<P>\n" } 32: 33: print <<END; 34: <FORM METHOD=GET ACTION="$script_name"> 35: <TABLE BORDER=1> 36: <TR><TD>ハンドルネ―ム: 37: <TD><INPUT TYPE=text NAME=FROM SIZE=54> 38: <TR><TD COLSPAN=2><TEXTAREA ROWS=6 COLS=60 NAME=MESSAGE></TEXTAREA> 39: </TABLE> 40: <P><INPUT TYPE=submit VALUE="送信"> 41: </FORM><HR></BODY></HTML> 42: ENDと言っても、10: $value =~ tr/+/ /; 11: $value =~ s/%([0-9a-fA-F][0-9a-fA-F])/pack("C",hex($1))/eg;この 2行を追加しただけです。これにより、フォームに日本語や記号を書き込んでも正しく表示されるようになりました。
先ほど、「`あ'は EUC-JP では 0xA4 0xA2 なので」と書きましたが、 フォームに入力された文字を、必ずしもブラウザが EUC-JP で扱うとは限りません。SJIS か JIS で送信してくる可能性もあります。 例えば「あ」はですので、全ての場合に対応しなければいけません。
- EUC-JP では 0xA4 0xA2
- SJIS では 0x82 0xA0
- JIS では 0x1B 0x24 0x42 0x22 0x24 0x1B 0x28 0x42
全ての場合に対応というより、特定の文字コードに変換するのがよいでしょう。 スクリプトを EUC-JP で記述しているので、この入力も EUC-JP にしましょう。
便利なことに、文字コードを変換してくれる jcode.pl という perl 用のライブラリがあります。 掲示板 CGI プログラムと同じディレクトリに jcode.pl を置き、
require 'jcode.pl';として関数群を読み込んでから、&jcode::convert(\$str,'euc');とすることで、$str を EUC-JP に変換できます。 元々 $str に入っている文字コードが SJIS であろうが JIS であろうが、 勝手に判別して適切に変換してくれます。jcode.pl を利用したものが以下のスクリプトです。
1: #!/usr/local/bin/perl 2: 3: # 自分自身のファイル名を取得 4: $script_name = $ENV{SCRIPT_NAME}; 5: $script_name =~ s|.*/([^/]+)$|$1|; 6: 7: # jcode.pl をロード 8: require 'jcode.pl'; 9: 10: # 引数解析 11: foreach ( split('&',$ENV{QUERY_STRING}) ){ 12: ($key,$value) = split('=',$_); 13: $value =~ tr/+/ /; 14: $value =~ s/%([0-9a-fA-F][0-9a-fA-F])/pack("C",hex($1))/eg; 15: &jcode::convert(\$value,'euc'); 16: 17: if ( $key eq 'FROM' ){ 18: $from = $value; 19: } elsif ( $key eq 'MESSAGE' ){ 20: $message = $value; 21: } 22: } 23: 24: print "Content-type: text/html\n\n"; 25: 26: print <<END; 27: <HTML> 28: <HEAD><TITLE>掲示板</TITLE></HEAD> 29: <BODY BGCOLOR="#DDDDDD"> 30: <H1>掲示板</H1><HR> 31: <FORM METHOD=GET ACTION="$script_name"> 32: <TABLE BORDER=1> 33: <TR><TD>ハンドルネ―ム: 34: <TD><INPUT TYPE=text NAME=FROM SIZE=54> 35: <TR><TD COLSPAN=2><TEXTAREA ROWS=6 COLS=60 NAME=MESSAGE></TEXTAREA> 36: </TABLE> 37: <P><INPUT TYPE=submit VALUE="送信"> 38: </FORM><HR> 39: END 40: 41: # 引数が設定されていたら、それを表示 42: if ( $from ne '' ){ print "発言者: $from<P>\n" } 43: if ( $message ne '' ){ print "$message<P>\n" } 44: 45: print <<END; 46: <HR></BODY></HTML> 47: END変更点は8: require 'jcode.pl';と11: foreach ( split('&',$ENV{QUERY_STRING}) ){ 12: ($key,$value) = split('=',$_); 13: $value =~ tr/+/ /; 14: $value =~ s/%([0-9a-fA-F][0-9a-fA-F])/pack("C",hex($1))/eg; 15: &jcode::convert(\$value,'euc');の &jcode::convert の追加のみです。 URL デコードした後に &jcode::convert していることに注意して下さい。 逆にするとうまく動きません。
TEXTAREA 内で改行して送信しても、CGI の出力には改行が反映されません。 ブラウザの「表示->ページのソース」で CGI が出力したソースを見ると、 正しく改行されています。 しかし HTML では改行コードは意味を持たず、<BR> というタグを書かないと 改行されないのです。そこで、改行コードを <BR> に変換する必要があります。 ただし、OS によって改行コードは異なり、
となります。\n とは LF (LineFeed) のことで16進数で 0x0A です。 \r は CR (CarriageReturn) のことで 16進数で 0x0D です。 ブラウザがどの改行コードを送ってくるかはわからないので、 これら全てをうまく変換するには
- UNIX では改行コードは \n
- DOS/Windows では改行コードは \r\n
- Macintosh では改行コードは \r
$str =~ s/\r\n|\r|\n/<BR>/g;とします。ただし例えば \r\n は、ブラウザが %0D%0A に変換してから送信してくるので、 URL デコードしてから改行コードを変換しなくてはいけません。
Macintosh の Netscape Navigator 3.01 (だったかなぁ?) では、 改行コードを \r\n\n として送ってくるバグがありますので、 もしそれにも対応するなら$str =~ s/\r\n\n|\r\n|\r|\n/<BR>/g;となります。1: #!/usr/local/bin/perl 2: 3: # 自分自身のファイル名を取得 4: $script_name = $ENV{SCRIPT_NAME}; 5: $script_name =~ s|.*/([^/]+)$|$1|; 6: 7: # jcode.pl をロード 8: require 'jcode.pl'; 9: 10: # 引数解析 11: foreach ( split('&',$ENV{QUERY_STRING}) ){ 12: ($key,$value) = split('=',$_); 13: $value =~ tr/+/ /; 14: $value =~ s/%([0-9a-fA-F][0-9a-fA-F])/pack("C",hex($1))/eg; 15: &jcode::convert(\$value,'euc'); 16: $value =~ s/\r\n|\r|\n/<BR>/g; 17: 18: if ( $key eq 'FROM' ){ 19: $from = $value; 20: } elsif ( $key eq 'MESSAGE' ){ 21: $message = $value; 22: } 23: } 24: 25: print "Content-type: text/html\n\n"; 26: 27: print <<END; 28: <HTML> 29: <HEAD><TITLE>掲示板</TITLE></HEAD> 30: <BODY BGCOLOR="#DDDDDD"> 31: <H1>掲示板</H1><HR> 32: <FORM METHOD=GET ACTION="$script_name"> 33: <TABLE BORDER=1> 34: <TR><TD>ハンドルネ―ム: 35: <TD><INPUT TYPE=text NAME=FROM SIZE=54> 36: <TR><TD COLSPAN=2><TEXTAREA ROWS=6 COLS=60 NAME=MESSAGE></TEXTAREA> 37: </TABLE> 38: <P><INPUT TYPE=submit VALUE="送信"> 39: </FORM><HR> 40: END 41: 42: # 引数が設定されていたら、それを表示 43: if ( $from ne '' ){ print "発言者: $from<P>\n" } 44: if ( $message ne '' ){ print "$message<P>\n" } 45: 46: print <<END; 47: <HR></BODY></HTML> 48: END
フォーム内で <A HREF=".."> を入力すると、 そのまま書き込まれてしまいます。 ユーザにタグを使わせるならいいのですが、 不正なタグや閉じていないタグを書き込まれるおそれがあります。タグを禁止するには、< > を無効化すればよいのです。 < > と書けば < > そのものとして 表示されます。同様に & と書けば & と 表示されます。これらを変換するには
$str =~ s/&/&/g; $str =~ s/</</g; $str =~ s/>/>/g;とします。この処理を加えた CGI プログラムは以下の通りです。
1: #!/usr/local/bin/perl 2: 3: # 自分自身のファイル名を取得 4: $script_name = $ENV{SCRIPT_NAME}; 5: $script_name =~ s|.*/([^/]+)$|$1|; 6: 7: # jcode.pl をロード 8: require 'jcode.pl'; 9: 10: # 引数解析 11: foreach ( split('&',$ENV{QUERY_STRING}) ){ 12: ($key,$value) = split('=',$_); 13: $value =~ tr/+/ /; 14: $value =~ s/%([0-9a-fA-F][0-9a-fA-F])/pack("C",hex($1))/eg; 15: &jcode::convert(\$value,'euc'); 16: 17: $value =~ s/&/&/g; 18: $value =~ s/</</g; 19: $value =~ s/>/>/g; 20: 21: $value =~ s/\r\n|\r|\n/<BR>/g; 22: 23: if ( $key eq 'FROM' ){ 24: $from = $value; 25: } elsif ( $key eq 'MESSAGE' ){ 26: $message = $value; 27: } 28: } 29: 30: print "Content-type: text/html\n\n"; 31: 32: print <<END; 33: <HTML> 34: <HEAD><TITLE>掲示板</TITLE></HEAD> 35: <BODY BGCOLOR="#DDDDDD"> 36: <H1>掲示板</H1><HR> 37: <FORM METHOD=GET ACTION="$script_name"> 38: <TABLE BORDER=1> 39: <TR><TD>ハンドルネ―ム: 40: <TD><INPUT TYPE=text NAME=FROM SIZE=54> 41: <TR><TD COLSPAN=2><TEXTAREA ROWS=6 COLS=60 NAME=MESSAGE></TEXTAREA> 42: </TABLE> 43: <P><INPUT TYPE=submit VALUE="送信"> 44: </FORM><HR> 45: END 46: 47: # 引数が設定されていたら、それを表示 48: if ( $from ne '' ){ print "発言者: $from<P>\n" } 49: if ( $message ne '' ){ print "$message<P>\n" } 50: 51: print <<END; 52: <HR></BODY></HTML> 53: END追加したのは17: $value =~ s/&/&/g; 18: $value =~ s/</</g; 19: $value =~ s/>/>/g;だけですが、追加した場所に注意して下さい。 URL デコードの前や、\r\n の変換の後に追加するとうまく 変換できないことがわかりますか?なお、この改行コード変換は、必ず文字コードを EUC-JP にした後で行わなくてはいけません。 もしブラウザが JIS コード (ISO-2022-JP) を送ってきた場合、JIS コードの 日本語部分の中に & が含まれている可能性があるからです。 例えば JIS コードで「あいう」は
文字コード (16進数) 1B 24 42 24 22 24 24 24 26 1B 28 42 意味 JIS X 0208 へ切り替え あ い う ASCII へ切り替え ASCII コードで表現すると ESC $ B $ " $ $ $ & ESC ( B となります。「う」の2バイト目 (赤い部分) は & と同じコードですので、 単純に
s/&/&/g;と変換すると、
文字コード (16進数) 1B 24 42 24 22 24 24 24 26 61 6D 70 3B 1B 28 42 意味 JIS X 0208 へ切り替え あ い う 瘢 雹 ASCII へ切り替え ASCII コードで表現すると ESC $ B $ " $ $ $ & a m p ; ESC ( B となり、「あいう」が「あいう瘢雹」に化けてしまいます。
前へ << CGI プログラムのはじめの一歩 | 掲示板を作ろう (2) >> 次へ |