@nqounetです。
前回はファイルを1行ずつ読み込む LogReader クラスを作成しました。
今回は、読み込んだログの文字列を「ハッシュリファレンス」に変換して、プログラムで扱いやすくします。いわゆる「パース(構文解析)」処理の実装です。
前回の振り返り
前回作成した LogReader は、単に access.log の中身を1行ずつ文字列として返していました。
1
| 127.0.0.1 - - [19/Jan/2026:21:15:21 +0900] "GET /index.html HTTP/1.1" 200 1234
|
これだと、「ステータスコードが200以外の行だけ知りたい」といった処理を書くのが大変です。
今回のゴール:LogParserクラス
LogReader を継承して、next_line メソッドではなく next_log メソッドを提供する LogParser クラスを作ります。
戻り値は以下のようなハッシュリファレンスにします。
1
2
3
4
5
6
7
8
| {
ip => '127.0.0.1',
datetime => '19/Jan/2026:21:15:21 +0900',
method => 'GET',
path => '/index.html',
status => '200',
size => '1234',
}
|
ApacheやNginxの標準的なログ形式(Combined Log Format)をパースするための正規表現を定義します。
Perl 5.10から使える「名前付きキャプチャ(Named Captures)」を使うと、正規表現の結果をそのままハッシュとして取り出せるので非常に便利です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| package LogParser;
use Moo;
use strict;
use warnings;
use experimental qw(signatures);
use namespace::clean;
# LogReaderを継承する
extends 'LogReader';
# Combined Log Formatの正規表現
# 名前付きキャプチャ (?<name>...) を使用
my $LOG_REGEX = qr{^
(?<ip>[\d\.]+) # IPアドレス
\s-\s-\s
\[(?<datetime>[^\]]+)\] # 日時
\s"
(?<method>[A-Z]+)\s # メソッド
(?<path>[^\s]+)\s # パス
[^"]+"
\s
(?<status>\d+) # ステータスコード
\s
(?<size>\d+|-) # サイズ
}x;
# LogReaderのnext_lineを使って1行読み、パースして返す
sub next_log ($self) {
while (defined(my $line = $self->next_line)) {
if ($line =~ $LOG_REGEX) {
# 名前付きキャプチャの結果(%+)をハッシュリファレンスのコピーとして返す
return { %+ };
}
# マッチしない行(空行や壊れたログ)はスキップして次へ
warn "Skipped invalid line: $line\n";
}
return undef;
}
1;
|
ポイント解説
extends 'LogReader': LogReader の機能をすべて受け継ぎます(継承)。これにより、filename アトリビュートや file_handle の管理機能を再実装する必要がなくなります。qr{ ... }x: x オプションを付けることで、正規表現の中にコメントや空白を書けるようになります。複雑な正規表現には必須ですね。(?<name>...): 名前付きキャプチャです。マッチした内容は特殊変数 %+ に入ります。例えば (?<ip>...) にマッチした内容は $+{ip} でアクセスできます。return { %+ }: %+ ハッシュを無名ハッシュリファレンス { ... } にコピーして返しています。
コード例2: パース結果を表示する
1
2
3
4
5
6
7
8
9
10
11
12
| use strict;
use warnings;
use lib '.';
use LogParser;
use Data::Dumper;
my $parser = LogParser->new(filename => 'access.log');
while (defined(my $log = $parser->next_log)) {
# 構造化されたデータとして扱える!
print "IP: $log->{ip}, Path: $log->{path}, Status: $log->{status}\n";
}
|
出力例:
1
| IP: 127.0.0.1, Path: /index.html, Status: 200
|
継承の力
LogParser は LogReader を継承しているので、ファイルを開く処理や閉じる処理は何も書いていませんが、ちゃんと動きます。これが継承のメリットです。
次回予告
次回は、この LogParser をさらに拡張して、「特定のIPアドレスからのアクセスだけを抽出する機能」を追加してみます。
しかし、そこで私たちは「継承の落とし穴」に遭遇することになります…。
第3回: IPアドレスでフィルタリングしよう