掲示板を作ろう (1)

前へ << CGI プログラムのはじめの一歩 掲示板を作ろう (2) >> 次へ

スクリプトの文字コード

CGI プログラムの習作として、掲示板を作りましょう。

bbs-first.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";
…
と全く同じですが、 という利点があります。比較的長い文章を出力したい場合は できるだけヒアドキュメントを使いましょう。

一つ工夫をしたのは、先頭部分で自分自身のファイル名を 取得し、<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 に変えたものが以下のスクリプトです。

bbs-euc.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
スクリプトの見た目は全く変わりませんが :-) 、 実行結果を見ると「ハンドルネ―ム」が正しく出力されています。

これ以降のスクリプトは、全て 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 を参照すればよいわけです。

bbs-query-string.cgi (実行結果)

    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 '') なら、書き込み (発言) ではなく、読み込み (閲覧) である」
ということです。

URL デコード

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 エンコードの正確な定義は、

  1. [-a-zA-Z0-9_\* ] 以外の文字列を %XX という形式に変換
  2. ` ' (半角スペース) を `+' に変換
です。例えば「aあ +~*」は、 となります。

CGI プログラム側では、これとは逆の変換である「URL デコード」を行わなければいけません。 URL デコードを perl で記述すると、

$str =~ tr/+/ /;
$str =~ s/%([0-9a-fA-F][0-9a-fA-F])/pack("C",hex($1))/eg;
だけで済みます。
ちなみに URL エンコードは
$str =~ s/([^-a-zA-Z0-9_\* ])/sprintf("%%%02lX",unpack("C",$1))/eg;
$str =~ tr/ /+/;
です。
URL デコードに対応した CGI スクリプトは以下の通りです。

bbs-url-decode.cgi (実行結果)

    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 で記述しているので、この入力も EUC-JP にしましょう。

便利なことに、文字コードを変換してくれる jcode.pl という perl 用のライブラリがあります。 掲示板 CGI プログラムと同じディレクトリに jcode.pl を置き、

require 'jcode.pl';
として関数群を読み込んでから、
&jcode::convert(\$str,'euc');
とすることで、$str を EUC-JP に変換できます。 元々 $str に入っている文字コードが SJIS であろうが JIS であろうが、 勝手に判別して適切に変換してくれます。

jcode.pl を利用したものが以下のスクリプトです。

bbs-jcode.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:     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 です。 ブラウザがどの改行コードを送ってくるかはわからないので、 これら全てをうまく変換するには
$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;
となります。

bbs-newline.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:     $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=".."> を入力すると、 そのまま書き込まれてしまいます。 ユーザにタグを使わせるならいいのですが、 不正なタグや閉じていないタグを書き込まれるおそれがあります。

タグを禁止するには、< > を無効化すればよいのです。 &lt; &gt; と書けば < > そのものとして 表示されます。同様に &amp; と書けば & と 表示されます。これらを変換するには

$str =~ s/&/&amp;/g;
$str =~ s/</&lt;/g;
$str =~ s/>/&gt;/g;
とします。

この処理を加えた CGI プログラムは以下の通りです。

bbs-disable-tag.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/&/&amp;/g;
   18:     $value =~ s/</&lt;/g;
   19:     $value =~ s/>/&gt;/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/&/&amp;/g;
   18:     $value =~ s/</&lt;/g;
   19:     $value =~ s/>/&gt;/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/&/&amp;/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) >> 次へ

$Id: bbs-perl-1.html,v 1.6 2005/07/02 03:13:38 68user Exp $