ぱるも日記

ぱるも日記は id:palmo がプログラミング言語 Perl を1から勉強していく日記です。

2006-07-25

CGI モジュール(1) パラメータを受け取る

CGI プログラムの基本的な作り方がわかってきたので、そろそろ CGI モジュールを勉強したいと思います。CGI モジュールは Perl5 標準の組み込みモジュールで、CGI プログラムを作るのに便利な機能が利用できるようになるモジュールです。(Japanized Perl Resources Project にて日本語に翻訳された POD も公開されています)

CGI モジュールの利用方法は、従来の「関数型」(関数エクスポートして、モジュールの外部で関数を利用する)と、「オブジェクト型」(new クラスメソッドを呼び出して CGI パッケージのインスタンスを生成してメソッドを利用する)という2つの方法があります。前者の関数型は手軽に利用できますが、多くの関数エクスポートされる事になりますので、識別子の衝突など名前空間の汚染が心配です。後者のオブジェクト型は多少手続きが必要ですが、名前空間の汚染はありません。

多くのモジュールは、このように「関数型」と「オブジェクト型」という2つのインターフェースを提供していますが、ぱるも日記では主にオブジェクト型を利用していきたいと思います。ただし、Perl によるオブジェクト指向プログラミングは「関数型に比べて遅い」という事を留意しておかなくてはいけませんね。(^_^)

CGI モジュールオブジェクトとして利用するには、まず use してから、コンストラクタ new を呼び出します。慣例として CGI オブジェクトを入れる変数名には $q が使われるそうです。(query からきているのかな?)

use CGI;
my $q = CGI->new();

では、CGI モジュールの機能の一部を利用してみたいと思います。

パラメータの受け取り」は、環境変数 QUERY_STRING を介する方法と、標準入力を介する方法という2通りの方法を勉強しました。

CGI モジュールを利用すると、このパラメータの受け取りをとても簡単に行なう事ができます。こうなると、上のエントリで勉強した方法は「車輪の再発明」と呼ばれてしまいそうですが、CGI の仕組みを理解する為でしたのでお許しください。(^_^;)

与えられたパラメータの値を取得するには、param メソッドを利用します。このメソッドは "GET" の QUERY_STRING 形式にも "POST" の標準入力形式にも対応しているので、これらのリクエストメソッドの違いを意識する事なく利用できます。

例えば、以下のような URL で GET リクエストされたとします。

/action.cgi?apple=red&banana=yellow&peach=pink

「名前=値」のリストが「&」で区切られてるのでしたね。(^_^)

この action.cgi の中で param メソッドを使えば、以下のように名前に対応する値を取得できます。

print $q->param('apple'), "\n";   # red
print $q->param('banana'), "\n";  # yellow
print $q->param('peach'), "\n";   # pink

また、引数を渡さずに param メソッドを呼び出すと、パラメータとして渡された名前のリストを取得できます。下のコードでは全てのパラメータを列挙しています。

for my $name ($q->param) {
  print "$name is ", $q->param($name), ",\n";
}

上のコードでは、全てのパラメータを列挙します。簡単ですね。(^_^)


POST でパラメータを受け取る」で作った echo.cgi を、CGI モジュールを使って作り変えると、以下のようになりました。テンプレートは流用しています。

echo2.cgi 実行結果 (ソースコードテンプレート

自前でパラメータ解析を実装していた echo.cgi と比べると、大分コードが短くなっています。

また、もう一つのメリットとして、param メソッドを通じて取得した値は自動的にアンエスケープされるので、日本語や改行を入力しても、ちゃんと表示されます。便利ですね。(^_^)

2006-07-24

範囲演算子

スライスを勉強した時に「範囲構文」として勉強した「a..b」という書き方ですが、ラクダ本を読んでいたところ、これは構文というよりは「..」という「範囲演算子」によるもの、という事がわかりました。

今まで「整数整数の間の範囲の整数リストを作り出す」という程度の認識で使っていたのですが、やはりちゃんと勉強しないとダメですね。知らなかった使い方が見つかりました。(^_^;)

というわけで、範囲演算子をちゃんと勉強したいと思います。


ラクダ本プログラミングPerl〈VOLUME1〉)によると、範囲演算子(range operator)は「コンテキスト」によってその意味が変わるそうです。

