Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 19 из 20 Средний 22 мин 140 XP

Даты в логах: разбор реального формата

Журнал из строк вида «2026-10-21 09:15:03 ERROR …»: разбираем момент через strptime, сортируем события по времени и вырезаем записи за интервал.

Редакция Питоники

Таблицы из прошлого урока — лишь половина источников дат-строк. Вторая половина — логи: серверные журналы, платёжные выгрузки, истории заказов. Типичная строка выглядит так: сначала момент в формате «2026-10-21 09:15:03», потом уровень и сообщение. Задачи с логами всегда одни и те же — разобрать момент, выстроить события по времени и найти записи за интервал. Разбор строк нам уже знаком: strptime из урока 5 и сравнения из урока 7.

журнал, как он лежит на диске
from datetime import datetime

log = [
    '2026-10-21 10:02:11 INFO обработка завершена',
    '2026-10-21 09:15:03 ERROR таймаут платежного шлюза',
    '2026-10-21 09:58:40 WARNING повторная попытка оплаты',
    '2026-10-21 08:47:55 INFO сервер запущен',
    '2026-10-21 10:15:27 ERROR заказ 402 не сохранен',
]

print(len(log))
print(log[1])
Вывод
5
2026-10-21 09:15:03 ERROR таймаут платежного шлюза

Пять событий, и порядок в списке не хронологический — так и бывает в реальных журналах: строки прилетают из нескольких потоков, а при слиянии логов нескольких серверов перемешиваются гарантированно. Сообщения здесь на русском для наглядности, момент — главный, он стандартен: год-месяц-день, пробел, часы:минуты:секунды.

Срез и strptime: первые 19 символов

Момент в логе имеет фиксированную ширину: «2026-10-21 09:15:03» — это 19 символов (10 на дату, пробел, 8 на время). Значит, отделять его от сообщения можно срезом line[:19], а разобрать — шаблоном %Y-%m-%d %H:%M:%S из урока про strptime:

строка превращается в момент
from datetime import datetime

line = '2026-10-21 09:15:03 ERROR таймаут платежного шлюза'

head = line[:19]
moment = datetime.strptime(head, '%Y-%m-%d %H:%M:%S')
print(head)
print(moment)
print(repr(moment))
Вывод
2026-10-21 09:15:03
2026-10-21 09:15:03
datetime.datetime(2026, 10, 21, 9, 15, 3)

Строки head и strptime(head) печатаются одинаково — приятная сторона ISO-формата. Но это лишь текстовое совпадение: moment — настоящий datetime, в чём убеждает repr — datetime.datetime(2026, 10, 21, 9, 15, 3) с числовыми полями. У момента можно вычесть другой момент, сравнить с границей и отформатировать заново любым strftime.

Сортировка событий по времени

Чтобы выстроить журнал хронологически, разберём каждую строку и соберём пары «момент, строка». Сортировка пар упорядочит их по первому элементу — моменту:

журнал в хронологии
from datetime import datetime

log = [
    '2026-10-21 10:02:11 INFO обработка завершена',
    '2026-10-21 09:15:03 ERROR таймаут платежного шлюза',
    '2026-10-21 09:58:40 WARNING повторная попытка оплаты',
    '2026-10-21 08:47:55 INFO сервер запущен',
    '2026-10-21 10:15:27 ERROR заказ 402 не сохранен',
]

events = []
for line in log:
    moment = datetime.strptime(line[:19], '%Y-%m-%d %H:%M:%S')
    events.append((moment, line))

for moment, line in sorted(events):
    print(moment.strftime('%H:%M:%S'), line[20:])
Вывод
08:47:55 INFO сервер запущен
09:15:03 ERROR таймаут платежного шлюза
09:58:40 WARNING повторная попытка оплаты
10:02:11 INFO обработка завершена
10:15:27 ERROR заказ 402 не сохранен

