Perl/CGI研究室 'PERL-LABO'

Perl/CGI研究室 'PERL-LABO' TOPへ
戻る(History.Back)

IDでジャンプ(外部ファイル)

研究内容

IDとジャンプ先URLの対応表を別のファイルにしてみましょう。

詳細

ジャンプ先URLをCGIに埋め込むのでは管理がちょっと大変かも知れません。 というのは、この、IDで別のページにジャンプするっていうCGIプログラムを、 複数の場所で使いたくなったとしましょう。 それぞれの場所で、IDとジャンプ先の組み合わせは異なるとしましょう。 そうすると、それぞれの場所にこのCGIプログラムをコピーして、 CGIプログラムの中の連想配列のところを 書き換える必要があります。で、その後、CGIプログラムに実はセキュリティーホールが あって、CGIプログラムを変更しなきゃいけなくなったとしましょう(凄い仮定ですけど (^^;) そうすると、それぞれでその修正を別々にしなきゃいけないってことになりますよね。

こういう、なにかデータを参照してアクションを起こすCGIプログラムの場合、 そのデータは別のファイルに入っていた方がいいような気がします。 上のセキュリティホールの例えは変かも知れませんが、 後で追加や変更が起こるデータファイルは CGIプログラム本体と離しておいた方が、いいんじゃないかと。

そんなわけで、 IDとURLの対応表を外部のファイルで用意する方法を研究してみます。

結果

データファイルの仕様を決める

まずデータファイルの仕様を決めましょう。 仕様というのは、 データをどうやってデータファイル内で表現するか、ということです。 データファイルっていうのはIDとURLの対応表を入れるファイルです。 もちろんテキストファイルですね。 データとしては、IDとURLのペアが複数、ということになります。

例えば、ブラウザからフォームが送られてくるときのように = と ? を使って 「ID=URL&ID=URL」とする方法が考えられますね。 でもこの方式は、複数のデータを改行を含まない1つの文字列として表すには 便利なんですが、ファイルのように複数行でデータを表すことができる場合には 逆に不便だったりします。というのは、データを追加したり、変更したり するときに、こういう、1行でだらだらと長いデータだったら、嫌になっちゃいますよ。 = と & でデータを表すこの形式、人間が見たら見にくくて仕方ありません。 ということで、人間にとっても見やすくて扱いやすいのがいいですね。 でも見やすいだけじゃ駄目で、CGIプログラムが扱いやすいようなものに しないといけませんけど。

そうすると、まず、1行につきIDとURLが1個ずつ、というのが良さそうですね。 & の代わりに改行を使う感じです。 1行ずつ分かれていれば、人間にとって見やすいですし、 Perlは、1行ずつ処理するっていうのは得意ですから。

次に、IDとURLを区別する方法が必要です。 まず、フォームデータを見習って、区切りに = を使う方法がありますね。 ID=URL です。これでもいいんですが、ちょっと注意することがあります。 フォームデータの時に、? / = などを %〜 という別の文字列にエンコードしていたのって、 データと区切り文字が混ざらないようにするためでした。 もし、IDの中に = があったら、1行に = が2個になって、どこが区切りなのか分からなく なってしまいます。 そんなわけで、データ内に区切り文字が出てきたら、 本当の区切り文字と区別するために別の文字にエンコードするっていうのが 必要になるんです。でも、それはとりあえずちょっと面倒。 データ内で使われないものを区切り文字に選べば、そういう手間がはぶけます。

これに関しては、「よく使われる区切り」っていうのがあるんです。 それは、<> です。区切りは1文字じゃなくても特に困りません。 この区切りが使われ始めたのがいつでどこなのか、分からないのですが、 <> はもちろんURLなどに出てこないし、 これで区切られてると人間にとってもすごく見やすい。 データの中にこれが出てきちゃ駄目なわけですが、 こういうデータはあんまりなさそうです。 もしでてきたら、なにか手を打たないといけませんけど、 とりあえずしばらくは大丈夫そうです。 ということで、区切りはこの <> を使わせていただくことにしましょう!

あと、ファイルを見やすくできるように、空行を入れても大丈夫、ということにしましょう。 これで仕様が決まりました。ファイルの中身はこんな感じになります。

yahoo<>http://www.yahoo.co.jp/
google<>http://www.google.co.jp/
データファイルの拡張子はcgi

データファイルの拡張子なんてどうでもいいような気もしますが、 ちょっと注意が必要なんです。データファイルはCGIプログラムのそばとか、 そのCGIプログラムを呼び出すHTMLファイルのそばとかにありますよね。 ということは、そのデータファイルのファイル名をブラウザで入力すれば、 ブラウザからアクセスできる場所に置かれることがほとんどです。 別に中を見られてもどうってことのないファイルならいいんですが、 そうでない場合は大変!秘密のパスワードとかが丸見えなんてことに なったら目もあてられません。そんなわけで、データファイルは、 ブラウザ上で見えないようにしたいです。

そのためにいい方法があります。データファイルの拡張子をcgiにするんです。 データファイルはCGIプログラムじゃありません。 パーミッションで実行権も与えません。 でもこれでいいんです。ウェブサーバーは、cgiという拡張子のファイルを CGIプログラムだと思って実行しようとしますが、実行権が無いので エラーになります。ブラウザにはエラー画面が出るだけです。 もし実行権を間違って設定してしまったとしても大丈夫。 実際に実行しても、中身はなんだかわからんデータですから、 実行後にエラーになります。 どうしたって、中身がブラウザで表示されることはありません。 というわけで、データファイルの拡張子はcgi! これで決まりですね。

データファイルを読んで連想配列に入れる手順

データファイルは 名前=値 で連想配列にぴったりの形式。 ていうか今回は連想配列をデータファイルにするんで当たり前ですけども。 これを読み込んで、連想配列にする関数を作りましょう。 作るのは、IDとジャンプ先URLのペアを読み込んで連想配列に入れる関数ですが、 よく考えてみると、なにも、そのためにしか使えないというわけではありません。 名前<>値 という形のファイルならなんでも同じやり方で読み込めて、連想配列にできます。 これなら、他のところでも使える可能性があります。 だから、関数にして、 例によって plabパッケージ内のライブラリ関数として作成します。

さて、やることは「ファイルを開く」「1行読み込む」 「名前と値を取り出す(splitを使えばいいですね)」 「連想配列にデータを入れる」「繰り返し」「ファイルの終わりまでいったら終わり」 という感じです。

ファイルを開く

ファイルを開くには open を使います。メールを送る処理では、パイプというものを 使ってsendmailに対してデータを出力するということをしましたが、 今回はファイルからの読み込みです。ファイルを開くときには、 読み込むのか、書き込むのか、ということを指定する必要があります。 読み込むとき次のようにします。

open ファイルハンドル, "< ファイル名";

< というのがポイントです。今回は、ファイル名を jumptable.cgi として、 CGIプログラムと同じところにこのファイルがあるとしましょう。次のようになります。

open FILE, "< jumptable.cgi";

ファイルハンドルは好きな名前でよかったんでしたね。それに全部大文字にするのが通例なんでした。 ファイルが存在しなかった場合はエラーになります。 エラーが起きたときになにか処理をする場合は、if (! open 〜 とするんでした。

あ、そうだ。前回 open がでてきたときは、その後ろを ( ) で囲っていましたね。 今回は囲っていません。これ、どっちでもいいみたいです。( ) っていうのは、 省略できることが多いみたいです。なので、見やすい方を使えばいいんですね。 前回、if (! open〜 という形ででてきたときは、( ) があった方が見やすかったと 思います。だから、基本的に ( ) が省略できるときでも、( ) を付けるように した方がいいのかな?でも、( ) で囲むと逆に見にくくなったりするところも あるだろうし、これは好みですが、付ける付けないをどちらかに決めるものでも ないかもしれません。今後も、付けたり付けなかったりしてしまいそうですが、 そのうち付けるときと付けないときがはっきりしてくると思います。

ファイルを1行ずつ読み込みながら処理をする

Perlはファイルの処理もすごく便利なようになっています。 ファイルを1行ずつ読み込んで、1行ずつなにか処理をするっていうのは Perlの得意な処理の1つみたいですね。 次のようにします。

while (<ファイルハンドル>) {
	変数 $_ に1行分のテキストが入っているので、処理する
}

while というのは、条件が成り立つ間処理を繰り返すっていう命令です。 while (条件) 〜 という形で使います。 <ファイルハンドル> っていうのは、ここでは、ファイルから 1行読み込んで変数 $_ に入れるっていう意味になります。 この < > を行入力演算子っていいます。 ここではwhileの条件に行入力演算子を入れていますが、 これは、行の読み込みが成功すれば条件はtrueとなって処理が行われ、 ファイルを全部読み終わって行の読み込みに失敗するとfalseとなって 処理が終わります。(正確には、行の読み込みに失敗すると未定義値っていうのを返すみたいです。 未定義値は条件式の中に現れるとfalseになるということですね。) ということで、ファイルの終わりまで、1行ずつ処理をするっていう 意味になります。それが、while (<ファイルハンドル>) 〜 っていう短い文で 表現できるわけです。

行を解析して名前と値を取り出す

行の内容は $_ に入っていますので、splitでこれを名前と値に分けましょう。 そして、連想配列に入れます。これは前にやった知識で作れますね。 splitを使います。前は、1つの文字で文字列を区切ってある場合に使いましたが、 今回のように2文字、あるいはそれ以上の文字列で区切ってある場合にも使えます。 こんな感じになります。

while (<FILE>) {
	($name, $val) = split('<>', $_);
	$table{$name} = $val;
}

たったこれだけで、できちゃいました!…といいたいところですが、 この処理には注意点があります。 <FILE> で行を読み込んだ場合、行末の目に見えない改行コードも 一緒に読み込むんです。改行コードは単にデータの区切りですから、 必要ないんですが、でも読んでくれるんです…。そういう機能ですから、仕方ありません。 これを、除去する処理が必要になってきます。

chompで改行を取る

改行コードを取るのに、これまたいい機能があるんです! それは、chomp というものです。chomp 変数 とすると、変数の中の末尾が 改行コードだった場合、その改行コードを取ってくれるんです。 改行コードが無かった場合、なにもしません。 まさにしたいことをしてくれる関数です。 splitする前に、改行コードを捨てちゃいましょう。

while (<FILE>) {
	chomp $_;
	($name, $val) = split('<>', $_);
	$table{$name} = $val;
}
なにも無い行を無視する

あともう1つ。データファイルの中になにも無い行があったら、 それを無視するようにしましょう。 これ、なんでかというと、データファイルを見やすくするために 途中に改行を入れても大丈夫なようにするっていうのもあるんですけど、 ファイルの最後になにもない空の行があったとき、それを無視するように したいからです。ファイルの最後の空の行っていうのは、よくありますから。 で、こんなふうにしました。

while (<FILE>) {
	chomp $_;
	($name, $val) = split('<>', $_);
	if ($name ne "") {
		$table{$name} = $val;
	}
}

ne っていうのは、文字列比較で、「〜でない」という意味です。 "" っていうのは、空の文字列っていうこと。 つまり、空の文字列でない場合だけ、連想配列に値を入れるっていうことですね。

これでOKですね!

ファイルを閉じる

ファイルを閉じるには、close を使うんでしたね。

close FILE;

ファイルを開いたら、必ず閉じましょう。 閉じないと駄目なのかっていうと、読み込みに関してはそれほど 駄目じゃないような気もしますが、ファイルは必ず処理が終わったらすぐに閉じる って覚えておいた方がいいです。閉じ忘れると、ヘンなことが起きて、原因が分からない なんていうことになりますから。

これで大体、やり方がわかりました。これをまとめて関数にしましょう。

デフォルト引数 $_ について

ちょっと説明が後回しになりましたが、 while の中では $_ に1行分のテキストが入っているということでした。 この $_ をデフォルト引数と言うんだそうです。 デフォルト引数というのは、引数を省略したときに使われる引数なんだそうです。 本来、<ファイルハンドル> というのは $line = <ファイルハンドル> というように 変数に代入する形で使うものですが、この代入先引数が省略されたため、デフォルト引数 $_ に 代入されるんだそうです。このデフォルト引数、なんのためにあるかと言えば、 プログラムを作るのが楽になるようにっていうことみたいです。

たとえば、print "文字列"; ってすると 文字列 が出力されますよね。 それじゃ print; ってするとどうなるかっていうと、引数が省略されたので $_ が指定された場合、 つまり print $_; と同じことになるんだそうです。うーん。楽ちん。 要するに、$_ を使うときは書かなくていいんですね。

でも、管理人のような初心者には、慣れていないせいもあってよく飲み込めない感じもあります。 それに、省略すると分かりにくくなる気がします。 chomp; より chomp $_; の方が分かりやすくないですか? そう思うのは今だけかも知れませんけど (^^; 慣れたら便利なんでしょうね。でもしばらくは、$_ と省略せずに書くようにしますね。 省略せずに書いてもたったの2文字ですし (^^

関数名

関数には関数名が必要です。さあ、この関数の名前、なんにしましょう? ファイルから読み込むので、readほげほげ、という名前にしたいです。 ほげほげ、のところをなんにしたらいいのか…。こういう形式データファイルって どんなふうに呼ぶんでしょう? 困りました。 で、ファイルの形式を関数名にするのはやめて、 ファイルを読んで連想配列にするっていう機能から、 readhashfileという関数にしちゃいます。 連想配列のことを、ハッシュともいうので。

関数にファイル名を渡すには

そう、今回、ファイルからデータを読み込んで連想配列に入れる関数を作るんでした。 関数にして、他のプログラムでも利用できるようにするためです。 ということは、ファイルを開くときのファイル名、これをどうしたらいいんでしょう? というのは、

sub readhashfile
{
	open FILE, "< jumptable.cgi";
	…
}

としてしまうと、この名前のファイルしか読めなくなってしまいます。 いろんなプログラムで利用するということは、ファイル名を関数の呼び出し側で 指定できないといけません。

こういうときは、関数の引数っていうのを使います。 関数の呼び出しのときに、データを関数に渡すんです。こんな風に

%table = readhashfile('jumptable.cgi');

そして、関数の方でこれを受け取ります。

sub readhashfile
{
	$filename = $_[0];
	(処理)
}

こういう、関数に渡すデータを引数というんです。 ヒキスウです。 今までも、openとかsplitとかの関数を使うときに引数を渡していました。 自分で作った関数にも引数を渡すことができます。 引数は、関数の方では $_[番号] という形で利用します。 番号は 0 からの整数で、引数の番号です。1つ目の引数が 0 番、次が 1 番っていうふうに なっています。 これ、見覚えがありますね。番号でアクセスするもの。 そう、配列です。関数の中では、引数に与えられたものが 配列 @_ というものに入っているんです。 配列はアクセスするときに $ になるんでしたね。 そんなわけで、$_[0] みたいになります。 なお、今回は引数は1個なので、$_[0] だけしか使いません。

ふう。問題解決。

これで作れるかな?

これくらい調べれば、作れそうな気がしますね。 作ってみましょう。

作成したCGIプログラム

gotobyid.cgi
#!/usr/bin/perl

require 'hashfile.pl';

$id = $ENV{"QUERY_STRING"};

%urltable = plab::readhashfile("jumptable.cgi");

if (exists $urltable{$id}) {
	$url = $urltable{$id};
	print "Location: $url\n";
	print "\n";
}
else {
	print "Content-type: text/html\n";
	print "\n";
	print "ID:$idは登録されていません。";
}
hashfile.pl
package plab;

sub readhashfile
{
	local %table;
	local $fname;
	local($name, $val);

	$fname = $_[0];

	open FILE, "< $fname";
	while (<FILE>) {
		chomp $_;
		($name, $val) = split('<>', $_);
		if ($name ne "") {
			$table{$name} = $val;
		}
	}

	close FILE;
	return %table;
}

1;
jumptable.cgi (データファイル)
yahoo<>http://www.yahoo.co.jp/
google<>http://www.google.co.jp/

結果

次のリンクをクリックしてください。

gotobyid.cgi?yahooを実行してYahooを開きます!
gotobyid.cgi?googleを実行してGoogleを開きます!
gotobyid.cgi?errを実行してエラーのテストをします!

考察

無事動きました。今回の研究で、ファイルの読み込み処理がちょっと分かりました。 あと、ちょっと試したんですが、関数の中のファイルハンドル、今回は FILE という名前なんですが、 これ、local にしなくていいのかな?と思って local にしてみたらエラーになってしまいました。 ファイルハンドルは local にしなくてもいいっていうか、local にできないみたいです。 なんでだろう?ちょっと分かりませんでした。

あと、後で思ったんですが、データを別ファイルにするやり方として、 require を使うっていう手もありましたね。 初期化データ付きの連想配列の状態で別ファイルにしておいて、require で読み込む。 それでも良かったのかなぁ。でも今回、汎用的な関数を1個作れたのでよしとしましょう。

…今回作った readhashfile関数、どこかで役に立つかな?

分かったこと

  1. データファイル内で区切りに <> を使用しました。 これはよく使われる区切り記号のようです。
    見やすいというのと、一般の文字列にはあんまり現れないなどの理由から使われているのだと思います。
  2. データファイルの拡張子は、中身を見られないようにするために cgi とします。
  3. ファイルの読み込みオープンは open ファイルハンドル, "< ファイル名"; とします。
  4. 関数呼び出しの ( ) はあっても無くてもいいみたいです。
  5. while()とすると、ファイルを1行ずつ読み込んで各行に対して処理を行えます。
    ファイルの行は $_ という変数に入っています。
  6. split は文字区切りだけじゃなくて文字列区切りでも使えます。
  7. 改行も含めて読み込まれるようですので、文字列末尾の改行を除去する関数 chomp を使用しました。
  8. 関数への引数は、関数内で $_[0]、$_[1] などとして参照できます。
  9. ファイルハンドルは local 宣言できませんでした。
Perl/CGI研究室 'PERL-LABO' TOPへ
戻る(History.Back)

Copyright (c) 'PERL-LABO' All Rights Reserved.  リンクフリーです。