RSS 2.0などで使われる日時の解析。
RSS2.0の日時(pubDate、lastBuildDate?)にはRFC#822で定められた形式が使われています。 これは可読性が高いものの時刻値同士の比較などが面倒なので、これを内部時刻値(1990/01/01 00:00:00 GMTからの経過秒数)やW3C形式に変換するために作成しました。
W3C形式の日時の解析と異なり、月やタイムゾーンの語彙が多く数値への変換を都度考えていると厄介なので、これにも触れています。
日時の記述は、以下の正規表現でパースできます。
(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun), )?(\d{1,2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}|\d{2}) (\d{2}):(\d{2})(?::(\d{2}))? (UT|GMT|[ECMP][SD]T|[ZAMNY]|[+-]\d{4})
日時の記述を上の正規表現でマッチングすると、$1から順に曜日、日、月、年、時、分、秒、タイムゾーンが入ります。 以下の様にして年は4桁の数値、月は数値、タイムゾーンは秒数に変換してしまった方が扱いやすいでしょう。
my %monthes = qw(Jan 1 Feb 2 Mar 3 Apr 4 May 5 Jun 6 Jul 7 Aug 8 Sep 9 Oct 10 Nov 11 Dec 12);
my %timezones = (
UT=>'+0000', GMT=>'+0000', EST=>'-0500', EDT=>'-0400', CST=>'-0600', CDT=>'-0500', MST=>'-0700', MDT=>'-0600',
PST=>'-0800', PDT=>'-0700', Z=>'+0000', A=>'-0100', M=>'-1200', N=>'+0100', Y=>'+1200'
);
$year = ($year < 70) ? $year + 2000 : ($year < 1000) ? $year + 1900 : $year;
$month = $monthes{$month};
$timezone = $timezones{timezone} if ($timezone =~ /[A-Z]/);
$timezone = (timezone =~ /([+-])(\d{2})(\d{2})/) ? $1 . $2 * 3600 + $3 * 60 : 0;
こんなマジックかトリックのような正規表現が嫌いであれば、Date::Calcモジュールを使う良いでしょう。曜日は解析不要でしょうし、日付はDecode_Date_EU関数がうまく解析してくれそうです。 後はコロン区切りの時刻と、その後に空白を挟んで続くタイムゾーンを処理すればいいだけです。
RFC#822は(ARPA)インターネット上でのテキストメッセージの標準に関するもので、セクション5で日時の記法を定めています。 これによると、以下の記法を使うことができます。
| 記法 | 記述例 |
| [日] [月] [年] [時]:[分] [タイムゾーン] | 07 Sep 2002 09:42 GMT |
| [日] [月] [年] [時]:[分]:[秒] [タイムゾーン] | 07 Sep 2002 09:42:31 GMT |
| [曜日], [日] [月] [年] [時]:[分] [タイムゾーン] | Sat, 07 Sep 2002 09:42 GMT |
| [曜日], [日] [月] [年] [時]:[分]:[秒] [タイムゾーン] | Sat, 07 Sep 2002 09:42:31 GMT |
各部の書式は次のようになっています。
| 年 | 2桁の正数、ただしRSS 2.0では例外として4桁も許す。 |
| 月 | Jan、Feb、Mar、Apr、May、Jun、Jul、Aug、Sep、Oct、Nov、Decのいずれか。 |
| 日 | 1~2桁の正数。 |
| 曜日 | Mon、Tue、Wed、Thu、Fri、Sat、Sunのいずれか。省略可。 |
| 時、分 | 2桁の正数。 |
| 秒 | 2桁の正数。省略可。 |
| タイムゾーン | +hhmm、-hhmm、またはEST、EDT、CST、CDT、MST、MDT、PST、PDT、Z、A、B、C、D、E、F、G、H、I、K、L、M、N、O、P、Q、R、S、T、U、V、W、X、Yのいずれか。(Jは使わない。) |
タイムゾーンの語彙と実際の時刻オフセットの対応は以下のようになります。
| 語彙 | オフセット |
| UT | (標準時刻) |
| GMT | (標準時刻) |
| EST | -0500 |
| EDT | -0400 |
| CST | -0600 |
| CDT | -0500 |
| MST | -0700 |
| MDT | -0600 |
| PST | -0800 |
| PDT | -0700 |
| Z | (標準時刻) |
| A | -0100 |
| B | -0200 |
| C | -0300 |
| D | -0400 |
| E | -0500 |
| F | -0600 |
| G | -0700 |
| H | -0800 |
| I | -0900 |
| K | -1000 |
| L | -1100 |
| M | -1200 |
| N | +0100 |
| O | +0200 |
| P | +0300 |
| Q | +0400 |
| R | +0500 |
| S | +0600 |
| T | +0700 |
| U | +0800 |
| V | +0900 |
| W | +1000 |
| X | +1100 |
| Y | +1200 |
上の正規表現は、以下の様にして、端っこから少しずつ作成しました。 年の記述はRFC#822の2桁の数値の他、RSS 2.0で許される4桁の数値にもマッチするものを選びました。
my $year = '\d{4}|\d{2}'; # RSS 2.0に合わせる
my $month = 'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec';
my $day = '\d{1,2}';
my $date = "($day) ($month) ($year)";
my $time = '(\d{2}):(\d{2})(?::(\d{2}))?';
my $tz = 'UT|GMT|[ECMP][SD]T|[ZABCDEFGHIKLMNOPQRSTUVWXY]|[+-]\d{4}';
my $wday = 'Mon|Tue|Wed|Thu|Fri|Sat|Sun';
my $pattern = "(?:($wday), )?$date $time ($tz)";
print $pattern;
スクリプト中でこの正規表現を使う時には、上のサンプルスクリプトのように直接正規表現を記述してしまう代わりに、この1ブロックを(print行抜きで)埋め込むのでも、もちろん構いません。
各記法の日時をパースします。
my $pattern = '(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun), )?(\d{1,2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}|\d{2}) (\d{2}):(\d{2})(?::(\d{2}))? (UT|GMT|[ECMP][SD]T|[ZAMNY]|[+-]\d{4})';
my %monthes = qw(Jan 1 Feb 2 Mar 3 Apr 4 May 5 Jun 6 Jul 7 Aug 8 Sep 9 Oct 10 Nov 11 Dec 12);
my %timezones = (
UT=>'+0000', GMT=>'+0000', EST=>'-0500', EDT=>'-0400', CST=>'-0600', CDT=>'-0500', MST=>'-0700', MDT=>'-0600',
PST=>'-0800', PDT=>'-0700', Z=>'+0000', A=>'-0100', M=>'-1200', N=>'+0100', Y=>'+1200'
);
my @dates = (
'07 Sep 2002 09:42 EST',
'07 Sep 2002 09:42:31 EST',
'Sat, 07 Sep 2002 09:42 EST',
'Sat, 07 Sep 2002 09:42:31 EST',
);
foreach my $date (@dates) {
$date =~ /$pattern/;
my ($wday, $day, $month, $year, $hour, $min, $sec, $timezone) = ($1, $2, $3, $4, $5, $6, $7, $8);
$year = ($year < 70) ? $year + 2000 : ($year < 1000) ? $year + 1900 : $year;
$month = $monthes{$month};
$timezone = ($timezone =~ /[A-Z]/) ? $timezones{$timezone} : $timezone;
$timezone = ($timezone =~ /([+-])(\d{2})(\d{2})/) ? $1 . $2 * 3600 + $3 * 60 : 0;
printf("%-30s -> %04d/%02d/%02d(%-3s) %02d:%02d:%02d(%s)\n", $date, $year, $month, $day, $wday, $hour, $min, $sec, $timezone);
}
[[#rcomment]]