[Perlメモ] HTTPアプリケーションで用いる日付フォーマットで出力するサンプルスクリプト (2009-05-05)

HTTPヘッダのLast-Modified エンティティヘッダフィールドなどで用いられる、
"Tue, 05 May 2009 14:24:27 GMT"
のようなフォーマットで日付/時刻を出力するサブルーチンです。

このフォーマットの詳細については、[Studying HTTP] HTTP Header FieldsのDateに関する記述を参考にしました。

 1: # RFC 1123の形式で出力
 2: sub DateTime_byRFC1123
 3: {
 4:     my $ctime = $_[0] ? $_[0] : time();
 5:     my($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($ctime);
 6:     $wday = qw(Sun Mon Tue Wed Thu Fri Sat)[$wday];
 7:     $mon  = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)[$mon];
 8:     return sprintf("%s\, %02d %s %04d %02d:%02d:%02d GMT",
 9:                    $wday, $mday, $mon, $year+1900, $hour, $min, $sec);
10: }

time()関数が返すような、1970年1月1日UTCからの(うるう秒を除いた)秒数を、引数として受け取ります。引数がなければ現在時刻を受け取ったものとして処理します。

使用例

上述のサブルーチンを使ってCGIでHTML文書を出力するの例です。

print "Content-type: text/html;charset=utf-8", "\n";
print "Content-length: ". length($Output), "\n";
print "Last-Modified: ". DateTime_byRFC1123( ( stat("./somefile.html") )[9] ), "\n";
print "\n";
print $Output;

簡略化のためこのスクリプトでは、変数$Outputの内容は、テキストファイル "./somefile.html" のHTML文書そのものとしています。

HTTPヘッダに "Last-Modified" を付加することで、クライアントは受け取った文書の最終更新日時も保持します。次にアクセスするとき、自身のキャッシュを呼び出すかまたは再度サーバへリクエストするかを、日時を元に判断します。クライアントが自身のキャッシュを利用してもらうことでサーバの負荷を軽減することができます。

また、余談ですが、 "Content-length" を付加することで、クライアントはこれから受け取る文書のサイズを事前に知ることができます。それによって、IEなどのクライアントソフトウェアは、"何%読み込み済み"のようなステータス情報を表記することが可能になります。大きなファイルを受け取る場合や回線の遅い環境での利用者にとっては、読み込みを待っている間のストレス軽減となります。

サンプルスクリプトの解説

上述のサブルーチンについて、できる限り丁寧に解説していきます。

4行目: my $ctime = $_[0] ? $_[0] : time();

この行では、変数 $ctime の宣言と初期化を同時に行なっています。

$_[0] は、顔文字ではありません。特殊変数と呼ばれる変数の一つです。Perlでは、サブルーチンへの引数は配列 @_ によって渡されます。その配列の先頭という意味で添え字 0 を与えて、 $_[0] と記述しています。つまり、呼び出し元で &sub( 123 ); と記述した時の "123" が $_[0] の値となります。

my は、変数宣言で用いられる関数です。my $ctime と記述することで、変数 $ctime はレキシカル変数と呼ばれるこのサブルーチン内のみで有効な変数となります。仮にサブルーチンの外で $ctime というグローバル変数があっても、このサブルーチンで用いられている$ctimeとは無関係です(別のメモリ領域が確保されています)。また、このサブルーチンを抜けたときに $ctime が保持していたメモリ領域は解放されるはずです。

TEST_EXPR ? IF_TRUE_EXPR : IF_FALSE_EXPR は、条件演算子と呼ばれる演算子を用いた記述方法です。if-then-else構文の短縮形として用いられることが多いです。$_[0] ? $_[0] : time() は、" $_[0] に値があればそれを、無ければ time() の実行結果を" という意味になります。