「コンテキスト」は、ぱるも日記でも何度か登場して勉強していますが、「文脈」の事ですね。スカラー変数への代入文の右辺や if 文の条件文など、「スカラー値」が必要と判断される場所は「スカラーコンテキスト」と呼ばれ、配列変数への代入文の右辺や foreach 文の対象など、「リスト値」(複数のスカラー値)が必要と判断される場所は「リストコンテキスト」になるのでした。


範囲演算子を「リストコンテキスト」で使った場合は、今までの使い方である「整数整数の間の整数リストを生成」という意味になります。

my @onetoten = (1..10);   # 配列変数への代入なのでリストコンテキスト
print "@onetoten\n";
1 2 3 4 5 6 7 8 9 10

また、Perl には「マジックインクリメント」という機能があり、/^[a-zA-Z]*[0-9]*$/ にマッチする文字列が入ったスカラー変数をインクリメント(++)する事ができます。数字の10文字にアルファベット26文字を加えて、「36進数」であるかのように振舞うのです。ケタ上がりにも対応しています。

my @nums = qw(A ab AZ Z9 01 perl);
print "$_ -> ", ++$_, "\n" for (@nums);

@nums に入れた文字列をそれぞれインクリメントして表示します。実行結果は以下の通りです。

A -> B
ab -> ac
AZ -> BA
Z9 -> AA0
01 -> 02
perl -> perm

範囲演算子は内部でマジックインクリメントを利用しているので、範囲演算子の項には文字列を渡す事もできます。

my @atoz = ('a'..'z');
print "@atoz\n";
a b c d e f g h i j k l m n o p q r s t u v w x y z

便利ですね。(^_^)


範囲演算子スカラーコンテキストで評価した場合の動作は全く異なります。

スカラーコンテキストでの範囲演算子は、左右に真偽値をとり、真偽値を返す、論理演算子のように振舞います。しかし、ただの演算子とは違い、それぞれの範囲演算子が個別の「内部状態」(ON/OFF)を持っている、というのがポイントです。どの範囲演算子の内部状態も最初は「OFF」となっています。

例えば「A..B」がスカラーコンテキストに書かれていて、繰り返し構文などで何度も評価されるとします。

この「A..B」を評価すると、内部状態が「OFF」の時は A が評価されます。A が偽の場合、範囲演算子は何もせずに偽を返します。ですが、A が真になると範囲演算子の内部状態は「ON」となり、範囲演算子は真を返します。「A..B」が評価された時に、内部状態が「ON」の時は B が評価されます。B が真の場合、範囲演算子は何もせずに真を返しますが、B が偽になると内部状態が「OFF」になり、範囲演算子は偽を返します。

つまり、スカラーコンテキストでの範囲演算子は、「ON 条件と OFF 条件をとるスイッチ」のように振舞うのです。最初は ON 条件だけを見張っていて、一度 ON 条件が真になれば次からは OFF 条件を見張りだす、というように動くわけですね。見張られていない方の条件は「評価されない」事に注意してください。

また、ON 条件が真になった時 OFF 条件も評価されますが、ドットを3つにする(...)と ON 条件が真になっても次回の評価まで OFF 条件の評価は始まりません。


このスカラーコンテキストでの範囲演算子は、使い道が思い浮かばないかもしれませんが、色々な場面で利用できます。スイッチの代わりになる、というのが便利なのです。

while (<DATA>) {
  chomp;
  print "$_\n" if ( /^_START_$/ .. /^_END_$/ );
}

__END__
AAA
_START_
BBB
CCC
_END_
DDD

