🎯 最終演習

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; }壊れた行を数えて飛ばしますunlessif 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は文字列の比較です。この使い分けを間違えると、静かに壊れます——109cmpで比べると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つしか入りません。12345671234,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_FROM500に変えてみてください。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で比べると109より小さいと判定されます。

順番を逆にすれば降順です。$b <=> $aと書けば大きい順になります。

-> はいつ必要ですか

リファレンスの中身を取り出すときです。

ステップ2のparse_lineハッシュのリファレンスを返しています。だから中身を見るには$rec->{path}と矢印が要ります。

ハッシュそのものなら$hash{path}——矢印は要りません。

なぜリファレンスで返すのか。Perlのサブルーチンは、ハッシュをそのまま返すと平らな並びに崩れてしまいます。リファレンスなら形を保ったまま渡せるので、構造のあるデータを返すときの定石になっています。

🚩 次はこのカテゴリへ

同じログ集計をシェルのコマンドだけで書くこともできます。grepで絞り、awkで切り出し、sortuniq -cで数える——1行で書けてしまう場面も多いのです。Linuxカテゴリの最終演習でその流れを練習すると、どこまでをコマンドで済ませ、どこからPerlを書くかの判断がつくようになります。

Linux/シェルのレッスンへ進む
広告スペース(バナー)
広告スペース(記事内)