5行目: my($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($ctime);

日時をグリニッジ標準時(GMT)で表現するため、gmtime() 関数を用います。gmtime() 関数は、time()関数が返す形式の時刻を受け取り、9要素のリスト値を返します。返されるリストの要素はCプログラムの構造体 struct tm に返された値そのものだそうです。この記述のようにPerlでは、リストコンテキストを関数の左辺に直接記述すれば代入がなされます。

また、 my 関数は、このようにリストコンテキストに対して記述することが可能です。

6行目: $wday = qw(Sun Mon Tue Wed Thu Fri Sat)[$wday];

"0~6" の曜日を示す数値を "Wed" のような文字列に変換しています。

qw( .. ) 構文は、('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri, 'Sat') という記述と同じです。qw構文を用いた方が、タイプミスも少なく可読性も良いと、個人的には、考えており好んで使っています。

また、リストコンテキストは、直接後ろに添え字をつけることで配列と同じように取り扱うことができます。この場合、リスト中 $wday 番目の要素を取り出し代入しています。

スカラー変数 $wday は、元々は整数が格納されていたはずなのに、この行で文字列を代入してしまいました。Perlでは変数にどんなデータ型でも代入できます。とはいえ、混乱を招くかも知れません。変数の内容が整数から文字列へというまったく違うデータ型に変換してしまうのは、私自身も好ましくないと考えています。代入する文字列の格納先は例えば、 $wday_byString のように別名の変数を新たに宣言するのが教科書的かも知れません。ただし、このサブルーチン以外の記述もあることを考えると、取り扱う変数は極力少なくしたいという考えもあります。ということで、このサンプルのように "わずか数行で完結するサブルーチンだったら型の違うデータの代入も許す" という個人的記述ルールを設けました。要するに「見りゃわかんだろ」ということです。

7行目: $mon = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)[$mon];

"0~11" の月を示す数値を "May" のような文字列に変換しています。テクニック的には6行目と同じです。

8~9行目: return sprintf("%s\, %02d %s %04d %02d:%02d:%02d GMT",
$wday, $mday, $mon, $year+1900, $hour, $min, $sec);

return で目的の文字列を戻り値としてセットし、サブルーチンを抜けます。

sprintf はフォーマット付き文字列を返す関数です。第1引数のダブルクォートで囲まれた文字列が指定するフォーマットを示しています。"%02d" のように"%" から始まる文字列(フィールド指定子と呼ばれます)が第2引数以降のリストの要素1つ1つに置き換わります。"%s" は文字列を指定し、 "%02d" は2桁の数値文字列を指定します。

Written in 2009-05-05, Permalink, Comments(1)
この記事が属しているカテゴリ: OTHER, PERL

Perlメモ - "sort { $b cmp $a; }"と"reverse"関数の違い (2007-07-01)

本サイトで用いているPerlスクリプトにバグがあり、調べたところリストのソート方法に誤りがあったことがわかりました。初歩的なミスとは思いますが、自分自身のメモであると同時に、同じ勘違いをしてしまう人もいるかと考え掲載します。

内容

本サイトのトップページでは、新しい記事ほど先頭へ表示するよう処理させているつもりでした。しかし、過去の記事のいくつかを更新したら、なぜか順番が崩れてしまいました。

各々の記事は日付を元にしたファイル名で定義されているので、readdir関数で読み込んだファイルを逆ソートして処理すれば良いと考え、以下のように記述していました。


    foreach( grep !/^\./, reverse readdir(DIR) ){   # 誤り!
        some_action();
    }

これでは期待通りに動きません。

readdir関数は、opendir関数によってオープンしたディレクトリハンドルから、単に、ディレクトリエントリを読むだけで、得られるリスト値は昇順で並んでいることなど保証していません(この順序はOSが決めているんでしょうか?)。

一方、reverse関数は、中身の値を見てそれを逆ソートしてくれるわけではありません。単にリストの要素を逆順に並べ直す、つまり、ポインタを入れ換えるだけです。ここを私は勘違いしていました。

目的の動作をさせるには、結局、sort関数を使います。sort関数にBLOCKまたはSUBNAMEを省略すると、リスト値を昇順にソートしますが、sort { $b cmp $a; } @listとすれば逆順にソートされます。


    foreach( grep !/^\./, sort { $b cmp $a; } readdir(DIR) ){
        some_action();
    }

grep関数とsort関数の記述順も気になりますが、とりあえずはこのままで、と...。

参考資料:プログラミングPerl〈VOLUME1〉 (オライリー・ジャパン)

正確には、私が持っているのはPerl5向けの第2版です。1冊5,000円もするのにこの第3版ではVOLUME1とVOLUME2の2冊になってしまいました。合わせて1万。今さら買う気しないなぁ(笑)。

上記、逆ソートについて、sort { $b cmp $a; } @listだけでなく、reverse sort @listでもまったく等価であると書かれていました。後者の方が効率が悪そうですが、まぁ、開発者本人がまったく等価と言っているのでそうなのでしょう。

Written in 2007-07-01, Permalink, Comments(3)
この記事が属しているカテゴリ: OTHER, PERL