← все задачи
Python · задача 9 из 10
Разобрать лог и собрать статистику
Средний
20 минут
генераторыпотоковая обработкаагрегация
Условие
Дан лог доступа: в каждой строке время, метод, путь, код ответа и длительность. Посчитайте по каждому пути: число запросов, долю ошибок (код 5xx) и среднее время ответа. Файл может быть в несколько гигабайт.
Что требуется
- Файл не помещается в память — читать построчно
- Битые строки не должны ронять обработку
- Результат отсортирован по числу запросов
Пример
2026-09-13T10:00:01 GET /api/users 200 0.042 2026-09-13T10:00:02 GET /api/users 500 1.500 2026-09-13T10:00:03 POST /api/orders 201 0.310 # /api/users: 2 запроса, 50% ошибок, среднее 0.771с # /api/orders: 1 запрос, 0% ошибок, среднее 0.310с
Сначала уточните
Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.
- Формат строк стабильный или бывают варианты — можно ли рассчитывать на split?
- Нужно ли группировать пути с параметрами: /api/users/42 и /api/users/7 — это один путь или разные?
- Что делать с битой строкой: пропустить молча, посчитать или упасть?
Показать решение Скрыть решение
Решение
from collections import defaultdict
def parse_line(line):
parts = line.split()
if len(parts) != 5:
return None
_timestamp, _method, path, status, duration = parts
try:
return path, int(status), float(duration)
except ValueError:
return None
def collect_stats(lines):
stats = defaultdict(lambda: {"count": 0, "errors": 0, "total_time": 0.0})
skipped = 0
for line in lines: # lines — файл, читается лениво
parsed = parse_line(line)
if parsed is None:
skipped += 1
continue
path, status, duration = parsed
row = stats[path]
row["count"] += 1
row["total_time"] += duration
if status >= 500:
row["errors"] += 1
report = [
{
"path": path,
"count": row["count"],
"error_rate": row["errors"] / row["count"],
"avg_time": row["total_time"] / row["count"],
}
for path, row in stats.items()
]
report.sort(key=lambda item: -item["count"])
return report, skipped
with open("access.log") as f:
report, skipped = collect_stats(f)
Почему так
Почему функция принимает строки, а не путь к файлу
- Так её можно тестировать списком строк, без временных файлов на диске
- На вход годится что угодно итерируемое: файл, gzip-поток, ответ по сети, генератор
- Открытие файла остаётся снаружи — там же, где решают, закрывать его и как
Почему считаем суммы, а не храним все длительности
- Для среднего достаточно суммы и количества — держать список из миллиона чисел незачем
- Память остаётся пропорциональной числу уникальных путей, а не числу строк
- Важная оговорка: для p95 и медианы так уже не выйдет — нужен t-digest или приблизительные квантили; об этом стоит сказать самому
Почему битые строки считаем, а не глотаем
- Молча пропускать — значит однажды построить отчёт по трём строкам из миллиона и не заметить
- Счётчик skipped наружу превращает «данные странные» в измеримый факт
- Ронять обработку на одной кривой строке тоже нельзя: гигабайтный файл не дочитается из-за одного мусора
Что спросят дальше
- Спросят про пути с id: покажите нормализацию — замену цифровых сегментов на {id} регуляркой
- Спросят про распараллеливание: файл делится на куски по смещениям, агрегаты складываются — но сначала убедитесь, что узкое место в CPU, а не в диске
- Спросят про p95 — честный ответ «нужны все значения или приблизительный алгоритм» лучше выдуманного
Следующая задача
Ограничитель частоты вызовов — Rate limiter руками: проверяют алгоритм, работу со временем и понимание гонок.