События выстроились с 08:47:55 до 10:15:27. sorted(events) сравнивает кортежи: сначала моменты, а при равных моментах — строки; срезом line[20:] от хвоста строки отделяем само сообщение, чтобы не печатать дату дважды. Метод strftime('%H:%M:%S') печатает только время события — сортировку видно мгновенно.

События в интервале

Второй вечный вопрос к журналу — что происходило между двумя моментами: «что случилось с 9 до 10 утра?». Границы задаются обычным datetime, а отбор — тем же сравнением, что в уроке про сортировку дат:

час работы под микроскопом
from datetime import datetime

log = [
    '2026-10-21 10:02:11 INFO обработка завершена',
    '2026-10-21 09:15:03 ERROR таймаут платежного шлюза',
    '2026-10-21 09:58:40 WARNING повторная попытка оплаты',
    '2026-10-21 08:47:55 INFO сервер запущен',
    '2026-10-21 10:15:27 ERROR заказ 402 не сохранен',
]

events = []
for line in log:
    moment = datetime.strptime(line[:19], '%Y-%m-%d %H:%M:%S')
    events.append((moment, line))

start = datetime(2026, 10, 21, 9, 0, 0)
end = datetime(2026, 10, 21, 10, 0, 0)

for moment, line in sorted(events):
    if start <= moment <= end:
        print(line)
Вывод
2026-10-21 09:15:03 ERROR таймаут платежного шлюза
2026-10-21 09:58:40 WARNING повторная попытка оплаты

В интервале с 09:00:00 по 10:00:00 включительно оказались два события: таймаут шлюза и его повтор. Обе границы входят — сравнение нестрогое, так что событие ровно на 09:00:00 или 10:00:00 тоже попало бы в отчёт. Тайминг показателен: таймаут в 09:15, повтор в 09:58 — интервал поймал не только поломку, но и реакцию на неё.

Что дальше

Все инструменты курса на месте: даты, форматы, разницы, фильтры, таблицы и журналы. Осталось собрать из них работающий механизм — следующий урок финальный: проект трекер дедлайнов, где списки задач, сортировка по срокам и strftime сложатся в отчёт, который не стыдно отправить.

Строки сравниваются посимвольно, и логика календара им не знакома: сортируйте разобранные datetime, а не исходный текст.

Формат логов бывает разным

ISO-формат «2026-10-21 09:15:03» — удобный, но не единственный. В логах nginx время часто пишут как «21/Oct/2026:09:15:03 +0300», в syslog — как «Oct 21 09:15:03» без года, а в приложениях с микросекундами добавляется дробная часть: «2026-10-21 09:15:03.123». Шаблон меняется, а приём остаётся тем же: подобрать под формат свой срез и свой код strptime.

микросекунды в логе
from datetime import datetime

m = datetime.strptime('2026-10-21 09:15:03.123', '%Y-%m-%d %H:%M:%S.%f')

print(m.microsecond)
print(m.strftime('%H:%M:%S.%f')[:12])
Вывод
123000
09:15:03.123

Код %f читает дробную часть и заносит её в поле microsecond — 123 000 микросекунд, то есть 123 миллисекунды. Ширина среза при этом меняется: момент с микросекундами занимает 23 символа вместо 19, и line[:19] срезал бы саму дробную часть. Ещё важнее: %f требует либо ровно трёх, либо ровно шести цифр после точки. «.12» он не примет — ValueError.

часовой пояс прямо в строке
from datetime import datetime

m = datetime.strptime('2026-10-21 09:15:03+03:00', '%Y-%m-%d %H:%M:%S%z')

print(m)
print(m.utcoffset())
Вывод
2026-10-21 09:15:03+03:00
3:00:00

Код %z читает смещение прямо из строки и делает результат aware-датой — как в уроке про часовые пояса. Это дороже всего: приложение может писать логи в локальном времени сервера, и без %z вы не узнаете, в каком поясе они записаны. Если в логах есть смещение, разбирайте его обязательно — иначе сравнение событий из разных серверов будет сравнением цифр.

