Perlでアクセスログから集計レポートを作ってみよう(総合演習)
ここまでの Perl レッスンで学んだことを全部つないで、動くものを1つ完成させます。新しい構文は出てきません。スカラー変数・配列・ハッシュ・正規表現・サブルーチン・リファレンス——すでに練習したものだけで組み立てます。
作るのはアクセスログの集計です。1行ずつ切り分け、パス別・ステータス別・時間帯別に数え、レポートにまとめます。
これはPerlが最も得意とする仕事です。テキストを読んで、正規表現で切り分けて、ハッシュで数える——Perlはこのために生まれたような言語で、いまも現役で使われているのはこの強さがあるからです。
このページではコードを実行していません。Perlはブラウザの中では走らせられないためです。かわりにお手本と同じコードを書けたかを、その場で判定します(判定はあなたのブラウザの中だけで行われ、どこにも送信されません)。
表示している実行結果は本物です。このページを作るときに、実際のPerlで走らせて得た出力をそのまま載せています。
完成イメージ
最後まで進むと、このコードが完成します。下の実行結果は、このページを作るときに開発機で実際にコードを走らせて得たものです(このページ上では実行していません)。
===== アクセスログ集計 ===== 読み込み: 10行(解析できない行 0) 転送量計: 30,176 バイト --- パス別(多い順) --- /index.html 3回 15,360 バイト /api/items 2回 8,288 バイト /api/login 2回 952 バイト /about.html 1回 3,400 バイト /missing.png 1回 128 バイト /style.css 1回 2,048 バイト --- ステータス別 --- 200 7回 401 1回 エラー 404 1回 エラー 500 1回 エラー --- 時間帯別 --- 09時 ### 10時 ### 11時 ## 12時 ## -------------------------------------------- エラー応答: 3回(全体の 30.0%) エラーの内容を確認してください。
STEP 1__DATA__からログを読み込む
🎯 このステップのゴール: strictを効かせ、1行ずつ読んで配列に貯める
まずログを読み込みます。
use strict; use warnings;——この2行は必ず書いてください。Perlを書くうえでいちばん大事な習慣です。
strictは変数の宣言を強制します。これが無いと、$countと書くつもりで$conutと打ち間違えてもそのまま通ります。新しい変数が勝手に作られて、いつまでも0のまま——原因を探すのに何時間もかかる種類の不具合です。
warningsは怪しい書き方を教えてくれます。未定義の値を計算に使った、といったことを警告します。
<DATA>はスクリプトの末尾にある__DATA__より下を読むという書き方です。Perl特有の便利な仕組みで、小さなデータをスクリプトに埋め込むときに使います。
ファイルから読むときも書き方はほとんど同じです。openしてファイルハンドルを作れば、while (my $line = <$fh>)と同じ形になります。
chomp $line;で末尾の改行を取り除きます。これを忘れると、比較も表示もずれます——「なぜか一致しない」の原因の定番です。
next if $line =~ /^\s*$/;は「空行なら飛ばす」という意味です。next if 条件という後置の書き方がPerlらしいところで、「飛ばす、もし空行なら」と英語の語順で読めます。
push @lines, $line;で配列の末尾に足していきます。配列は@、その中の1つは$——この記号の使い分けがPerlの基本です。
読み込んだ行数: 10 1行目: 192.168.1.10 - - [18/Aug/2026:09:12:03] "GET /index.html" 200 5120 最後の行: 192.168.1.10 - - [18/Aug/2026:12:44:16] "GET /index.html" 200 5120
STEP 2正規表現で1行を切り分ける
🎯 このステップのゴール: 括弧で捕まえた部分を$1〜$6で取り出す
ログ1行を、意味のある部分に分けます。ここがPerlの本領です。
$line =~ m{^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+)" (\d+) (\d+)$}
長く見えますが、読み方は順番に追うだけです。
(\S+)が「空白でない文字の並び」——IPアドレスを捕まえます。括弧で囲むと、その部分が$1に入ります。以降、括弧の順に$2、$3……と続きます。
\[([^\]]+)\]が時刻の部分です。[は正規表現では特別な意味を持つので\[と打ち消します。中の[^\]]+は「]以外の文字の並び」——閉じ括弧まで貪欲に読まないようにする定番の書き方です。
m{...}と波括弧を使っているのに注目してください。/.../でも書けますが、パターンの中に/が出てくると、いちいち\/と打ち消すことになります。区切り文字は自由に選べるので、読みやすいものを選ぶのがPerlの作法です。
^と$で行の先頭と末尾を固定しています。これが無いと行の一部にたまたま合ってしまうことがあります。「1行まるごとがこの形か」を確かめたいなら、必ず両端を固定してください。
返しているのはハッシュのリファレンス——{ ip => $1, ... }です。波括弧で作ると、中身そのものではなく「参照」が返ります。
Perlのサブルーチンはハッシュをそのまま返せません(平らな並びに崩れてしまいます)。だからリファレンスで返すのが定石です。
形が合わない行にはundefを返します。ログには壊れた行が混ざるもの——落ちずに数えられるようにしておきます。
IP: 192.168.1.10 時刻: 18/Aug/2026:09:12:03 メソッド: GET パス: /index.html ステータス: 200 バイト数: 5120 壊れた行: undef
STEP 3ハッシュで数え上げる
🎯 このステップのゴール: 存在しないキーに直接足せることを使う
パスごとのアクセス回数を数えます。使うのはハッシュです。
$by_path{ $rec->{path} }++;——これだけで数えられます。
Perlのハッシュは、まだ無いキーに++してもエラーになりません。undefが0として扱われるので、初回でも0 + 1で正しく1になります。
「キーが無ければ0を入れてから足す」という前処理が要りません。他の多くの言語では2〜4行かかるところが、1行で済みます。Perlが集計に強い理由の1つです。
宣言はmy (%by_path, %by_status, %by_hour);とまとめて書けます。ハッシュは%、その中の1つは$——配列と同じ考え方です。
この記号の切り替わりがPerlの独特なところです。%by_pathというハッシュの中身を取るときは$by_path{...}。「取り出すものが1つならドル記号」と覚えてください。
バイト数の合計は+=です。こちらも初回はundef + 5120になり、正しく5120になります。
$rec->{path}の->はリファレンスの中身を取る記号です。ステップ2でハッシュのリファレンスを返したので、中身を見るには矢印が要ります。
unless (defined $rec) { $bad++; next; }で壊れた行を数えて飛ばします。unlessはif not——Perlらしい書き方で、否定の条件が素直に読めます。
壊れた行を黙って捨てないでください。何件あったかを出せば、ログの取り方に問題があることに気づけます。
解析できない行: 0 --- パス別 --- /about.html: 1回 /api/items: 2回 /api/login: 2回 /index.html: 3回 /missing.png: 1回 /style.css: 1回 --- ステータス別 --- 200: 7回 401: 1回 404: 1回 500: 1回
STEP 4sortに比べ方を渡して並べ替える
🎯 このステップのゴール: 回数の多い順、同数なら名前順にする
パス別の集計をアクセスの多い順に並べます。
sort { $by_path{$b} <=> $by_path{$a} || $a cmp $b } keys %by_path
波括弧の中が「比べ方」です。$aと$bという特別な変数に、比べる2つが入ってきます。
<=>は数値の比較、cmpは文字列の比較です。この使い分けを間違えると、静かに壊れます——10と9をcmpで比べると10のほうが小さいと判定されます(文字として先頭から比べるため)。
数を数字として並べたいなら必ず<=>です。
$by_path{$b} <=> $by_path{$a}と$bを先に書いているのは降順にするためです。$aを先にすれば昇順になります。順番を入れ替えるだけで向きが変わります。
||のあとが「同点のときの決め方」です。$a cmp $b——回数が同じなら名前順にします。
この一手間を省かないでください。同点の並び順を決めていないと、実行するたびに順番が変わることがあります。手元では正しく見えたのに本番で並びが違う——見つけにくい不具合の典型です。
<=>は同じなら0を返し、0は偽なので||の右側が使われます。この仕組みで「第1キー、第2キー」の並べ替えが1行で書けます。
keys %by_pathでキーの一覧が取れます。ハッシュは順番を持たないので、並べ替えずに回すと順序はばらばらです。
--- 多い順(同数なら名前順) --- /index.html 3回 /api/items 2回 /api/login 2回 /about.html 1回 /missing.png 1回 /style.css 1回 数値として: 9 10 20 100 文字として: 10 100 20 9
STEP 5置換で3桁ごとにカンマを入れる
🎯 このステップのゴール: s///を繰り返し当てて、後方参照で組み直す
転送量を1,234,567のように読みやすくします。
1 while $s =~ s/^(\d+)(\d{3})/$1,$2/;
Perlらしさが凝縮された1行です。順に読み解きます。
s/探すもの/置き換えるもの/が置換です。^(\d+)(\d{3})は「行頭から、数字の並びのあとに数字3つ」——後ろから3桁を切り出しています。
$1と$2が捕まえた部分です。置換後の$1,$2でそのあいだにカンマを挟みます。
(\d+)が貪欲に読むのが要点です。できるだけ長く取ろうとするので、残り3桁だけを(\d{3})に残します。だからいちばん右のカンマから順に入っていきます。
1 while ...は「もう置き換えられなくなるまで繰り返す」という書き方です。whileの本体が1——何もしません。置換そのものが条件のところで行われ、成功するかぎり回り続けます。
1回の置換ではカンマが1つしか入りません。1234567は1234,567になるだけ。もう一度当てて1,234,567になります。だから繰り返しが要ります。
この書き方は暗号めいて見えますが、Perlでは定番として広く使われています。読めるようになっておくと、人のコードで必ず出会います。
my $s = "$n";と文字列に変換してから置換しています。Perlは数値と文字列を自動で行き来しますが、置換は文字列に対する操作——意図をはっきりさせるために明示しています。
5120 -> 5,120
35276 -> 35,276
1234567 -> 1,234,567
999 -> 999
1000 -> 1,000
1回だけ: 1234,567
STEP 6ひとつのサブルーチンにまとめて仕上げる
🎯 このステップのゴール: reportにまとめ、時間帯の棒グラフまで出す
最後の仕上げです。ここまでの部品をreportという1つのサブルーチンにまとめます。
my @lines = @_;——@_が引数の入った配列です。Perlのサブルーチンには引数の宣言がありません。渡されたものが全部@_に平らに並びます。
だから最初の行で名前の付いた変数に受け直すのが作法です。そうしないと、あとから読む人に「この関数は何を受け取るのか」が分かりません。
時間帯の抽出にhour_ofを使っています。return $1 if $time =~ /:(\d{2}):\d{2}:\d{2}$/;——末尾から「時:分:秒」を探して、時だけを返します。
ここでも$で末尾を固定しているのが大事です。日付にも:が含まれるので、固定しないと違う数字を拾います。
棒グラフは"#" x $by_hour{$hour}です。xが文字列の繰り返し演算子——"#" x 3で###になります。Perlでは掛け算の記号ではなくxを使います。
区切り線も"-" x 44で引けます。同じ演算子が2か所で効いています。
printfで桁を揃えます。%-16sが左揃え、%2dが右揃えの2桁、%8sが右揃えの8文字。%%でパーセント記号そのものを出します。
完成です。$ERROR_FROMを500に変えてみてください。401と404がエラーに数えられなくなり、締めの一言も変わります。
__DATA__に行を足すのも良い確認になります。わざと形の違う行を1つ混ぜてみてください——「解析できない行 1」と出て、落ちずに数えられることが確かめられます。
===== アクセスログ集計 ===== 読み込み: 10行(解析できない行 0) 転送量計: 30,176 バイト --- パス別(多い順) --- /index.html 3回 15,360 バイト /api/items 2回 8,288 バイト /api/login 2回 952 バイト /about.html 1回 3,400 バイト /missing.png 1回 128 バイト /style.css 1回 2,048 バイト --- ステータス別 --- 200 7回 401 1回 エラー 404 1回 エラー 500 1回 エラー --- 時間帯別 --- 09時 ### 10時 ### 11時 ## 12時 ## -------------------------------------------- エラー応答: 3回(全体の 30.0%) エラーの内容を確認してください。
よくある質問
このページではコードを実行しないのですか?
していません。Perlはブラウザの中では走らせられないためです。このページではお手本と同じコードを書けたかどうかを判定する形にしています(判定はあなたのブラウザの中だけで行われ、コードはどこにも送信されません)。
ただし表示している実行結果は本物です。このページを作るときに実際のPerlで走らせた出力を、そのまま載せています。
use strict は本当に必要ですか
必ず書いてください。Perlを書くうえで、いちばん効く1行です。
これが無いと変数の打ち間違いが黙って通ります。$countのつもりで$conutと打つと、新しい変数が勝手に作られ、いつまでも0のままになります。エラーも警告も出ません。
use warnings;も一緒に書いてください。未定義の値を計算に使った、といった怪しい書き方を教えてくれます。
古いコードでは省かれていることがあります。書き足すと大量の警告が出ますが、その警告こそが直すべき場所です。
$a と $b はどこから来るのですか
sortが用意する特別な変数です。自分で宣言する必要はありませんし、use strictがあっても怒られません。
比べ方のブロックの中で、比較する2つの要素が$aと$bに入ってきます。
数値なら<=>、文字列ならcmpで比べます。間違えると静かに壊れます——数をcmpで比べると10が9より小さいと判定されます。
順番を逆にすれば降順です。$b <=> $aと書けば大きい順になります。
-> はいつ必要ですか
リファレンスの中身を取り出すときです。
ステップ2のparse_lineはハッシュのリファレンスを返しています。だから中身を見るには$rec->{path}と矢印が要ります。
ハッシュそのものなら$hash{path}——矢印は要りません。
なぜリファレンスで返すのか。Perlのサブルーチンは、ハッシュをそのまま返すと平らな並びに崩れてしまいます。リファレンスなら形を保ったまま渡せるので、構造のあるデータを返すときの定石になっています。
🚩 次はこのカテゴリへ
同じログ集計をシェルのコマンドだけで書くこともできます。grepで絞り、awkで切り出し、sortとuniq -cで数える——1行で書けてしまう場面も多いのです。Linuxカテゴリの最終演習でその流れを練習すると、どこまでをコマンドで済ませ、どこからPerlを書くかの判断がつくようになります。