Даты в логах: разбор реального формата
Журнал из строк вида «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)
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. Почему список пар (момент, строка) строят один раз, а не разбирают журнал заново для каждого отчёта?
Журнал из урока уже разобран в список пар (момент, строка). Выведите три строки: количество событий с уровнем ERROR, самое раннее событие дня целиком и число минут между первым ERROR (09:15:03) и последним ERROR (10:15:27).
Как разобрать дату из строки лога в 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-канал или свой блог — так о проекте узнают новые читатели.
Что дальше
Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.
Что читать дальше
Datetime · Урок 18
Даты в Pandas: to_datetime
Столбец дат приезжает из выгрузки строками — pd.to_datetime делает его настоящими датами, после чего работают dt.year, dt.month, фильтры по периоду и сортировка по времени.
Datetime · Урок 20
Проект: трекер дедлайнов
Финальный проект раздела: список задач со сроками, сортировка по дедлайну, дни до срока, просроченные дела и отчёт строками со strftime — всё, что накопили девятнадцать уроков.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
openpyxl · Урок 9
Даты в Excel через openpyxl: запись, формат и арифметика
Пишем в ячейку настоящий datetime, показываем его по-русски маской DD.MM.YYYY и считаем дни между датами — а заодно узнаём, почему дату из файла нельзя сравнивать со строкой.
openpyxl датыopenpyxl дата в ячейке
Строки · Урок 20
Проект: анализатор текстового отчёта
Финальный проект курса: настоящий текстовый отчёт магазина, полный ловушек из уроков 19 — пробелы, регистр, пустые строки. Собираем анализатор, который чистит, разбирает и выводит итоги.
проект строки pythonpython разбор текста
Datetime · Урок 5
strptime: строка превращается в дату
Лог, форма и таблица присылают даты строками — для вычислений нужен объект. Учим strptime читать строку по шаблону и отличать настоящий формат от почти настоящего.
strptime pythonpython strptime примеры