綾小路龍之介の素人思考

蛇足:cgiがクエリをもらう時にもう少しスマートな書き方はないのか

cgiの解説をしたサイトでよくある例が次のようなものである。やっていることは、メソッドで分岐、適当な場所からクエリを取り込み、アンドで分割、得られた結果をイコールで分割、連想配列にする前にurlデコード。でもぜんぜんうれしくない。だって1つの仕事を完了させるのに長すぎるじゃないか。もうちょっと何とかならないもんかなぁ。探しても見つからないなら作ってしまえということで作ってみた。

my %q = map{&UrlDecode($_)}map{m/([^=]+)=([^=]+)/ ? ($1,$2) : ()}split/&/,$ENV{'QUERY_STRING'}.(read(STDIN,$_,$ENV{'CONTENT_LENGTH'}) ? '&'.$_ : '');

できばえはあまりよくないけど、とにかく使える。まぁCGI.pmとか使えばいいと言われればそれまでなんだけどさ。さて説明しよう。式の左辺を左側から見ていってほしい。

最初に標準入力とクエリ文字列を連結した。標準出力を受け取るのにread()を使うことはよくある例と同じだが、標準入力を特殊変数$_で受け取った。$bufferとかで標準入力を受け取る例をよく見るけど、名前のとおり一時変数なわけで、CGIが終了するまで保持する必要はどこにもない。そのうえ、加工後は使わないわけだからメモリがもったいないと思う。undefすれば無駄メモリを抑えられるが、これについて言及したスクリプト例はあまり見かけない。そんなわけで、特殊変数$_を使った。標準出力の受け取り後、read()の結果を評価して、真なら特殊変数'&'.$_を返し、偽なら''を返した。&は、標準入力の受け取りエラーや、受け取りバイト数が0バイトのような場合には不要なわけで、これをあらかじめクエリ文字列の後に付けてはならない。真偽の判定には、3項演算子を使った。3項演算子は簡潔に式がかけるのでうれしい。その上でこれとクエリ文字列を連結した。ここまでがよくある例でいえば、$bufferへの代入である。3項演算子と普通のif文との対応付けは下のような感じと解釈している。大雑把に言えばif(...){...}else{...}を簡単に書けるということだ。

if($_ =~ m/a/){$_='Include a';}else{$_='Not include a'}
$_ = m/a/ ? 'Include a' : 'Not include a' ;

忘れてはならないことがある。先のスクリプトではPOSTメソッドやGETメソッドの確認を行っていない。CGIのデファクトスタンダードを定めたRFCでは確認を行わねばならないという記述があったと思う。でもここでは無視。まぁ気が向いたらそちらのほうも作ってみようかな。といってもそんなに難しいことではないな。未来の自分への宿題ということで、ご勘弁ください。

次にこれらを&でsplitした。受け取った文字列の中にいくつ&が含まれるのかわからないので、そんな場合はlimit無しのsplitを使った。忘れてはならないのはlimitは最大の分割個数のことだということである。limitの個数に分割するのではない。言い換えれば、limitを2にしたら1個か2個かの要素をもつ配列を返すということである。このことは後から効いてくる。splitは配列を返すが、これの受け取りにmap{}を使っているため、分割パターンが現れた分だけ分割が行われる。分割されたそれぞれの要素について再度map{}である。

map{}は引数に配列をとり、戻り値に配列を取る。これは配列要素の加工に使う事が多いと思う。僕はgrep{}よりも汎用的に使える関数だと思っている。ここのmap{}ブロックがよくある例と大きく異なるところだ。ここには注意しなければならない。何を注意するかというと、map{}後の受け取りがスカラ変数なのか、配列なのかハッシュなのかということである。実際は連想配列%qを要求している。連想配列にキーと値を代入するには配列に代入するのと同じ手法、つまり、キー、値の順番で列挙する手法が使える。言い換えれば、キーと値を同時に指定しなければならないということである。どちらがかけても思ったようには動かない。

これは例を考えればよくわかる。今map{m/([^=]+)=([^=]+)/ ? ($1,$2) : ()}の代わりにmap{split/=/}を考え、ブロックに文字列'a='が渡されたとする。するとどうだろうか。splitの結果は要素数が1の配列である。新たに別の文字列'b=c'が渡されると、splitの結果は要素数が2の配列である。つまり%q=(a,b,c)ということになる。したがって、$q{a}=b,$q{c}=''という結果になってしまう。これが望まれたものでないことは明らかである。

そこでmap{m/([^=]+)=([^=]+)/ ? ($1,$2) : ()}とした。こうすることで戻り値は必ず要素数が0個または2個の配列が渡されることになる。これで先の例では%q=(b,c)のようになり、望まれた結果となるはずだ。これはよくある例で無意識的に行っていたことである。よくある例ではsplit文の戻り値に$keyと$valueを指定し、その上で%q{$key}=$valueだった。これにより必ず2つの要素からなる配列をsplit文から受け取り、これらを元にハッシュを作っていた。ただよくある例にも問題がある。それは悪意ある入力をフィルタリングできないことである。改良版ではこの点を正規表現を使ってクリアした。つまりURLエンコードの予約語である=を含まない文字列で囲まれたキーと値でなおかつ、key=value、のフォーマットでかかれたものだけを受け取り後は全て捨て去った。例でいえばaをキーに持つハッシュ%qは定義されていないのである。これは余分なメモリを消費しないという点では重要である。

最後にURLデコード処理を行った。URLデコードは汎用的な処理なのでサブルーチンにしたほうがよいと思ったのでここでは明記しなかった。さて、上のようにして十分なクエリ取得処理がかけたと思う。スクリプトの作成にあたって気をつけたことは、余分なメモリを消費しないこと、my宣言が必要な変数を汎用の特殊変数$_で置き換えることだろう。

ソーシャルブックマーク

  1. はてなブックマーク
  2. Google Bookmarks
  3. del.icio.us

ChangeLog

  1. Posted: 2008-01-23T10:58:10+09:00
  2. Modified: 2008-01-23T05:27:28+09:00
  3. Generated: 2017-01-01T23:09:41+09:00