Счётчики: сколько чего в журнале

Когда журнал разобран, он становится обычным списком данных, и дальше работают приёмы из курса Python: счётчики, сортировка, фильтры. Первый вопрос, который задают про логи, — сколько событий каждого уровня.

сколько событий по уровням
from collections import Counter

log = [
    '2026-10-21 10:02:11 INFO обработка завершена',
    '2026-10-21 09:15:03 ERROR таймаут платежного шлюза',
    '2026-10-21 09:58:40 WARNING повторная попытка оплаты',
    '2026-10-21 08:47:55 INFO сервер запущен',
    '2026-10-21 10:15:27 ERROR заказ 402 не сохранен',
]

levels = Counter(line.split(' ', 2)[2].split(' ')[0] for line in log)

for level in sorted(levels):
    print(level, levels[level])
Вывод
ERROR 2
INFO 2
WARNING 1

Три уровня, два ERROR и два INFO. Разбор уровня сделан через split с ограничением: split(' ', 2) режет строку максимум на три части, поэтому третья часть — это «уровень и сообщение» целиком, а не первое слово сообщения. Ещё одно слово из неё и получаем уровень. Счётчик Counter из стандартной библиотеки не нужен в pandas, но для обычного списка строк он короче любого цикла со словарём.

сколько событий в каждый час
from datetime import datetime
from collections import Counter

log = [
    '2026-10-21 09:15:03 ERROR таймаут платежного шлюза',
    '2026-10-21 09:58:40 WARNING повторная попытка оплаты',
    '2026-10-21 10:02:11 INFO обработка завершена',
]

by_hour = Counter(datetime.strptime(l[:13], '%Y-%m-%d %H') for l in log)

for hour in sorted(by_hour):
    print(hour.strftime('%H:00'), by_hour[hour])
Вывод
09:00 2
10:00 1

Здесь ключом счётчика стал уже не уровень, а час — и посчитали мы события, а не строки. Срез [:13] отрезает «2026-10-21 09», и шаблон '%Y-%m-%d %H' разбирает ровно это. Получилось два события в девятом часу и одно в десятом: инцидент начался в 09:15, повтор был в 09:58, а в 10:02 обработка уже завершилась. Такой разрез — первый шаг к разбору инцидентов по границам времени.

От первого до последнего

Вторая классическая задача — оценить длительность. Разница между двумя моментами даёт timedelta, а деление на минуты превращает её в число, которое можно показать в отчёте.

сколько минут длился сбой
from datetime import datetime

log = [
    '2026-10-21 10:02:11 INFO обработка завершена',
    '2026-10-21 09:15:03 ERROR таймаут платежного шлюза',
    '2026-10-21 08:47:55 INFO сервер запущен',
]

moments = [datetime.strptime(l[:19], '%Y-%m-%d %H:%M:%S') for l in log]

