Логи — это журналы событий серверов, приложений и сервисов: каждый запрос к сайту, каждая ошибка, каждое действие пользователя оставляет в них строку. Парсинг логов нужен для аналитики трафика, поиска ошибок, расследования инцидентов безопасности и мониторинга. Формально логи — это текстовые файлы, и общие приёмы их разбора те же; но у них достаточно своей специфики, чтобы выделить тему в отдельную статью нашего обзора парсинга документов.
Чем логи отличаются от обычного текста
Главная особенность — в пределах одного источника формат строки строго стабилен. Веб-сервер пишет каждую строку по одному шаблону, и это позволяет разбирать миллионы строк одним регулярным выражением. Но есть и осложнения. Логи бывают огромными — гигабайты в день, поэтому их нельзя читать целиком в память, только потоково. Они ротируются — старые файлы переименовываются и сжимаются в .gz, и парсер должен уметь читать сжатые архивы. И в них встречаются многострочные записи — например, трассировка ошибки (stack trace) занимает десятки строк, относящихся к одному событию.
Стандартные форматы веб-логов
Чаще всего разбирают access-логи Nginx и Apache в «combined» формате. Одна строка выглядит так:
192.168.1.10 - - [01/Jun/2026:13:55:36 +0300] "GET /product/101 HTTP/1.1" 200 4523 "https://example.com/" "Mozilla/5.0 ..."
Здесь по порядку: IP-адрес, дата и время, метод и путь запроса, код ответа, размер ответа, реферер и User-Agent. Поскольку формат фиксирован, его удобно разобрать одним регулярным выражением с именованными группами.
Python
Базовый и самый гибкий подход — регулярное выражение плюс потоковое чтение. Именованные группы делают результат читаемым.
import re
import gzip
LOG_RE = re.compile(
r'(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] '
r'"(?P<method>\S+) (?P<path>\S+) [^"]*" '
r'(?P<status>\d{3}) (?P<size>\S+) '
r'"(?P<referer>[^"]*)" "(?P<agent>[^"]*)"'
)
def open_log(path):
# прозрачно читаем и обычные, и сжатые файлы
return gzip.open(path, "rt") if path.endswith(".gz") else open(path, "r")
with open_log("access.log") as f:
for line in f:
m = LOG_RE.match(line)
if not m:
continue
row = m.groupdict()
if row["status"] != "200":
print(row["status"], row["method"], row["path"], row["ip"])
Когда нужно не просто разобрать, а проанализировать — посчитать топ страниц, распределение кодов ответа, трафик по часам — разобранные строки удобно сложить в pandas и работать с ними как с таблицей.
import pandas as pd
records = [m.groupdict() for line in open_log("access.log")
if (m := LOG_RE.match(line))]
df = pd.DataFrame(records)
# топ-10 самых запрашиваемых страниц
print(df["path"].value_counts().head(10))
# доля ошибок 5xx
errors = df[df["status"].str.startswith("5")]
print(len(errors) / len(df))
Отсюда же результат легко выгрузить в CSV или Excel для отчёта.
Командная строка: быстрый разбор без кода
Для разовой задачи часто быстрее всего обойтись утилитами Unix. awk разбивает строку на поля и считает на лету.
# топ-10 IP по числу запросов
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
# все запросы, завершившиеся ошибкой 404
awk '$9 == 404 {print $7}' access.log | sort | uniq -c | sort -rn
# суммарный объём отданного трафика (9-е поле — размер)
awk '{sum += $10} END {print sum/1024/1024 " MB"}' access.log
Связка grep + awk + sort + uniq решает большинство ad-hoc вопросов к логам и не требует ничего писать.
Готовые анализаторы
Если задача — общая аналитика веб-трафика, а не извлечение конкретных полей, изобретать парсер не нужно. GoAccess читает access-логи Nginx/Apache и строит интерактивный отчёт в терминале или в браузере в реальном времени. Для сложных пайплайнов с разнородными источниками применяют стек на основе Logstash с его наборами шаблонов grok — это, по сути, библиотека готовых именованных регулярных выражений под популярные форматы логов.
Структурированные логи (JSON-логи)
Современные приложения всё чаще пишут логи не строками, а в виде JSON — по объекту на строку (формат JSON Lines). Такой лог разбирать намного проще и надёжнее: не нужны хрупкие регулярные выражения, каждая строка просто десериализуется.
import json
with open("app.log") as f:
for line in f:
event = json.loads(line)
if event.get("level") == "ERROR":
print(event["timestamp"], event["message"])
Если есть возможность влиять на то, как пишутся логи, переход на JSON Lines радикально упрощает их последующий разбор — это стоит закладывать в проект заранее.
С чем приходят сложности
Самые частые проблемы — это разный формат времени (логи из разных систем используют разные часовые пояса и шаблоны дат, которые нужно приводить к единому виду), многострочные ошибки (stack trace ломает построчный разбор, и записи приходится «склеивать» по признаку начала новой записи) и объём (анализ дневного лога в гигабайты требует потоковой обработки и фильтрации на лету, а не загрузки в память).
Если нужно регулярно разбирать логи с нескольких серверов, сводить их в единую аналитику или мониторить ошибки и подозрительную активность — мы настроим сбор и парсинг логов под ваши форматы с выгрузкой метрик в удобную систему. Общие приёмы работы с произвольным текстом, на которых это строится, разобраны в статье про парсинг TXT.