R.A. Epigonos et al.

Perl > クエリを連想配列で受け取るスマートな方法

読み直してみるとこのページはわかりにくいコードを推奨しているような気がする

目次

序論:cgiがクエリをもらう時の良くある方法にスマートなものが無い理由

探しても探しても出てくるのは下のようなものかこれの亜種ばかり。これが一番なのか?1ブロックに1機能もいいけど1行で1機能を追及したいな。大体共有サーバで容量制限がある中では1文字でも惜しいしね。とりあえず今のところは可読性のある論理1行でクエリを変数に取り込むという機能がほしいな。

if($ENV{'REQUEST_METHOD'} eq 'POST'){
  read(STDIN,$buffer,$ENV{'CONTENT_LENGTH'});
}else{
  $buffer = $ENV{'QUERY_STRING'};
}

if}else{}を3項演算子で1行に集約できないかな?と言うことで下のようになる。でもこれだけだと余り嬉しくないな。だって2度と使わないスカラー変数$bufferを使っているからね。だから願わくば$buffer=として受け取りたい。だって、この後にハッシュにキーと値を入れる時にsplit文で切り分けるけど、この時に切り分け対象文字列の含まれた変数を指定しなきゃならないからね。でも良く考えてみればこの問題はすぐに解決するかもしれない。split文は何も指定なければ特殊変数$_を切り分け対象文字列の含まれた変数として取るんだ。このことを使えないかな。

$ENV{'REQUEST_METHOD'} eq 'POST' ? read(STDIN,$buffer,$ENV{'CONTENT_LENGTH'}) : ( $buffer = $ENV{'QUERY_STRING'} );

そう、$bufferを$_に置き換えてしまえば良いんだ。例えば下のような感じにできる。このほうが1行が短くできるし、自分で定義した変数を使っていないと言う点でアドバンテージがあると思うなぁ。何よりブロックで囲んで$bufferにmy宣言をしなくてもいいと言うのが楽だ。

$ENV{'REQUEST_METHOD'} eq 'POST' ? read(STDIN,$_,$ENV{'CONTENT_LENGTH'}) : ( $_ = $ENV{'QUERY_STRING'} );

さて、良くある例に戻って考える。いまスカラー変数$bufferにクエリの内容が入っている。これを分割してハッシュに格納したい。良くある例は下のような感じかな。まぁ突っ込みどころ満載のスクリプトだけどさ。やっぱり見やすいけど長い。連想配列に格納するだけにしては勝手に定義した変数を4つも使っているのはなんだかいやだな。

my @query = split/&/,$buffer;
foreach(@query){
        my ($key,$value) = split/=/,$_;
        $key     = &UreDecode($key);
        $value   = &UreDecode($value);
        $q{$key} = $value;
}

文句言ってても始まらないので短くしてみる。下のような感じだ。上のやつとは異なる結果を返すが、実用上ほとんど相違ないと判断したのがこれ。ただ正規表現による篩い分けを行っているので特にクリエが長くなった時の実行速度に難ありかも。でも篩い分けは必要だWebMasterは悪意ある入力からSystemを守らなければならないと思うから。

my %q = map{&UrlDecode($_)}map{m/^([^=]+)=([^=]+)$/}split/&/;

注意:既存の方法との差異

例えばフォームからメールアドレスとニックネームの入力を受け取る時、メールアドレスを書き込まないようにしている訪問者もいるだろう。すると、これまで述べてきた方法を使用した結果、特殊変数$_に収められたクエリは次のようになる。

$_ = 'email=&nicname=hoge';

これを分割してフィールドごとに分割し、後に、名前と値で分割するが、このとき正規表現による篩い分けを行っているため、キーにemailをもつハッシュ%qは作られない。つまり、$q{email}は未定義ということになる。これは旧来の手法と異なる結果である。旧来の方法では$q{email}は定義されているが空文字列ということになる。旧来の方法では余分なメモリを消費していたことになると思う。つまり、CGIにとったらメールアドレスが書き込まれたか書き込まれなかったかを知りたいので、書き込まれなかった場合にこれが空文字なのか未定義なのかはどうでも良いことだと思う。したがって多分新しい方法と旧来の方法は互換性があると思う。

また別の例で次のようなれいについて考えてみる。この場合、$q{nicname}=0である。

$_ = 'email=&nicname=0';

ニックネームの入力判定を行う場合、旧来の方法を使うと次のように条件判定文を書くことになるだろう。つまり、$q{'nicname'}が空文字列の場合にエラーメッセージ表示という事である。

if ( $q{'nicname'} eq '' ){
  print 'ニックネームが入力されていません。';
}

新しい方法では値が空文字列の場合には要素自体が作製されないため、ニックネームの入力判定を行うには次のようにするのが望ましい。

if ( !defined $q{'nicname'} ){
  print 'ニックネームが入力されていません。';
}

新しい方法に置換して旧来の判定を使うと、設定によってはエラーメッセージが出ることがある。なぜなら、旧来の条件判定文では$q{'nicname'}が判定以前に定義されていることが判定を行うための必要条件だからだ。$q{'nicname'}が未定義の場合、定義されていない可能性があります、というエラーを吐く可能性がある。といってもこの手のエラーはPerlのエラーメッセージの危険度的に言えば、最も危険度が低いエラーメッセージだったはずなので、多分問題ないだろう。まぁ気になるのなら旧来の方法をそのまま使い続ければいい。新しい方法は旧来の方法が長すぎて入力がめんどくさいから作ったのである。スクリプトを書き換えるほうがよっぽどめんどくさいので、新しい方法はこれから書き始めるスクリプトに適応されるものと考えたほうがいいと思う。

つまり未定義と言うことは、空文字列だったと言うことである。

結論:クエリを連想配列に格納する最もスマートな方法

最終的には2行で事足りる。1行目でクリエの取得機能、2行目で連想配列格納機能を実現している。以降スクリプトの中で使われるであろうハッシュ%qを作るまでに、これ以外の自分で定義した変数は1つも使われていない。全て特殊変数$_で話をつけている。特殊変数$_はmy宣言できないのはもちろんのこと、グローバル変数である。

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

使用する上で十分に気をつけねばならないことは、上記2行の間に別の処理を挟まないことである。言い換えれば、クエリを連想配列に格納する作業は一気に行え、ということである。なぜなら、前述の如く、特殊変数$_を何回も書き換えているため、順番がずれたり、余分な処理が入ると、整合性が取れなくなり、意図しない結果を得るからである。上の2行をブロックにしてスクリプト中で使用するのが望ましいと思う。

蛇足: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: 2003-11-16T18:01:02+09:00
  2. Modified: 2003-11-16T13:57:09+09:00
  3. Generated: 2023-08-27T23:09:16+09:00