CODE Polyglot / よく使う書き方 / 05

アクセスログを集計する

Web サーバーのアクセスログを標準入力から読み、ステータスごとの件数と、応答の遅いリクエストの上位3件を出します。空白で区切って何番目かを取る書き方は手軽ですが、壊れた行を数えたり、応答秒を文字のまま並べたりします。

画面にこう出れば正解

どの言語で書いても、同じ入力なら画面に出る文字は1文字も違わない、というのが正解の決め方です(例は Python で動かしたときのコマンドです)。

基本

input.txt の中身

203.0.113.5 - - [30/Oct/2026:09:00:01 +0900] "GET /index.html HTTP/1.1" 200 5120 0.012
203.0.113.7 - - [30/Oct/2026:09:00:02 +0900] "GET /api/search?q=%E5%A4%A9%E6%B0%97 HTTP/1.1" 200 812 0.950
198.51.100.2 - - [30/Oct/2026:09:00:03 +0900] "POST /api/login HTTP/1.1" 401 64 0.034
203.0.113.5 - - [30/Oct/2026:09:00:04 +0900] "GET /favicon.ico HTTP/1.1" 404 - 0.001
192.0.2.10 - - [30/Oct/2026:09:00:05 +0900] "GET /api/report HTTP/2.0" 500 128 2.103
203.0.113.9 - - [30/Oct/2026:09:00:06 +0900] "GET /index.html HTTP/1.1" 200 5120 0.010
198.51.100.2 - - [30/Oct/2026:09:00:07 +0900] "POST /api/login HTTP/1.1" 200 256 0.120
$ python3 access_log.py < input.txt
件数 7
ステータス
200 4
401 1
404 1
500 1
遅い順
2.103 GET /api/report
0.950 GET /api/search?q=%E5%A4%A9%E6%B0%97
0.120 POST /api/login

読めない行と空の行が混ざる

input.txt の中身

203.0.113.5 - - [30/Oct/2026:09:00:01 +0900] "GET /index.html HTTP/1.1" 200 5120 0.012
203.0.113.7 - - [30/Oct/2026:09:00:02 +0900] "GET /api/search?q=%E5%A4%A9%E6%B0%97 HTTP/1.1" 200 812 0.950
198.51.100.2 - - [30/Oct/2026:09:00:03 +0900] "POST /api/login HTTP/1.1" 401 64 0.034
203.0.113.5 - - [30/Oct/2026:09:00:04 +0900] "GET /favicon.ico HTTP/1.1" 404 - 0.001
192.0.2.10 - - [30/Oct/2026:09:00:05 +0900] "GET /api/report HTTP/2.0" 500 128 2.103
203.0.113.9 - - [30/Oct/2026:09:00:06 +0900] "GET /index.html HTTP/1.1" 200 5120 0.010
198.51.100.2 - - [30/Oct/2026:09:00:07 +0900] "POST /api/login HTTP/1.1" 200 256 0.120

これはログではありません
192.0.2.1 - - [30/Oct/2026:09:00:08 +0900] "GET / HTTP/1.1" 2OO 1 0.1
$ python3 access_log.py < input.txt
件数 7
読めない行 2
ステータス
200 4
401 1
404 1
500 1
遅い順
2.103 GET /api/report
0.950 GET /api/search?q=%E5%A4%A9%E6%B0%97
0.120 POST /api/login

応答秒の桁数が違う(文字として比べると 10.5 が 9.8 より小さくなる)

input.txt の中身

192.0.2.1 - - [30/Oct/2026:09:01:00 +0900] "GET /a HTTP/1.1" 200 1 9.8
192.0.2.1 - - [30/Oct/2026:09:01:01 +0900] "GET /b HTTP/1.1" 200 1 10.5
192.0.2.1 - - [30/Oct/2026:09:01:02 +0900] "GET /c HTTP/1.1" 200 1 0.75
192.0.2.1 - - [30/Oct/2026:09:01:03 +0900] "GET /d HTTP/1.1" 200 1 1.25
$ python3 access_log.py < input.txt
件数 4
ステータス
200 4
遅い順
10.5 GET /b
9.8 GET /a
1.25 GET /d