特殊ファイルハンドル DATA には __END__ 以降のテキストが入っているのでしたね。DATA に入っているテキストを行入力演算子で毎行読み込んで、範囲演算子正規表現パターンを組み合わせて判定しています。

この場合 "_START_" という行が見つかったら、範囲演算子は "_END_" という行を見つけるまで真を返すようになります。("_START_" から "_END_" まで)

実行結果は以下の通りです。

_START_
BBB
CCC
_END_

_START_ と _END_ の間のテキストだけを表示する事ができました。


また、ON/OFF 条件として「リテラルな正の整数」(0, 1, 2...)を指定した場合、暗黙のルールとして特殊変数「$.」(最後に行入力演算子で読み込んだ行の行番号)と比較されます。

while (<DATA>) {
  chomp;
  print "$_\n" if (2..4);
}

__END__
AAA
BBB
CCC
DDD
EEE

この場合の範囲演算子は「2行目から4行目」と読み替えればいいわけですね。(^_^)

実行結果は以下の通りです。

BBB
CCC
DDD

「○○から××まで」を直感的に書く事ができて、コードがとても簡潔になりますね。使いどころがちょっと難しいですが、色々と応用できそうです。

2006-07-22

エスケープ処理された文字列を戻す

POST でパラメータを受け取る で作った echo.cgi に改行や日本語を含むテキストを送信してみると、ちゃんと表示されず「%」が含まれた文字列になってしまいますが、これは文字化けではありません。

例えば、名前(name)に「ぱるも」、本文(body)に「こんにちは(改行)かわいい犬ですね」と書いて送信すると

name  %82%CF%82%E9%82%E0
body  %82%B1%82%F1%82%C9%82%BF%82%CD%0D%0A%82%A9%82%ED%82%A2%82%A2%8C%A2%82%C5%82%B7%82%CB

と表示されます。(Shift-JIS の場合)

これは、パラメータブラウザによって送信される時に「エスケープ処理」されているからです。TMPL_VAR の ESCAPE 属性でも勉強しましたが、エスケープ処理とは「特殊な記号の意味を打ち消す為の処理」です。この場合の「特殊な記号」とは英数字以外の文字の事で、改行や日本語のマルチバイト文字なども含まれます。

例えば、パラメータの一部として「&」や「=」が入力されていると、パラメータを受け取ったプログラムが正しく解釈する事ができません。もし、「&」や「=」がパラメータの一部に含まれているとすると、以下のような REQUEST_URI でリクエストされた場合

/foo.cgi?title=aaa&body=bbb

title に「aaa&body=bbb」を指定したのか、それとも title は「aaa」までで、別に body が「bbb」として指定されているのか判断できませんよね。

このように、「特殊な意味」を持つ記号はブラウザによってエスケープ処理されてからパラメータに渡される事になっているのです。ただ、どの記号が「特殊な意味」を持っているのか、というのはプログラムによって異なるので、ブラウザの場合は英数字以外の文字は全てエスケープされる事になっています。


エスケープ処理後の文字列に含まれる「%82」や「%CF」の後ろの二文字「82」「CF」は、16進数によって表された「文字コード」ですね。(^_^)

コンピュータは 0 か 1 かのデジタルなものですので、「文字」も内部では数字で表されています。文字に対応する数字の事を、文字の「文字コード」(キャラクターコード)と言います。

1バイトの文字は「0から255の間の数字どれか」で表す事ができますが、この「0から255」というのは、2ケタの16進数(00からFF)で表せる数字の範囲と一致しているので、1バイトの文字を2ケタの16進数で表す事ができるのです。

日本語の1文字は「2バイト」で表されているので、「ぱるも」は 6 バイトで表される事になります。だから「%82%CF%82%E9%82%E0」と、6つの「%xx」が並びます。(Shift-JISEUC-JP などの場合)


では、エスケープ処理された文字列を元に戻す処理(アンエスケープ : unescape)を作ってみます。

