画面にこう出れば正解
どの言語で書いても、同じ入力なら画面に出る文字は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/login3件より少ない
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/login10言語のプログラム
どれもそのまま保存して動かせます。毎朝、上の正解の例をすべて実際に動かして確かめています。
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
access_log.py3.14.7 で 8 / 8 件の出力が正解と一致
# 標準入力のアクセスログを読んで、件数・ステータスごとの件数・遅い順の上位3件を出す
import re
import sys
# 日時やHTTPの版には空白が入るので、空白で区切って何番目と数えず、行の形そのものを正規表現で確かめる
LOG = re.compile(r'\S+ \S+ \S+ \[[^\]]+\] "(\S+) (\S+) [^"]+" ([0-9]{3}) ([0-9]+|-) ([0-9]+\.[0-9]+)')
rows = []
bad = 0
# 既定の sys.stdin は \r\n を \n にそろえて読むが、newline="" やバイトで読むと \r が行に残る。どちらでも困らないよう \r を取る
for line in sys.stdin.read().split("\n"):
line = line.rstrip("\r")
if line == "":
continue
# match は先頭だけ合えば通る。行の終わりまで形どおりか確かめるには fullmatch
m = LOG.fullmatch(line)
if m is None:
bad += 1
continue
method, path, status, _, sec = m.groups()
rows.append((sec, method, path, status))
print(f"件数 {len(rows)}")
if bad:
print(f"読めない行 {bad}")
counts = {}
for _, _, _, status in rows:
counts[status] = counts.get(status, 0) + 1
print("ステータス")
# ステータスは3桁そろいなので、文字のまま並べても数の小さい順になる
for status in sorted(counts):
print(f"{status} {counts[status]}")
print("遅い順")
# 応答秒は文字のまま並べると "10.5" < "9.8" になるので、数にして比べる。
# sorted は安定(同じ値なら元の順)。reverse=True でも同じ秒どうしの順は崩れない
for sec, method, path, _ in sorted(rows, key=lambda r: float(r[0]), reverse=True)[:3]:
print(f"{sec} {method} {path}")
動かし方(ファイルを保存したフォルダで)
$ python3 access_log.py < input.txt
access_log.go1.27.2 で 8 / 8 件の出力が正解と一致
// 標準入力のアクセスログを読んで、件数・ステータスごとの件数・遅い順の上位3件を出す
package main
import (
"bufio"
"fmt"
"os"
"regexp"
"sort"
"strconv"
)
// 日時やHTTPの版には空白が入るので、空白で区切って何番目と数えず、行の形そのものを正規表現で確かめる
var logLine = regexp.MustCompile(`^\S+ \S+ \S+ \[[^\]]+\] "(\S+) (\S+) [^"]+" ([0-9]{3}) ([0-9]+|-) ([0-9]+\.[0-9]+)$`)
type row struct {
sec string
secNum float64
method string
path string
status string
}
func main() {
var rows []row
bad := 0
// bufio.Scanner は行の終わりの \n と、その手前の \r を取って返す。最後に改行が無い行も読む
sc := bufio.NewScanner(os.Stdin)
for sc.Scan() {
line := sc.Text()
if line == "" {
continue
}
m := logLine.FindStringSubmatch(line)
if m == nil {
bad++
continue
}
// 正規表現で「数字.数字」と確かめてあるので、ParseFloat は失敗しない
n, _ := strconv.ParseFloat(m[5], 64)
rows = append(rows, row{sec: m[5], secNum: n, method: m[1], path: m[2], status: m[3]})
}
// Scan が false になったのが読み終わりか失敗かは Err で見分ける(1行が 64KB を超えると失敗する)
if err := sc.Err(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("件数 %d\n", len(rows))
if bad > 0 {
fmt.Printf("読めない行 %d\n", bad)
}
counts := map[string]int{}
for _, r := range rows {
counts[r.status]++
}
// map を range で回す順番は毎回わざと変えられている。キーを取り出して並べてから出す
statuses := make([]string, 0, len(counts))
for s := range counts {
statuses = append(statuses, s)
}
sort.Strings(statuses)
fmt.Println("ステータス")
for _, s := range statuses {
fmt.Printf("%s %d\n", s, counts[s])
}
fmt.Println("遅い順")
// sort.Slice は安定でない(同じ秒の行の順が入れ替わることがある)。先に出た行を先に残すには SliceStable
sort.SliceStable(rows, func(i, j int) bool { return rows[i].secNum > rows[j].secNum })
for i, r := range rows {
if i == 3 {
break
}
fmt.Printf("%s %s %s\n", r.sec, r.method, r.path)
}
}
動かし方(ファイルを保存したフォルダで)
$ go run access_log.go < input.txt
access_log.js24.15.0 で 8 / 8 件の出力が正解と一致
// 標準入力のアクセスログを読んで、件数・ステータスごとの件数・遅い順の上位3件を出す
// 日時やHTTPの版には空白が入るので、空白で区切って何番目と数えず、行の形そのものを正規表現で確かめる
const LOG = /^\S+ \S+ \S+ \[[^\]]+\] "(\S+) (\S+) [^"]+" ([0-9]{3}) ([0-9]+|-) ([0-9]+\.[0-9]+)$/;
const rows = [];
let bad = 0;
// \r\n と \n のどちらでも区切る。空の入力は [""] になり、下で空の行として飛ばされる
for (const line of require("fs").readFileSync(0, "utf8").split(/\r?\n/)) {
if (line === "") continue;
const m = LOG.exec(line);
if (m === null) {
bad++;
continue;
}
rows.push({ sec: m[5], method: m[1], path: m[2], status: m[3] });
}
console.log(`件数 ${rows.length}`);
if (bad > 0) console.log(`読めない行 ${bad}`);
// Map は入れた順に回る。並べる順は自分で決める
const counts = new Map();
for (const r of rows) counts.set(r.status, (counts.get(r.status) ?? 0) + 1);
console.log("ステータス");
// 引数なしの sort は文字として並べる。ステータスは3桁そろいなので、これで数の小さい順になる
for (const status of [...counts.keys()].sort()) console.log(`${status} ${counts.get(status)}`);
console.log("遅い順");
// 応答秒は Number にして引き算で比べる。sort は ES2019 から安定と決まった(同じ秒なら先に出た行が先)。
// toSorted は元の配列を変えずに並べ替えた新しい配列を返す
const slow = rows.toSorted((a, b) => Number(b.sec) - Number(a.sec)).slice(0, 3);
for (const r of slow) console.log(`${r.sec} ${r.method} ${r.path}`);
動かし方(ファイルを保存したフォルダで)
$ node access_log.js < input.txt
AccessLog.java21.0.12 で 8 / 8 件の出力が正解と一致
// 標準入力のアクセスログを読んで、件数・ステータスごとの件数・遅い順の上位3件を出す
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AccessLog {
// 日時やHTTPの版には空白が入るので、空白で区切って何番目と数えず、行の形そのものを正規表現で確かめる
static final Pattern LOG = Pattern.compile(
"\\S+ \\S+ \\S+ \\[[^\\]]+\\] \"(\\S+) (\\S+) [^\"]+\" ([0-9]{3}) ([0-9]+|-) ([0-9]+\\.[0-9]+)");
record Row(String sec, double secNum, String method, String path, String status) {}
public static void main(String[] args) throws IOException {
List<Row> rows = new ArrayList<>();
int bad = 0;
// readLine は \n でも \r\n でも行を区切り、行の終わりの記号を取って返す
BufferedReader in = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
String line;
while ((line = in.readLine()) != null) {
if (line.isEmpty()) {
continue;
}
// find は行のどこかに合えば通る。行全体が形どおりか確かめるのは matches
Matcher m = LOG.matcher(line);
if (!m.matches()) {
bad++;
continue;
}
rows.add(new Row(m.group(5), Double.parseDouble(m.group(5)), m.group(1), m.group(2), m.group(3)));
}
System.out.println("件数 " + rows.size());
if (bad > 0) {
System.out.println("読めない行 " + bad);
}
// HashMap は取り出す順番が決まっていない。TreeMap はキーの小さい順に取り出せる
Map<String, Integer> counts = new TreeMap<>();
for (Row r : rows) {
counts.merge(r.status(), 1, Integer::sum);
}
System.out.println("ステータス");
for (Map.Entry<String, Integer> e : counts.entrySet()) {
System.out.println(e.getKey() + " " + e.getValue());
}
System.out.println("遅い順");
// List.sort は安定(同じ秒なら先に出た行が先)。応答秒は文字ではなく double で比べる
rows.sort(Comparator.comparingDouble(Row::secNum).reversed());
for (Row r : rows.subList(0, Math.min(3, rows.size()))) {
System.out.println(r.sec() + " " + r.method() + " " + r.path());
}
}
}
動かし方(ファイルを保存したフォルダで)
$ java AccessLog.java < input.txt
access_log.cs10.0.402 で 8 / 8 件の出力が正解と一致
// 標準入力のアクセスログを読んで、件数・ステータスごとの件数・遅い順の上位3件を出す
using System.Globalization;
using System.Text.RegularExpressions;
// 日時やHTTPの版には空白が入るので、空白で区切って何番目と数えず、行の形そのものを正規表現で確かめる。
// .NET の \d は全角やアラビア文字の数字にも合うので [0-9] と書く。$ は最後の改行の手前にも合うので \z
var log = new Regex(@"\A\S+ \S+ \S+ \[[^\]]+\] ""(\S+) (\S+) [^""]+"" ([0-9]{3}) ([0-9]+|-) ([0-9]+\.[0-9]+)\z");
var rows = new List<(string Sec, double SecNum, string Method, string Path, string Status)>();
int bad = 0;
// Console.ReadLine は \n でも \r\n でも行を区切り、行の終わりの記号を取って返す
string? line;
while ((line = Console.ReadLine()) != null)
{
if (line == "")
{
continue;
}
Match m = log.Match(line);
if (!m.Success)
{
bad++;
continue;
}
string sec = m.Groups[5].Value;
// double.Parse は国の設定に従う(ドイツ語の設定では「,」が小数点)。ログの「.」で読むため InvariantCulture
rows.Add((sec, double.Parse(sec, CultureInfo.InvariantCulture), m.Groups[1].Value, m.Groups[2].Value, m.Groups[3].Value));
}
Console.WriteLine($"件数 {rows.Count}");
if (bad > 0)
{
Console.WriteLine($"読めない行 {bad}");
}
// SortedDictionary はキーの小さい順に取り出せる。文字の比べ方は国の設定に左右されない Ordinal にする
var counts = new SortedDictionary<string, int>(StringComparer.Ordinal);
foreach (var r in rows)
{
counts[r.Status] = counts.GetValueOrDefault(r.Status) + 1;
}
Console.WriteLine("ステータス");
foreach (var (status, n) in counts)
{
Console.WriteLine($"{status} {n}");
}
Console.WriteLine("遅い順");
// List.Sort は安定でない(同じ秒の行の順が入れ替わることがある)。LINQ の OrderByDescending は安定
foreach (var r in rows.OrderByDescending(r => r.SecNum).Take(3))
{
Console.WriteLine($"{r.Sec} {r.Method} {r.Path}");
}
動かし方(ファイルを保存したフォルダで)
$ dotnet run access_log.cs -- < input.txt
access_log.rs1.99.0 で 8 / 8 件の出力が正解と一致
// 標準入力のアクセスログを読んで、件数・ステータスごとの件数・遅い順の上位3件を出す
use std::collections::BTreeMap;
use std::io::BufRead;
struct Row {
sec: String,
sec_num: f64,
method: String,
path: String,
status: String,
}
// 空でなく、空白を含まない(正規表現の \S+ にあたる)
fn is_token(s: &str) -> bool {
!s.is_empty() && !s.chars().any(char::is_whitespace)
}
fn is_digits(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}
// Rust の標準には正規表現が無いので、決まった区切り(「 [」「] "」「"」「 」)で順に切り分けて形を確かめる。
// 日時やHTTPの版には空白が入るので、行をまるごと空白で区切って何番目と数えることはしない
fn parse(line: &str) -> Option<Row> {
// <IP> - - [<日時>] "<リクエスト>" <ステータス> <バイト数> <応答秒>
let (head, rest) = line.split_once(" [")?;
let head: Vec<&str> = head.split(' ').collect();
if head.len() != 3 || !head.iter().all(|t| is_token(t)) {
return None;
}
let (date, rest) = rest.split_once("] \"")?;
if date.is_empty() || date.contains(']') {
return None;
}
// リクエストの中には「"」が無い。次の「"」までがリクエスト
let (request, tail) = rest.split_once('"')?;
let mut req = request.splitn(3, ' ');
let method = req.next()?;
let path = req.next()?;
let version = req.next()?;
if !is_token(method) || !is_token(path) || version.is_empty() {
return None;
}
let tail: Vec<&str> = tail.strip_prefix(' ')?.split(' ').collect();
let [status, bytes, sec] = tail[..] else { return None };
if status.len() != 3 || !is_digits(status) {
return None;
}
if bytes != "-" && !is_digits(bytes) {
return None;
}
// parse::<f64>() は "1e3" や "inf" も受け付けるので、「数字.数字」の形は自分で確かめる
let (int_part, frac_part) = sec.split_once('.')?;
if !is_digits(int_part) || !is_digits(frac_part) {
return None;
}
Some(Row {
sec: sec.to_string(),
sec_num: sec.parse().ok()?,
method: method.to_string(),
path: path.to_string(),
status: status.to_string(),
})
}
fn main() {
let mut rows = Vec::new();
let mut bad = 0;
// lines() は \n でも \r\n でも行を区切り、行の終わりの記号を取って返す
for line in std::io::stdin().lock().lines() {
let line = line.unwrap();
if line.is_empty() {
continue;
}
match parse(&line) {
Some(r) => rows.push(r),
None => bad += 1,
}
}
println!("件数 {}", rows.len());
if bad > 0 {
println!("読めない行 {}", bad);
}
// HashMap は回す順番が動かすたびに変わる。BTreeMap はキーの小さい順に回る
let mut counts: BTreeMap<&str, usize> = BTreeMap::new();
for r in &rows {
*counts.entry(&r.status).or_insert(0) += 1;
}
println!("ステータス");
for (status, n) in &counts {
println!("{} {}", status, n);
}
println!("遅い順");
// f64 は NaN があるので Ord ではなく、sort_by_key には渡せない。total_cmp で比べる。
// sort_by は安定(同じ秒なら先に出た行が先)。sort_unstable_by は安定でない
let mut slow: Vec<&Row> = rows.iter().collect();
slow.sort_by(|a, b| b.sec_num.total_cmp(&a.sec_num));
for r in slow.iter().take(3) {
println!("{} {} {}", r.sec, r.method, r.path);
}
}
動かし方(ファイルを保存したフォルダで)
$ rustc access_log.rs && ./access_log < input.txt
access_log.rb4.0.7 で 8 / 8 件の出力が正解と一致
# 標準入力のアクセスログを読んで、件数・ステータスごとの件数・遅い順の上位3件を出す
# 日時やHTTPの版には空白が入るので、空白で区切って何番目と数えず、行の形そのものを正規表現で確かめる。
# Ruby の ^ と $ は「行の始まり・終わり」で、文字列の中の改行の前後にも合う。文字列全体の始まり・終わりは \A と \z
LOG = /\A\S+ \S+ \S+ \[[^\]]+\] "(\S+) (\S+) [^"]+" ([0-9]{3}) ([0-9]+|-) ([0-9]+\.[0-9]+)\z/
rows = []
bad = 0
# 文字コードを UTF-8 と決めて読む。chomp は行の終わりの \n も \r\n も取る
$stdin.read.force_encoding("UTF-8").each_line do |line|
line = line.chomp
next if line.empty?
m = LOG.match(line)
if m.nil?
bad += 1
next
end
rows << { sec: m[5], method: m[1], path: m[2], status: m[3] }
end
puts "件数 #{rows.length}"
puts "読めない行 #{bad}" if bad > 0
puts "ステータス"
# tally は「値 => 出てきた数」の Hash を作る。Hash は入れた順なので、キーで並べてから出す
rows.map { |r| r[:status] }.tally.sort.each do |status, n|
puts "#{status} #{n}"
end
puts "遅い順"
# sort_by は安定でない(同じ秒の行の順が入れ替わることがある)。何行目かを2つ目のキーにして、先に出た行を先にする
slow = rows.each_with_index.sort_by { |r, i| [-r[:sec].to_f, i] }.first(3)
slow.each do |r, _|
puts "#{r[:sec]} #{r[:method]} #{r[:path]}"
end
動かし方(ファイルを保存したフォルダで)
$ ruby access_log.rb < input.txt
access_log.kt2.4.21 で 8 / 8 件の出力が正解と一致
// 標準入力のアクセスログを読んで、件数・ステータスごとの件数・遅い順の上位3件を出す
// 日時やHTTPの版には空白が入るので、空白で区切って何番目と数えず、行の形そのものを正規表現で確かめる
val LOG = Regex("""\S+ \S+ \S+ \[[^\]]+\] "(\S+) (\S+) [^"]+" ([0-9]{3}) ([0-9]+|-) ([0-9]+\.[0-9]+)""")
data class Row(val sec: String, val method: String, val path: String, val status: String)
fun main() {
val rows = mutableListOf<Row>()
var bad = 0
// readLine は \n でも \r\n でも行を区切り、行の終わりの記号を取って返す
while (true) {
val line = readLine() ?: break
if (line.isEmpty()) continue
// find は行のどこかに合えば通る。行全体が形どおりか確かめるのは matchEntire
val m = LOG.matchEntire(line)
if (m == null) {
bad++
continue
}
val (method, path, status, _, sec) = m.destructured
rows.add(Row(sec, method, path, status))
}
println("件数 ${rows.size}")
if (bad > 0) println("読めない行 $bad")
println("ステータス")
// groupingBy { }.eachCount() は入れた順の Map を返す。toSortedMap でキーの小さい順にする
for ((status, n) in rows.groupingBy { it.status }.eachCount().toSortedMap()) {
println("$status $n")
}
println("遅い順")
// sortedByDescending { it.sec } では文字として比べてしまい "10.5" が "9.8" より後になる。toDouble してから比べる。
// Kotlin の並べ替えは安定(同じ秒なら先に出た行が先)
for (r in rows.sortedByDescending { it.sec.toDouble() }.take(3)) {
println("${r.sec} ${r.method} ${r.path}")
}
}
動かし方(ファイルを保存したフォルダで)
$ kotlinc access_log.kt -include-runtime -d access_log.jar && java -jar access_log.jar < input.txt
access_log.swift6.3.3 で 8 / 8 件の出力が正解と一致
// 標準入力のアクセスログを読んで、件数・ステータスごとの件数・遅い順の上位3件を出す
// 日時やHTTPの版には空白が入るので、空白で区切って何番目と数えず、行の形そのものを正規表現で確かめる。
// Swift の正規表現の \d は全角などの数字にも合うので [0-9] と書く。#/…/# は「/」を書きやすくする囲み方
let log = #/\S+ \S+ \S+ \[[^\]]+\] "(\S+) (\S+) [^"]+" ([0-9]{3}) ([0-9]+|-) ([0-9]+\.[0-9]+)/#
var rows: [(sec: Substring, secNum: Double, method: Substring, path: Substring, status: Substring)] = []
var bad = 0
// readLine は \n でも \r\n でも行を区切り、行の終わりの記号を取って返す
while let line = readLine() {
if line.isEmpty {
continue
}
// firstMatch は行のどこかに合えば通る。行全体が形どおりか確かめるのは wholeMatch
guard let m = try? log.wholeMatch(in: line) else {
bad += 1
continue
}
// 正規表現で「数字.数字」と確かめてあるので、Double(…) は nil にならない
rows.append((m.5, Double(m.5)!, m.1, m.2, m.3))
}
print("件数 \(rows.count)")
if bad > 0 {
print("読めない行 \(bad)")
}
var counts: [Substring: Int] = [:]
for r in rows {
counts[r.status, default: 0] += 1
}
print("ステータス")
// Dictionary を回す順番は決まっておらず、動かすたびに変わる。キーを並べてから出す
for status in counts.keys.sorted() {
print("\(status) \(counts[status]!)")
}
print("遅い順")
// Swift の sort は安定と約束されていない(いまの実装は安定だが、文書では保証しない)。
// 何行目かを2つ目のキーにして、同じ秒なら先に出た行を先にする
let slow = rows.enumerated().sorted { a, b in
a.element.secNum != b.element.secNum ? a.element.secNum > b.element.secNum : a.offset < b.offset
}.prefix(3)
for (_, r) in slow {
print("\(r.sec) \(r.method) \(r.path)")
}
動かし方(ファイルを保存したフォルダで)
$ swift access_log.swift < input.txt
よく見かける書き方で試すと
仕事でよく見かける書き方(標準の関数をそのまま使う形や、よく紹介される正規表現など)を、同じ正解表で動かした結果です。言語を出した答えごとにまとめ、赤い字はその言語が実際に出した間違った行です。
| 入力 | よく見かける書き方の答え(10言語) |
|---|---|
| 基本 |
|
| 読めない行と空の行が混ざる |
|
| 応答秒の桁数が違う(文字として比べると 10.5 が 9.8 より小さくなる) |
|
| 同じ応答秒は先に出た順 |
|
| 改行が \r\n(Windows で保存したログ) |
|
| 3件より少ない | ✓10言語すべて正解 |
| 空の入力 | ✓10言語すべて正解 |
| 最後に改行が無い |
|
この書き方の決まり
ログの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、JavaCollections.sort・List.sort、KotlinsortedByDescending、Gosort.SliceStable、C# の LINQOrderByDescending、JavaScriptArray.prototype.sort(ES2019 から)、Rustsort_by - 安定と決まっていない: Go
sort.Slice、C#List.Sort、Rubysort_by、Swiftsorted(文書では約束されていない)
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 のような、ハッシュで決まる順になります。小さい順に並べ直してから出します。