print((max(moments) - min(moments)).seconds // 60)
print(min(moments).strftime('%H:%M:%S'))
print(max(moments).strftime('%H:%M:%S'))
Вывод
74
08:47:55
10:02:11

Между первым и последним событием прошло 74 минуты — столько длился рабочий отрезок журнала. Функции max и min по списку моментов не требуют сортировки: они просто находят крайние. Обратите внимание на seconds, а не на days: у timedelta в пределах суток поле days равно нулю, и все минуты лежат в секундах.

Границы интервала: включать или нет

Отбор за интервал мы уже делали сравнением start <= moment <= end, и обе границы входят. Иногда это не то, что нужно: событие ровно в 09:00:00 может принадлежать предыдущему отрезку, а не текущему. Для отчётных окон чаще удобнее полуоткрытый интервал — слева включительно, справа исключительно.

события на самой границе
from datetime import datetime

log = [
    '2026-10-21 09:00:00 INFO ровно в начало',
    '2026-10-21 09:30:00 ERROR внутри',
    '2026-10-21 10:00:00 INFO ровно в конец',
]

start = datetime(2026, 10, 21, 9, 0, 0)
end = datetime(2026, 10, 21, 10, 0, 0)

for line in log:
    moment = datetime.strptime(line[:19], '%Y-%m-%d %H:%M:%S')
    if start <= moment <= end:
        print(line[20:])
Вывод
INFO ровно в начало
ERROR внутри
INFO ровно в конец

Здесь в выборку попали все три события, включая оба граничных. Составьте условие как start <= moment < end — и событие ровно в 10:00:00 исчезнет из отчёта. Мы договорились о нестрогом сравнении с обеих сторон и держим это правило во всех отчётах: событие, случившееся в момент окончания окна, принадлежит этому окну, а не следующему. Смена правила на полуткрытый интервал — одна правка в одном месте, но менять её надо осознанно.

через полночь
from datetime import datetime

log = [
    '2026-10-21 23:58:00 INFO вечерняя смена',
    '2026-10-22 00:03:00 INFO уже ночь',
]

events = [(datetime.strptime(l[:19], '%Y-%m-%d %H:%M:%S'), l) for l in log]

for moment, line in sorted(events):
    print(moment.strftime('%d.%m %H:%M:%S'), line[20:])
Вывод
21.10 23:58:00 INFO вечерняя смена
22.10 00:03:00 INFO уже ночь

Сортировка разложила события по дате, и видно, что полночь прошла между ними. Заметьте и другое: шаблон '%d.%m %H:%M:%S' в strftime печатает и дату, и время — одним вызовом, без склейки строк. В реальном отчёте по логам это удобно: одна колонка с полным моментом читается лучше, чем две отдельные.

ЗадачаПриёмРезультат
Разобрать моментdatetime.strptime(line[:19], '%Y-%m-%d %H:%M:%S')объект datetime
Отделить сообщениеline[20:]текст без даты
Разделить по частямline.split(' ', 2)дата, время, остаток
Отсортировать хронологическиsorted(events)пары (момент, строка)
Отобрать интервалstart <= moment <= endсобытия окна
Посчитать по уровнямCounter(...)ERROR 2, INFO 2, WARNING 1
Посчитать по часамCounter(strptime(l[:13], '%Y-%m-%d %H'))09:00 — 2, 10:00 — 1

Семь строк, которые покрывают разбор логов целиком. Порядок работы всегда один: сначала разобрать все строки в список пар, и только потом сортировать, фильтровать и считать. Разбор — самая дорогая часть, потому что strptime внутри работает с разбором строк посимвольно; сортировка моментов стоит в десятки раз меньше. Поэтому список пар строят один раз, а дальше крутят его сколько нужно.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

from datetime import datetime

line = '2026-10-21 10:15:27 ERROR заказ 402 не сохранен'
moment = datetime.strptime(line[:19], '%Y-%m-%d %H:%M:%S')
print(moment.strftime('%H:%M'))
from datetime import datetime

a = datetime(2026, 10, 21, 9, 15, 3)
b = datetime(2026, 10, 21, 9, 58, 40)
print(b - a)
print('21.10.2026' < '09.11.2026')
from datetime import datetime

m = datetime.strptime('2026-10-21 09:15:03.123', '%Y-%m-%d %H:%M:%S.%f')
print(m.microsecond)
from datetime import datetime

log = [
    '2026-10-21 10:02:11 INFO a',
    '2026-10-21 09:15:03 ERROR b',
    '2026-10-21 08:47:55 INFO c',
]
moments = [datetime.strptime(l[:19], '%Y-%m-%d %H:%M:%S') for l in log]
print((max(moments) - min(moments)).seconds // 60)
Проверь себя
0 / 9

1. Почему момент из строки лога достают срезом line[:19]?

2. Какой шаблон разбирает строку '2026-10-21 09:15:03'?

3. Почему для сортировки лога собирают пары (moment, line), а не сортируют строки напрямую?

4. Событие ровно в 10:00:00 попадёт ли в выборку с условием start <= moment <= end, где end = 10:00:00?

5. Что напечатает print(repr(moment)) после разбора '2026-10-21 09:15:03'?

6. Какой шаблон разберёт момент с микросекундами «2026-10-21 09:15:03.123»?

7. Что делает код %z в шаблоне strptime?

8. Что даст line.split(' ', 2) на строке «2026-10-21 09:15:03 ERROR таймаут платежного шлюза»?

9. Почему список пар (момент, строка) строят один раз, а не разбирают журнал заново для каждого отчёта?

Карточки терминов
Запомнено: 0 / 11
Практика

Журнал из урока уже разобран в список пар (момент, строка). Выведите три строки: количество событий с уровнем ERROR, самое раннее событие дня целиком и число минут между первым ERROR (09:15:03) и последним ERROR (10:15:27).

practice.py
Вопросы и ответы по уроку

Как разобрать дату из строки лога в Python?

Отделите момент срезом фиксированной ширины и разберите strptime: datetime.strptime(line[:19], '%Y-%m-%d %H:%M:%S') для строк вида 2026-10-21 09:15:03. Шаблон обязан совпадать со строкой побуквенно, включая ведущие нули; при другом формате лога соберите свой шаблон из кодов %Y, %m, %d, %H, %M, %S.

Урок 19: даты в логах

Как отсортировать лог по времени?

Соберите пары (момент, строка), разобрав каждую строку через strptime, и отсортируйте: sorted(events) упорядочит кортежи по первому элементу — моменту. Сортировать сырые строки допустимо только для фиксированного ISO-формата; для форматов вида «21.10.2026» строковая сортировка даёт неверный порядок.

Как выбрать события из лога за период?

Задайте границы обычными datetime и отфильтруйте разобранные события сравнением: if start <= moment <= end. Сравнение нестрогое, поэтому события ровно на границах попадают в интервал. Для нескольких интервалов удобно составить список границ и проверять moment в цикле.

Почему strptime падает с ValueError на строке из лога?

Строка не совпала с шаблоном побуквенно: пропал ведущий ноль, изменился разделитель или в моменте оказались миллисекунды. strptime не ищет дату в тексте, а сверяет строку с шаблоном целиком. Сверьте формат лога с шаблоном и шириной среза; для разных форматов нужен свой шаблон на каждый.

Как разобрать в логе момент с микросекундами?

Шаблоном '%Y-%m-%d %H:%M:%S.%f': код %f читает дробную часть и кладёт её в поле microsecond — 123000 для «.123». Но учтите, что момент с микросекундами занимает 23 символа, и срез line[:19] срежет саму дробь. Код %f требует ровно трёх или шести цифр после точки.

Урок 19: даты в логах

Как посчитать, сколько событий каждого уровня в логе?

Разберите уровень из третьей части строки и соберите счётчик: Counter(line.split(' ', 2)[2].split(' ')[0] for line in log). Для журнала из пяти строк получится ERROR 2, INFO 2, WARNING 1. Второй аргумент split ограничивает число разрезов, поэтому сообщение остаётся целым в третьей части.

Как посчитать, сколько минут длился сбой по журналу?

Разберите моменты всех строк, возьмите min и max, вычтите и разделите: (max(moments) - min(moments)).seconds // 60. Помните, что у timedelta поле seconds хранит секунды после вычета дней — для интервала меньше суток это и есть вся длительность, а для более длинного нужен total_seconds().

Что делать, если в логе встречается смещение часового пояса?

Разбирать его кодом %z в шаблоне: datetime.strptime('2026-10-21 09:15:03+03:00', '%Y-%m-%d %H:%M:%S%z') даст aware-дату с utcoffset() = 3:00:00. Без %z вы не узнаете, в каком поясе записан лог, и сравнение событий с разных серверов превратится в сравнение цифр на часах.

Понравился урок? Сошлитесь на него

«Строки сравниваются посимвольно, и логика календара им не знакома: сортируйте разобранные datetime, а не исходный текст.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.