Perl/CGIプログラムの時刻取得と時間計算
今回は、Perl/CGIプログラムで現在の時刻を取得したり、過去や未来の時間を計算する方法について学習していきましょう。
編集前記
今回の編集前記は、 Perl/CGIプログラム の排他制御についてのお話です。
排他制御というのは、記録ファイルなどのデータベースの整合性をはかるため、プログラムに対して一時的な動作制限をかけることです。
あえていろんなことを犠牲にし、簡単に説明するならこんな感じです(苦笑)。
どういうことかと言いますと…。
CGIとしてインターネットサーバーに常駐していると、複数の人間がほとんど同時に1つのプログラムファイルにアクセスするという現象が起こるときがありますよね。
そんなとき、もしそのPerl/CGIプログラム中に、記録ファイルなどのデータベースを書き換えるプロセスが含まれていた場合…。
ほとんど同時にアクセスしてきた人たちで一斉に記録ファイルなどのデータベースを書き換えることになりますよね。
しかし、データを書き換える作業というのは、どんなに高速なコンピューターであっても、1回の書き換え作業に対して1人分しかこなせないものなのです。
つまり、1回の書き換え作業に対して10人分も20人分も同時にこなすことはできないのです。
わかりやすいところでは、少し前に open関数 を使ったファイル処理について学習しましたよね。
あのファイル処理で書き換えられるデータというのは、そのPerl/CGIプログラムが持っているデータつまり、1人分のデータですよね。
当然、同じプログラムであってもアクセスした人が違えば、それは別のプログラム扱いです。
したがって、10人アクセスしてプログラムを動作させていれば、10回のファイル処理が発生するわけです。
それは10人ばらばらにアクセスした場合でも、ほぼ同時にアクセスした場合でも同じです。
なので、アクセスしたタイミングが同じであれば、複数の人で同時に1つのファイルに対して変更処理を行おうとすることになりますよね。
つまり、複数の人で同時にopen関数の書き込みモードでファイルを開こうとするわけですね。
そうなるとどうなってしまうのかというと、単純にファイルの中身があいまいになってしまいます。
最悪の場合、ファイルの中身が全部消えてしまうことだってあるんです。
わかりやすくするために、ファイル処理のプロセスをもっと具体的にしてみましょう。
例えば、在庫管理システムがあったとします。
外部ファイルには商品在庫数を記録しておきます。
注文を受けたプログラムは外部ファイルの在庫数をチェックし、在庫があれば「OK」を、なければ「NO」を表示するものだとします。
流れ図はこんな感じ。
1、記録ファイルを開き在庫があれば次のプロセスへ、なければファイルを閉じ「NO」を表示し終了。
2、在庫数から1マイナスして残りの在庫数を求める。
3、記録ファイルに求めた在庫数を書き込む。
4、ファイルを閉じ「OK」を表示し終了。
まぁこんな単純な在庫管理システムなんて実際には存在しませんが、イメージしやすくするためにおもいっきり単純化してみました。
で、このシステムの場合どんなことが起こるのかというと…。
例えば、AさんとBさんの2人が、ほぼ同時にこのシステムにアクセスした場合を時系列で考えてみます。
1、Aさんのプロセス1「記録ファイルを開き在庫があれば次のプロセスへ、なければファイルを閉じ「NO」を表示し終了」。
2、Bさんのプロセス1「記録ファイルを開き在庫があれば次のプロセスへ、なければファイルを閉じ「NO」を表示し終了」。
3、Aさんのプロセス2「在庫数から1マイナスして残りの在庫数を求める」。
4、Bさんのプロセス2「在庫数から1マイナスして残りの在庫数を求める」。
5、Aさんのプロセス3「記録ファイルに求めた在庫数を書き込む」。
6、Bさんのプロセス3「記録ファイルに求めた在庫数を書き込む」。
7、Aさんのプロセス4「ファイルを閉じ「OK」を表示し終了」。
8、Bさんのプロセス4「ファイルを閉じ「OK」を表示し終了」。
1つのPerl/CGIプログラムが複数の人にほぼ同時にアクセスされると、こんな感じになります。
しかし、この流れには矛盾がありますよね。
どこに矛盾があるのかというと…。
それは、AさんもBさんも、同じ内容のファイルを読んでいるというところです。
同じファイルの内容を読み、それを基準に処理していますから、本当なら2人分の在庫が減っていなければいけないのに、上の流れでは1つしか減らないことになります。
本来、Bさんが読み込むべきファイルというのは、もともとの在庫数からAさん分の商品を引いた数でなくてはいけませんよね。
でないと、在庫数と注文数が合わなくなりますからね。
しかし、Bさんも、Aさんと同じタイミングでファイルを開き、同じ値を読み込んでいるんです。
そして最後にファイルを書き込んだのはBさんですから、Aさんの注文がなかったことになってしまいます。
これでは困りますよね。
なので、 Perl/CGIプログラムでファイル処理 を使うときには、排他制御というプロセスを組み込むようにするわけなのです。
排他制御というのは冒頭でも書いたように、記録ファイルなどのデータベースの整合性をはかるという目的のため、プログラムに対して一時的な動作制限をかけることです。
前半の「データの整合性」ということについては、AさんとBさんの例でわかりましたよね。
なので次は後半の、「プログラムに対して動作制限をかける」ということについて書いていきます。
せっかく在庫管理システムの例を出したので、もう少しこれを使いまわして書いていくことにしましょう。
先ほども書きましたように、この在庫管理システムの問題は…
AさんとBさんの例のように、複数の人がほぼ同時にアクセスした場合、全員が同じ内容のファイルを読み込んでしまうというところでしたよね。
つまり、みんなで一斉にファイルを読み書きしようとするのがよくないわけですね。
なので、Perl/CGIプログラムを制御し、なんとか1人ずつファイルを読み書きするようにすれば問題ないわけです。
ようするに、Aさんがファイルを読み書きしている間はBさんを待ち状態にし、Aさんの処理が終わったら、Bさんがファイル処理を開始するようにすればよいということです。
このような、1人がファイルを読み書きしている間は誰も割り込みできないようにさせる仕組みのことを、プログラミングの専門用語でファイルロックと言ったりします。
文字通りファイルがロックされているわけですから、お一人様限定の環境を作り出すわけですね。
汚い例で申し訳ないのですが、ひとつしかないトイレに鍵をかけ、あなただけの空間を作るのと同じようなイメージです。
他の人もトイレをしたければ、鍵のかかったドアの前で待つしかありませんよね。
まぁ、ドアをたたいて催促することぐらいはできますが、中に入ることはできませんよね(笑)。
早い話、このような仕組みをPerl/CGIプログラムで作り上げようじゃないかということです。
それでは具体的に、ファイルロックの種類と使用する関数を挙げていきますね。
1、symlink式ロック
symlink関数を使うので、個人的にsymlink式ロックと呼んでいます。
あらかじめ指定したパス上にシンボリックリンクが存在するときが、ファイルロックのかかっているときと定義づけます。
ファイルロック時にsymlink関数でシンボリックリンクを作成します。
ファイルロックを解除するときは、unlink関数でシンボリックリンクを削除します。
symlink関数やunlink関数について は前回学習しましたね。
symlink関数ですから、シンボリックリンク作成に成功したときと失敗したときとでは、戻り値が違いますよね。
それを if文 で判断させれば、ファイルロックが成功したか失敗したかがわかりますよね。
2、mkdir式ロック
前述しましたsymlink式ロックの mkdir関数 バージョンです。
改めて説明することではないので省略しますね。
3、rename式ロック
これも、前述しましたsymlink式ロックとほぼ同じです。
違うところは、 rename関数 ですから、あらかじめキーとなるファイルを用意しておく必要があるというところです。
ファイルロック時に、rename関数でキーとなるファイル名を変更。
どんな名前に変更するかは自由ですが、ある特定の箇所に名前を変更した時刻を入れるようにしておけば、いろいろと便利そうです。
ファイルロックを解除するときは、再びrename関数でキーとなるファイル名を戻します。
以上この3種類が、ファイルロック構築の常套手段として多く使われています。
それぞれの関数について忘れてしまった場合は、復習しておいてくださいね。
ファイルロックシステム構築時に注意すべきこととしては、「すでにファイルロックがかかっていたらどうするか?」ということを考えておく必要があるというところです。
どれだけ待つのか?
Perl/CGIには、sleepという関数があります。
これは、引数に与えた秒数文だけ、プログラムを一時停止させる関数です。
なので、 ループ 箇所にファイルロックと、sleep関数を中にはさんでおくことにより、数秒おきにファイルロックを試みるシステムが完成します。
しかし、sleep関数も使いすぎれば、場合によってユーザーをずっと待たせてしまうことにもなりかねません。
というのも、ファイルロックがかかりっぱなしになっている状態というのがあるからですね。
単純に、アクセスが集中しすぎてなかなか順番が回ってこないこともありますし。
ごくまれにですが、ファイルロックをかけた瞬間何らかの原因でサーバーがダウンしてしまい、その後サーバーが復旧しても、ロックがかかりっぱなしになってしまうことがあります。
そのような事態も想定しておく必要があるので、自作する場合ファイルロックシステムは慎重に構築していきましょう。
最後に、ファイルを読み込みそれを加工した上で書き込む場合の安全な排他制御の手順を書いておきますね。
1、ロックする
2、ファイルを読み込む
3、一時ファイルに書き込む
4、一時ファイル名を元ファイル名に変更する
5、ファイルロックを解除する
このほかにもいろいろな方法があるとは思いますが、だいたいこんなところでしょうね。
それではいつものように強引に、Perl/CGIプログラミング学習に移りましょう(笑)。
今回は、時の計算方法について学習していきます。
Perl/CGIプログラムで時刻を求めたり時間を計算する場面というのはたくさんありますからね。
現在の時刻はもちろん、プログラムによっては過去や未来の時間を求めるといったことも必要になってきますから。
そういった意味でも、今回学習する内容というのは大切ですよ。
時刻表示
まずは、 Perl/CGIプログラム で現在の時刻を求める関数についてみていきましょう。
書式
「gmtime(time)」
「localtime(time)」
gmtime関数は、グリニッジ標準時を求めることができます。
localtime関数は、日本標準時を求めることができます。
なので日本国内であれば、localtime関数の方が圧倒的に利用頻度は多くなります。
gmtime関数は、世界共通のフォーマットで時刻を求めるときぐらいですね。
なので、localtime関数中心にみていきますね。
localtime関数の使用場面の例は、改めて説明する必要ないですね。
どんな用途のプログラムであっても、現在の時刻を求めたいときにはこの関数を使います。
これに対して、gmtime関数の使用用途は限られます。
例えばRSSのタイムスタンプとかですね。
RSSというのは、一言で言うと管理者が作成するウェブサイトの更新情報をひとまとめにしたファイルのことです。
ウェブサイト上に公開されているので、サイト訪問者は気に入ったRSSを登録し、RSSリーダーで読むことができます。
つまり、気に入ったサイトの更新情報を、わざわざウェブサイトにアクセスすることなく取得することができるわけです。
そんなRSSには、世界共通のフォーマットが決まっていますから、時間情報を付加したい場合には、gmtime関数を使うのです。
次に、それぞれの関数の役割をみていきましょう。
どちらの関数もある基準となる時刻を元に、引数の値(秒数)を足し、年月日などの各時刻データを返します。
つまり、ある基準となる時刻から何秒経過したら、何年何月何日になるかを算出してくれる関数だということです。
ちなみに、そのある基準となる時刻を求めたい場合は、単純に引数を「0」にすれば求めることができますよ。
ある基準となる時刻から0秒経過した時刻を求めるということになるわけですからね。
そうではなく、単純に現在の時刻を知りたい場合には…。
ここでの例のように、time関数を使ったり、引数そのものを省略すると現在の時刻を求めることができるようになっています。
gmtime関数とlocaltime関数は、求められる時刻のタイプは違えど、返される値のフォーマットは同じです。
次に、その返される値の話をします。
つまり、年月日などの各時刻を表したデータが、どんなフォーマットで返されるかという話ですね。
しかし、この関数から返される各時刻データというのは、プログラムの書き方によって、いくつかカスタマイズできるようになっています。
一般的には、配列変数のようなリスト形式で受け取ります。
そうすると、「年」とか「月」のように、各時刻データの要素をバラバラに扱うことができるようになりますからね…。
それでは次は具体的に、localtime関数の使い方についてみていきましょう。
localtime関数の使い方(その1)
まずは、シンプルな使い方からいきましょう。
#!/usr/bin/perl
use strict;
my $time = time;
print "Content-type: text/html\n\n";
print $time, "<br>\n";
print localtime($time), "<br>\n";
exit;
このPerl/CGIプログラムを実行すると、2種類の値が表示されます。
時刻を求めているわけですからタイミングによってその内容は変化します。
time関数で返された秒数を スカラ変数 「$time」に格納し、その値を元にprint関数で2度表示させています。
1度目はスカラ変数「$time」の中身がわかるようにそのまま表示させています。
2度目はlocaltime関数の引数にスカラ変数「$time」の値をわたし、時刻情報を表示させています。
例えば、「$time = time」で…。
変数「$time」には、「1192063721」という値が格納されたとします。
その場合、「localtime($time)」とすると、「4148911910742830」という値が返されます。
この「4148911910742830」がローカルタイムということになるわけです。
でも、これだけでは何がなんだかわからないですよね(苦笑)。
なので、この「4148911910742830」を分解すると…。
「41=41秒」。
「48=48分」。
「9=9時」。
「11=11日」。
「9=10月」。
「107=2007年」。
「4=木曜日」。
「283=経過日数」。
「0=フラグ」。
こんな感じになります。
わかりにくいところがいくつかあったと思うので、順に解説していきます。
「41」、これは秒を表す数値で、0~59の間で変化します。
「48」、これは分を表す数値で、0~59の間で変化します。
「9」、これは時を表す数値で、0~23の間で変化します。
「11」、これは日を表す数値で、1日~その月の月末日までの間で変化します。
「9」、これは月を表す数値ですが、1月が「0」でその後の月もひとつずつずれているので、1を足してやる必要があります。
「107」、これは年を表していますが、1900年が「0」でそこからのカウントなので、これも「1900」を足してやる必要があります。
「4」、これは数値で曜日を表しており、「0」の日曜日から「6」の土曜日まで変化します。
「283」、これは1月1日から今日までの日数(localtime関数で返された日)を表しています。
「0」、これはこの日がサマータイムであるかどうかをフラグ(1か0)で表しており、サマータイム期間中は「1」となります。
このようになるわけです。
これがもっともシンプルなlocaltime関数の使い方です。
でもこれでは戻り値としての仕組みはわかっても、ちょっと使い物にならないですよね。
localtime関数の使い方(その2)
次もわりとシンプルなやり方ですが、もう少し時刻っぽい値にする方法です。
#!/usr/bin/perl
use strict;
my $time = time;
my $localtime = localtime($time);
print "Content-type: text/html\n\n";
print $time, "<br>\n";
print $localtime, "<br>\n";
exit;
前述のPerl/CGIプログラム例と違うところは、localtime関数で返された値を、直接print関数で表示するのではなく、一度スカラ変数「$localtime」に格納しているというところです。
プログラムコードを見る限りは、そんなに違いはありませんよね。
でも実際に動かしてみると、その違いに気づくはずです。
例えば今回も、「$time = time」で、「1192063721」という数値が格納されたとします。
すると、「$localtime = localtime($time)」で格納される値は、「Thu Oct 11 09:48:41 2007」となります。
前述のlocaltime関数を直接print関数で表示させたときとはちがい、月を表すデータに1を足したり、年を表すデータに1900を足す必要はありません。
かなりわかりやすくなりましたね。
時刻の表現方法が、かなり人間向きになりましたよね(笑)。
time関数で同じように秒数を取得していても、localtime関数の使い方によって、表示形式が大きく変化することがわかっていただけたかと思います。
localtime関数の使い方(その3)
最後は、一般的に使われている例をみていきましょう。
#!/usr/bin/perl
use strict;
my $time = time;
my @localtime = localtime($time);
print "Content-type: text/html\n\n";
print $time, "<br>\n";
for (my $i=0;$i<=$#localtime;$i++) {
print $i, " : ", $localtime[$i], "<br>\n";
}
exit;
localtime関数から返される値を格納するには、前述しましたようにスカラ変数で受け取るか、ここでの例のように 配列変数 のようなリスト形式で受け取ります。
localtime関数から返された値を配列変数に格納させると、何年何月何日などの時刻情報の種類別に、配列の各要素に代入されます。
例えば今回も、「$time = time」で、変数「$time」には、「1192063721」という値が格納されたとします。
そのとき「@localtime = localtime($time)」とすると、配列「@localtime」の9つの要素に対して、それぞれ種類別の時刻情報が格納されます。
そして、配列「@localtime」の中身をforループを使ってすべて、「要素番号 : 格納されている値」という形式で表示させています。
変数「$time」が「1192063721」ですから、配列「@localtime」の要素番号とその中身は以下のようになります。
0 : 41
1 : 48
2 : 9
3 : 11
4 : 9
5 : 107
6 : 4
7 : 283
8 : 0
この数字の並びを見て、気づいたかもしれませんね。
実は、この数字の並びというのは、前述のlocaltime関数の戻り値を直接print関数で表示させたときの数値の並びと同じなのです。
あのときは配列「@localtime」に格納されている個々の要素が、すべてひとつにつながって表示されていたのでわかりにくかったのですが…。
今回のように、localtime関数の戻り値を、一度配列変数に格納させると、このように時刻情報の種類別に配列の各要素に代入されるのです。
配列要素「0」は秒を表す数値が代入され、0~59の間で変化します。
配列要素「1」は分を表す数値が代入され、0~59の間で変化します。
配列要素「2」は時を表す数値が代入され、0~23の間で変化します。
配列要素「3」は日を表す数値が代入され、1日~その月の月末日までの間で変化します。
配列要素「4」は月を表す数値が代入されますが、1月が「0」でその後の月もひとつずつずれているので、1を足してやる必要があります。
配列要素「5」は年を表す数値が代入されますが、1900年が「0」でそこからのカウントなので、これも「1900」を足してやる必要があります。
配列要素「6」は曜日を表す数値が代入され、「0」の日曜日から「6」の土曜日まで変化します。
配列要素「7」は1月1日から今日までの日数が代入されます。
配列要素「8」はこの日がサマータイムであるかどうかを判定する数値(0か1でサマータイム期間中であれば「1」)が代入されます。
以上が、配列要素と各時刻情報の対応です。
しかし、これでは要素番号とそれに対応する時間データの種類を常にすべて覚えていなくてはいけませんよね。
それはさすがにきつい(苦笑)。
そんな人は、以下のように書くこともできます。
「my @localtime = localtime($time);」
この部分を…
「my ($second,$minute,$hour,$day,$month,$year,$week, $days, $flag) = localtime($time);」
このように、localtime関数から返される時間データを任意のスカラ変数名で受ければわかりやすいですね。
時刻データを受け取っているのはスカラ変数ですが、受け取り方を見ていただければわかるように、全体をカッコでくくり配列状態にしていますよね。
この場合、スカラ変数の左から順に、配列の要素番号「0」「1」「2…」となります。
まぁ、変数名を見ればわかりますよね。
この場合でも、月を表す「$month」には1を足し、年を表す「$year」には1900を足す必要があるので注意です。
さらに、localtime関数から返される時間データは9種類ですが、いつも9種類必要としているわけではないですよね。
サマータイムであるかどうかなんて、ほとんどの日本人にとってはどうでもいいことですし。
そうなると上記例での変数「$flag」は不要ですよね。
それに基本的に、Perl/CGIプログラムでやらせたい内容によって、必要な時間データというもは変わってくるものですよね。
そこで、localtime関数を呼び出したときに、必要な種類の時間データのみを一緒に指定することにより、プログラムの無駄をなくすことができます。
具体的には、localtime関数から返される時間データの配列番号を使います。
例えば、必要な時間データが、何年何月何日の3種類だったとします。
localtime関数から返される年・月・日の時間データは、要素番号3・4・5なので…。
「my($day,$month,$year) = (localtime($time))[3..5];」
こんな感じになるわけです。
localtime関数と引数全体をカッコでくくり、返される時間データの要素番号を「[」と「]」にはさんで指定しています。
「..」とは、左に指定した数値から右に指定した数値までという意味で、以前学習しましたよね。
最後に、localtime関数で現在の年月日と時分秒を求めておきますね。
#!/usr/bin/perl
use strict;
my ($second,$minute,$hour,$day,$month,$year) = (localtime(time))[0..5];
print "Content-type: text/html\n\n";
print $year+=1900,"年",$month+=1,"月",$day,"日",$hour,"時",$minute,"分",$second,"秒";
exit;
このPerl/CGIプログラムを実行すると、「○年○月○日○時○分○秒」という感じで、現在の時刻が表示されます。
以上が、localtime関数の基本的な使い方でした。
時間計算
ここではlocaltime関数の応用として、過去や未来の時間を計算してみましょう。
localtime関数は、ある基準となる時刻を元に、引数に与えられた秒数を加算し、各種時刻データを作成してくれる関数でしたよね。
現在の時刻を求めたければ、「localtime(time)」とするんでしたよね。
ならばこの性質を利用して…。
現在の秒数を表すtime関数の値を変更し、過去や未来の時間を計算させることにしましょう。
time関数から返される値はすべて「秒」という単位になっています。
したがって例えば、今から1時間前の時刻を求めるには?
60秒×60分である3600秒を、time関数の戻り値から引いてやればよいことになります。
逆に、今から1時間後の時刻を求めるには?
1時間分の秒数である3600秒を、time関数からの戻り値に足してやればよいことになります。
この考え方であれば現在の秒数を元に、何日前・何ヶ月前・何年前から、何日後・何ヶ月後・何年後といった時間を求めることができます。
ただし、過去の時間計算ができるのはtime関数の値が「0」以上のときだけです。
未来の時間計算も、time関数の返す桁以上の値は使えないようになっています。
つまり、確かに過去や未来の時間計算はできますが、それは制限付での話だということを覚えておきましょう。
以上のことを踏まえたうえで、本日から前後25日を計算してみましょう。
#!/usr/bin/perl
use strict;
my $time = time;
my $before = 25;
my $after = 25;
my $beforeTime = $time - ((60*60*24) * $before);
my $afterTime = $time + ((60*60*24) * $after);
print "Content-type: text/html\n\n";
print '本日は', &date($time), "<br>\n";
print $before, '日前は', &date($beforeTime), "<br>\n";
print $after, '日後は', &date($afterTime), "<br>\n";
sub date {
my $time = shift;
my ($day,$month,$year,$week) = (localtime($time))[3..6];
my $date = "";
$month += 1;
$year += 1900;
$date = $year . "年" . $month . "月" . $day . "日(" . ('日','月','火','水','木','金','土')[$week] . ")";
return $date;
}
exit;
このPerl/CGIプログラムを実行すると、本日はもちろん、25日前と後の日付と曜日が表示されます。
まず、1日の秒数は、60秒×60分×24時間で86400秒です。
この1日の秒数86400に日数分の25をかければ25日分の秒数を求めることができます。
ここまできたら後はもう簡単ですね。
time関数からの戻り値に対して、過去の日付を求めたいときは引いて、未来の日付を求めたいときには足せばよいだけです。
そのほかの サブルーチン などの基本的なプログラミング方法に関しては、すでに学習済みですから、忘れてしまった場合は復習しておいてくださいね。
以上がlocaltime関数の応用、過去と未来の時間計算です。
編集後記
今回の編集後記は、もうすこし時刻取得関数について考えてみましょう。
本編で学習しました時刻取得関数ですが、あれはどこから時刻情報を取得していると思いますか?
つまり、ネタ元はどこかということですね。
まぁこれは冷静に考えればすぐに答えが出ることなので、もう書いてしまいますが…。
それは、サーバーのシステム時間のデータを参考にしています。
1台のサーバーを複数の人で利用している場合、これはあまり気にする必要ありません。
なぜなら、サーバー管理者側で正確な時刻を設定済みだからですね。
ただし、専用サーバーを使っている場合は注意しておく必要があります。
特に時刻取得がうまくいかない場合は…。
というのも、サーバーのシステム時間が微妙にずれていることが原因で、今回学習したようなやり方を実践しても、正確な時間を得ることができないからですね。
その場合は当然、サーバーのシステム時間を正しく設定してやれば解決します。
ものすごく当たり前のことなのですが、渦中の人はなかなか気づきにくいことだと思うのでここで書いておきます。
頭の片隅にでも覚えておいてください。
今回の学習は以上です。
ありがとうございました。
<戻る>