流れとしては

  1. 文字列の中から「%xx」を見つける
  2. 「%xx」の「xx」の部分を16進数→10進数の数字に変換する
  3. 数字を文字コードとして対応する文字を取得する
  4. 文字列の「%xx」の部分を取得した文字で置き換える

を繰り返せばいいですね。

2. の「16進数→10進数」の変換は、hex 関数でできます。

print hex("FF"), "\n";
print hex("80"), "\n";
print hex("10"), "\n";

実行結果は以下の通りです。

255
128
16

また、3. の「文字コード→文字」の変換は、chr 関数でできます。ちなみに、逆の「文字→文字コード」は ord 関数を使います。

例えば "A" の文字コードは「perl -e "print ord('A')"」で調べてみたところ「65」でした。

print chr(65), chr(66), chr(67), "\n";

実行すると

ABC

と表示されます。

これらの関数を利用して、さらに s/// 演算子/e オプションと組み合わせれば、簡単に変換できますね。(^_^)

use strict;

sub unescape($) {
  my $s = shift;
  $s =~ s/%([0-9A-F]{2})/chr(hex($1))/ieg;
  return $s;
}

my $name = "%82%CF%82%E9%82%E0";
my $body = "%82%B1%82%F1%82%C9%82%BF%82%CD%0D%0A"
         . "%82%A9%82%ED%82%A2%82%A2%8C%A2%82%C5%82%B7%82%CB";

print unescape($name), "\n";
print unescape($body), "\n";

unescape 関数が、アンエスケープ処理を行なっている関数です。「[0-9A-F]{2}」というのは、「0から9とAからFまでの文字が2つ続いてる」という意味なので %xx にマッチします。16進数の部分をキャプチャして、先ほどの2つの関数を使って文字に変換してから置き換えます。

/e オプションを指定しているので「chr(hex($1))」がコードとして評価され、その返り値が置換後の文字列になるのでしたね。(^_^)

このスクリプトの実行結果は以下の通りです。

ぱるも
こんにちは
かわいい犬ですね

うまく変換できました。(^_^)

注意: 文字セットについて

文字と、その文字のコードの対応を決めるのが「文字セット」または「エンコーディング」などと呼ばれる規格です。例えば、上のスクリプトでは「Shift-JIS」という文字セットを利用しました。

「Shift-JIS」は主に Windows で利用される文字セットで、他にも「EUC-JP」や「JIS」など、様々な文字セットが存在します。これらは日本国内でよく使われる文字セットですが、各国が独自の文字セットを持っていたりするので、文字セットは数限りなく存在します。*1

例えば各文字セットで書いた「ぱるも」をエスケープ処理すると

Shift-JIS: %82%CF%82%E9%82%E0
EUC-JP:    %A4%D1%A4%EB%A4%E2
JIS:       %1B%24%42%24%51%24%6B%24%62%1B%28%42

と、全く違うものになります。


厄介なのが、パラメータを受け取る CGI プログラムを作る場合、与えられるパラメータに使われている文字セットが「わからない」という点です。多くのブラウザは、ページの HTML を書くのに使われている文字セットをパラメータの文字セットに利用するので、自分でページを作る場合は予想が付きます。

ですが、もし CGI プログラムを自由に利用できるように不特定多数に公開した場合、どんな文字コードパラメータが渡されるかは全く予想できません。Perl 内部の文字セットと違う文字セットのパラメータをそのまま使おうとすると、正規表現がマッチしなくなったり、文字化けしたりと、色々な不具合が生じます。

この問題を解消する為には、かの Dan Kogai さんによってメンテナンスされている Encode モジュールを利用して、文字セットを Perl 内部のものに変換する必要があります。Encode::Guess を利用すれば、文字列に使われている文字セットを推測する事ができます。とても便利ですね。(^_^)

Encode モジュールの使い方については、別のエントリで勉強したいと思います。

*1:これらを統一する為に「Unicode」と呼ばれる UTF-8UTF-16 などの統一文字セットが策定されたので、外国語を扱う場合はこちらを利用すればいいですね