同じ応答秒は先に出た順

input.txt の中身

192.0.2.1 - - [30/Oct/2026:09:02:00 +0900] "GET /first HTTP/1.1" 200 1 0.500
192.0.2.1 - - [30/Oct/2026:09:02:01 +0900] "GET /second HTTP/1.1" 304 - 0.500
192.0.2.1 - - [30/Oct/2026:09:02:02 +0900] "GET /third HTTP/1.1" 200 1 0.500
192.0.2.1 - - [30/Oct/2026:09:02:03 +0900] "GET /fourth HTTP/1.1" 200 1 0.500
$ python3 access_log.py < input.txt
件数 4
ステータス
200 3
304 1
遅い順
0.500 GET /first
0.500 GET /second
0.500 GET /third

改行が \r\n(Windows で保存したログ)

input.txt の中身

203.0.113.5 - - [30/Oct/2026:09:00:01 +0900] "GET /index.html HTTP/1.1" 200 5120 0.012[CR]
203.0.113.7 - - [30/Oct/2026:09:00:02 +0900] "GET /api/search?q=%E5%A4%A9%E6%B0%97 HTTP/1.1" 200 812 0.950[CR]
198.51.100.2 - - [30/Oct/2026:09:00:03 +0900] "POST /api/login HTTP/1.1" 401 64 0.034[CR]
203.0.113.5 - - [30/Oct/2026:09:00:04 +0900] "GET /favicon.ico HTTP/1.1" 404 - 0.001[CR]
192.0.2.10 - - [30/Oct/2026:09:00:05 +0900] "GET /api/report HTTP/2.0" 500 128 2.103[CR]
203.0.113.9 - - [30/Oct/2026:09:00:06 +0900] "GET /index.html HTTP/1.1" 200 5120 0.010[CR]
198.51.100.2 - - [30/Oct/2026:09:00:07 +0900] "POST /api/login HTTP/1.1" 200 256 0.120[CR]
$ python3 access_log.py < input.txt
件数 7
ステータス
200 4
401 1
404 1
500 1
遅い順
2.103 GET /api/report
0.950 GET /api/search?q=%E5%A4%A9%E6%B0%97
0.120 POST /api/login

3件より少ない

input.txt の中身

192.0.2.1 - - [30/Oct/2026:09:03:00 +0900] "GET / HTTP/1.1" 200 10 0.020
$ python3 access_log.py < input.txt
件数 1
ステータス
200 1
遅い順
0.020 GET /

空の入力

input.txt の中身

(空)
$ python3 access_log.py < input.txt
件数 0
ステータス
遅い順

最後に改行が無い

input.txt の中身

203.0.113.5 - - [30/Oct/2026:09:00:01 +0900] "GET /index.html HTTP/1.1" 200 5120 0.012
203.0.113.7 - - [30/Oct/2026:09:00:02 +0900] "GET /api/search?q=%E5%A4%A9%E6%B0%97 HTTP/1.1" 200 812 0.950
198.51.100.2 - - [30/Oct/2026:09:00:03 +0900] "POST /api/login HTTP/1.1" 401 64 0.034
203.0.113.5 - - [30/Oct/2026:09:00:04 +0900] "GET /favicon.ico HTTP/1.1" 404 - 0.001
192.0.2.10 - - [30/Oct/2026:09:00:05 +0900] "GET /api/report HTTP/2.0" 500 128 2.103
203.0.113.9 - - [30/Oct/2026:09:00:06 +0900] "GET /index.html HTTP/1.1" 200 5120 0.010
198.51.100.2 - - [30/Oct/2026:09:00:07 +0900] "POST /api/login HTTP/1.1" 200 256 0.120
$ python3 access_log.py < input.txt
件数 7
ステータス
200 4
401 1
404 1
500 1
遅い順
2.103 GET /api/report
0.950 GET /api/search?q=%E5%A4%A9%E6%B0%97
0.120 POST /api/login

10言語のプログラム

どれもそのまま保存して動かせます。毎朝、上の正解の例をすべて実際に動かして確かめています。

access_log.php8.4.26 で 8 / 8 件の出力が正解と一致

<?php
// 標準入力のアクセスログを読んで、件数・ステータスごとの件数・遅い順の上位3件を出す
// 日時やHTTPの版には空白が入るので、空白で区切って何番目と数えず、行の形そのものを正規表現で確かめる。
// $ は「最後の改行の手前」にも合うので、行の終わりは \z で書く
$log = '/\A\S+ \S+ \S+ \[[^\]]+\] "(\S+) (\S+) [^"]+" ([0-9]{3}) ([0-9]+|-) ([0-9]+\.[0-9]+)\z/';

$rows = [];
$bad = 0;
// \r\n と \n のどちらでも区切る。最後に改行が無くても、最後の行はそのまま残る
foreach (preg_split('/\r?\n/', stream_get_contents(STDIN)) as $line) {
    if ($line === '') {
        continue;
    }
    if (!preg_match($log, $line, $m)) {
        $bad++;
        continue;
    }
    $rows[] = ['sec' => $m[5], 'method' => $m[1], 'path' => $m[2], 'status' => $m[3]];
}

echo "件数 " . count($rows) . "\n";
if ($bad > 0) {
    echo "読めない行 " . $bad . "\n";
}

$counts = [];
foreach ($rows as $r) {
    // "200" のような数字だけの文字列をキーにすると、PHP は整数の 200 に変えてしまう。並べる・出すだけなら困らない
    $counts[$r['status']] = ($counts[$r['status']] ?? 0) + 1;
}
ksort($counts);
echo "ステータス\n";
foreach ($counts as $status => $n) {
    echo $status . " " . $n . "\n";
}

echo "遅い順\n";
// 応答秒は (float) にして比べる。PHP 8.0 から usort は安定なので、同じ秒なら先に出た行が先に残る
usort($rows, fn($a, $b) => (float) $b['sec'] <=> (float) $a['sec']);
foreach (array_slice($rows, 0, 3) as $r) {
    echo $r['sec'] . " " . $r['method'] . " " . $r['path'] . "\n";
}

動かし方(ファイルを保存したフォルダで)

$ php access_log.php < input.txt

よく見かける書き方で試すと

仕事でよく見かける書き方(標準の関数をそのまま使う形や、よく紹介される正規表現など)を、同じ正解表で動かした結果です。言語を出した答えごとにまとめ、赤い字はその言語が実際に出した間違った行です。

入力よく見かける書き方の答え(10言語)
基本
  • Java500 1
    404 1
  • PHP・Python・Go・JavaScript・C#・Rust・Ruby・Kotlin・Swift✓ 正解
読めない行と空の行が混ざる
  • PHP・Python・Go・C#・Rust・Ruby・Kotlin・Swift件数 8
    読めない行 1
    2OO 1
    401 1
    404 1
    500 1
    遅い順
    2.103 GET /api/report
    0.950 GET /api/search?q=%E5%A4%A9%E6%B0%97
    0.120 POST /api/login
  • JavaScript件数 8
    読めない行 1
    2OO 1
    遅い順
    2.103 GET /api/report
    0.950 GET /api/search?q=%E5%A4%A9%E6%B0%97
    0.120 POST /api/login
  • Java件数 8
    読めない行 1
    2OO 1
    401 1
    404 1
    遅い順
    2.103 GET /api/report
    0.950 GET /api/search?q=%E5%A4%A9%E6%B0%97
    0.120 POST /api/login
応答秒の桁数が違う(文字として比べると 10.5 が 9.8 より小さくなる)
  • Python・Java・Kotlin・Swift9.8 GET /a
    10.5 GET /b
  • PHP・Go・JavaScript・C#・Rust・Ruby✓ 正解
同じ応答秒は先に出た順
  • Ruby0.500 GET /fourth
    0.500 GET /third
    0.500 GET /second
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Kotlin・Swift✓ 正解
改行が \r\n(Windows で保存したログ)
  • JavaScript・C#2.103
    GET /api/report
    0.950
    GET /api/search?q=%E5%A4%A9%E6%B0%97
    0.120
    POST /api/login
  • Java500 1
    404 1
  • Rustエラーで止まった
  • PHP・Python・Go・Ruby・Kotlin・Swift✓ 正解
3件より少ない

✓10言語すべて正解

空の入力

✓10言語すべて正解

最後に改行が無い
  • Java500 1
    404 1
  • PHP・Python・Go・JavaScript・C#・Rust・Ruby・Kotlin・Swift✓ 正解

この書き方の決まり

ログの1行の形(応答秒が最後に付いた形):

203.0.113.5 - - [30/Oct/2026:09:00:01 +0900] "GET /index.html HTTP/1.1" 200 5120 0.012
  • 1行ずつ形を確かめる。ステータスは3桁の数字、バイト数は数字か -、応答秒は「数字.数字」。合わない行は「読めない行」として数える
  • ステータスごとの件数を、ステータスの小さい順に出す
  • 応答秒の大きい順に上位3件。同じ秒なら、先に出た行を先に
  • 空の行は数えない。改行が \r\n でも、最後に改行が無くても同じ答えにする

解説

空白で区切ると、壊れた行も数えてしまう

line.split() で区切って「11個あれば読めた行」とする書き方は、10言語すべてで、ステータスが 2OO(オーではなく数字のゼロのつもり)の行を読めた行として数えました。数として使う欄は、形を確かめてから使います。この書き方では、行全体が正規表現に合うかを確かめています(Rust は標準に正規表現が無いので、区切り文字ごとに切って確かめています)。

応答秒を、文字のまま並べない

"10.5" と "9.8" を文字として比べると、1文字目の 1 と 9 を比べるので、10.5 秒が 9.8 秒より「小さい」ことになります。Python・Kotlin・Swift・Java の naive がこれで落ちました。PHP の <=> は、数字だけの文字列どうしなら数として比べるので、たまたま正しく並びました。

同じ秒の並び順(安定な並べ替え)

「同じ秒なら先に出た行を先に」を守るには、安定な並べ替え(同じ値の順番を変えないもの)を使います。

  • 安定: Python sorted、Java Collections.sort・List.sort、Kotlin sortedByDescending、Go sort.SliceStable、C# の LINQ OrderByDescending、JavaScript Array.prototype.sort(ES2019 から)、Rust sort_by
  • 安定と決まっていない: Go sort.Slice、C# List.Sort、Ruby sort_by、Swift sorted(文書では約束されていない)

Ruby の naive は sort_by { 秒 }.reverse で並べたため、同じ秒の行が逆の順になりました。安定と決まっていない言語では、何行目かを2つ目のキーにしています。

\r\n のログ

Windows で保存したログは、行の終わりが \r\n です。行ごとに読む関数の多く(Java・C#・Kotlin・Swift・Rust の行の読み方、Go の Scanner)は \r も取ってくれます。全体を読んで \n だけで区切る書き方では \r が残り、応答秒の数への変換に失敗したり(Rust の naive は止まった)、出力に \r が混ざったりしました。

件数の並び順

ステータスごとの件数を、ハッシュの表(Java の HashMap など)に入れてそのまま出すと、200, 401, 500, 404 のような、ハッシュで決まる順になります。小さい順に並べ直してから出します。