Регулярное выражение для даты в форматах DD.MM.YYYY и YYYY-MM-DD

2218
Регулярное выражение для даты в форматах DD.MM.YYYY и YYYY-MM-DD

Регулярное выражение для даты решает две разные задачи. При поиске regex извлекает из текста последовательности вроде 27.07.2026, 27/07/2026 и 2026-07-27. При строгой проверке шаблон должен подтвердить, что вся строка соответствует выбранному формату и не содержит лишних символов.

Даже строгий regex не заменяет календарную проверку. Выражение легко отсеет 99.99.9999, но обычный шаблон с диапазонами пропустит 31.02.2026. Я разделяю работу на два этапа. Регулярное выражение проверяет структуру, а библиотека дат подтверждает, что такой день действительно существует.

Regex для поиска даты в тексте

Для формата 27.07.2026 подойдет простой шаблон.

b[0-9]{2}.[0-9]{2}.[0-9]{4}b

Конструкция [0-9]{2} требует ровно две цифры. Экранированная последовательность . означает обычную точку. Неэкранированная точка в большинстве популярных движков совпадает почти с любым символом, поэтому шаблон [0-9]{2}.[0-9]{2}.[0-9]{4} найдет не только дату, но и строку 27x07x2026.

Границы b помогают не захватывать дату внутри длинной последовательности цифр. Поскольку движки по-разному определяют словесные символы, в Python для контроля именно соседних цифр можно записать более точный вариант.

(?<![0-9])[0-9]{2}.[0-9]{2}.[0-9]{4}(?![0-9])

Проверки слева и справа запрещают соседние цифры. Поддержка lookbehind зависит от движка, поэтому перед переносом выражения в другой язык нужно свериться со справочником по синтаксису regex.

Для трех распространенных форматов можно перечислить варианты через оператор |.

b(?:[0-9]{2}.[0-9]{2}.[0-9]{4}|[0-9]{2}/[0-9]{2}/[0-9]{4}|[0-9]{4}-[0-9]{2}-[0-9]{2})b

Группа (?:...) объединяет альтернативы без сохранения результата.

Короткая запись с классом разделителей выглядит привлекательнее.

b[0-9]{2}[./-][0-9]{2}[./-][0-9]{4}b

У сокращения есть дефект. Regex принимает смешанные варианты вроде 27.07/2026. Чтобы потребовать одинаковый разделитель, первый символ нужно запомнить, а затем повторить обратной ссылкой.

b[0-9]{2}([./-])[0-9]{2}1[0-9]{4}b

Группа сохраняет первый разделитель, а 1 требует повторить его. В большом Python-коде я предпочитаю именованные группы, поскольку их смысл не зависит от номера.

Поиск формы и строгая проверка даты

Шаблон [0-9]{2}.[0-9]{2}.[0-9]{4} проверяет только форму. Для него 27.07.2026 и 99.99.9999 устроены одинаково. В обеих строках стоят две цифры, точка, две цифры, точка и четыре цифры.

Уровень проверки Что подтверждает Что может пропустить
Поиск по форме Количество цифр и положение разделителей 99.99.9999
Строгий regex Всю строку, диапазоны дня и месяца, допустимый год 31.02.2026
Календарный парсер Существование даты с учетом месяца и високосного года Ошибки бизнес-правил, например слишком раннюю дату рождения

Диапазон дня от 01 до 31 записывается так.

(?:0[1-9]|[12][0-9]|3[01])

Три ветки покрывают дни от 01 до 31. Диапазон месяца от 01 до 12 короче.

(?:0[1-9]|1[0-2])

Строгий шаблон для DD.MM.YYYY можно собрать так.

^(?:0[1-9]|[12][0-9]|3[01]).(?:0[1-9]|1[0-2]).(?:19|20)[0-9]{2}$

Выражение отклонит нулевой день, месяц 13 и 99.99.9999. Ограничение (?:19|20) задает прикладной диапазон 1900-2099, а не правило календаря. Для архивов диапазон нужно изменить.

Для YYYY-MM-DD компоненты меняются местами.

^(?:19|20)[0-9]{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$

Для проверки нескольких форматов можно объединить обе схемы.

^(?:(?:0[1-9]|[12][0-9]|3[01])([./-])(?:0[1-9]|1[0-2])1(?:19|20)[0-9]{2}|(?:19|20)[0-9]{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01]))$

Шаблон принимает 27.07.2026, 27/07/2026, 27-07-2026 и 2026-07-27. Строка 27.07/2026 не пройдет из-за обратной ссылки.

В программном коде я не полагаюсь только на ^ и $. Многострочный режим меняет поведение якорей, а $ в некоторых движках совпадает перед завершающим переводом строки. В Python надежнее вызвать re.fullmatch().

Почему календарь нельзя надежно заменить одним regex

Диапазоны дня и месяца еще не связывают их между собой. Апрель, июнь, сентябрь и ноябрь содержат не больше 30 дней. Февраль содержит 28 или 29 дней. Поэтому строгий шаблон выше примет 31.04.2026, 31.02.2026 и 29.02.2025.

Правило високосного года тоже сложнее формулы «делится на четыре». Год, кратный 100, не считается високосным, если одновременно не делится на 400. Поэтому 29.02.1900 не существует, а 29.02.2000 существует.

Все ограничения можно зашить в одно большое выражение, но шаблон придется делить по месяцам, отдельно описывать февраль и кодировать високосные годы. Такой regex трудно читать, тестировать и менять.

Запись 07/08/2026 структурно корректна, но может означать 7 августа или 8 июля. Regex не угадает региональный порядок. Приложение должно выбрать формат заранее или требовать однозначную запись YYYY-MM-DD.

Класс d тоже не полностью переносим. В JavaScript d эквивалентен [0-9], а Python для строк Unicode по умолчанию включает другие десятичные цифры Unicode. PCRE2 меняет поведение при включении Unicode properties. Когда формат разрешает только символы от 0 до 9, явный класс [0-9] точнее выражает требование.

Проверка даты кодом на Python

Код удаляет пробелы по краям, проверяет структуру через regex, затем вызывает datetime.strptime(). Согласно документации Python, метод выбрасывает ValueError, если строка не соответствует формату или компоненты не образуют допустимую дату.

import re
 from datetime import datetime
 
 DATE_SHAPE = re.compile(
     r"(?:(?:0[1-9]|[12][0-9]|3[01])(?P<sep>[./-])"
     r"(?:0[1-9]|1[0-2])(?P=sep)(?:19|20)[0-9]{2}"
     r"|(?:19|20)[0-9]{2}-(?:0[1-9]|1[0-2])-"
     r"(?:0[1-9]|[12][0-9]|3[01]))"
 )
 
 DATE_FORMATS = (
     "%d.%m.%Y",
     "%d/%m/%Y",
     "%d-%m-%Y",
     "%Y-%m-%d",
 )
 
 def is_valid_date(value):
     value = value.strip()
 
     if DATE_SHAPE.fullmatch(value) is None:
         return False
 
     for date_format in DATE_FORMATS:
         try:
             datetime.strptime(value, date_format)
             return True
         except ValueError:
             continue
 
     return False
 
 
 print(is_valid_date("27.07.2026"))  # True
 print(is_valid_date("2026-07-27"))  # True
 print(is_valid_date("31.02.2026"))  # False
 print(is_valid_date("29.02.2024"))  # True
 print(is_valid_date("29.02.2025"))  # False
 print(is_valid_date("29.02.1900"))  # False
 print(is_valid_date("29.02.2000"))  # True

Именованная группа sep запрещает смешанные разделители. fullmatch() отсекает лишний текст, а strptime() проверяет календарь.

Для поиска внутри большого текста нужно сначала получить кандидатов, а затем прогнать каждый результат через ту же функцию.

CANDIDATE = re.compile(
     r"(?<![0-9])(?:"
     r"[0-9]{2}([./-])[0-9]{2}1[0-9]{4}"
     r"|[0-9]{4}-[0-9]{2}-[0-9]{2}"
     r")(?![0-9])"
 )
 
 text = """
 Договор подписан 27.07.2026. 
 В документе случайно указали 31.02.2026. 
 Следующая проверка пройдет 2026-08-15. 
 """
 
 valid_dates = [
     match.group(0)
     for match in CANDIDATE.finditer(text)
     if is_valid_date(match.group(0))
 ]
 
 print(valid_dates)
 # ['27.07.2026', '2026-08-15']

Поисковый regex извлекает кандидатов, а парсер отбрасывает невозможные даты.

После календарной проверки нужны правила продукта. Бронирование может запрещать прошедшие даты, форма рождения будущие, бухгалтерская система закрытый период. Такие ограничения проверяет отдельный код.

Какое регулярное выражение найти дату 27.07.2026?

Для простого поиска подойдет b[0-9]{2}.[0-9]{2}.[0-9]{4}b. Шаблон проверяет расположение цифр и точек, но не существование даты в календаре.

Почему regex пропускает 99.99.9999?

Шаблон [0-9]{2}.[0-9]{2}.[0-9]{4} контролирует только количество символов. Для ограничения дня и месяца нужны отдельные альтернативы с диапазонами.

Как разрешить точки, слеши и дефисы?

Класс [./-] разрешает три разделителя. Чтобы не принимать запись 27.07/2026, сохраните первый разделитель в группе и повторите его обратной ссылкой.

Можно ли проверить 31.02.2026 только регулярным выражением?

Можно написать очень длинный regex с отдельными правилами для месяцев и високосных лет, но такой код трудно поддерживать. Надежнее проверить формат выражением, а календарную корректность библиотекой дат.

Чем поиск даты отличается от проверки даты?

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

Для практического проекта я не пытаюсь превратить regex в календарь. Компактный шаблон ищет дату, более строгий проверяет форму и диапазоны, а стандартная библиотека подтверждает существование дня. Затем отдельный код применяет правила продукта. Такое разделение дает понятный результат, упрощает тесты и снижает риск принять невозможную дату.

Alt text
Обращаем внимание, что все материалы в этом блоге представляют личное мнение их авторов. Редакция SecurityLab.ru не несет ответственности за точность, полноту и достоверность опубликованных данных. Вся информация предоставлена «как есть» и может не соответствовать официальной позиции компании.
17.09
11:00
Вебинар SECURITM
Как за 10 шагов превратить формальность в реальное управление рисками?
17 сентября эксперт SECURITM объяснит, где заканчивается модель угроз и начинается риск-менеджмент — и почему выбирать между ними не нужно.
Регистрируйтесь!
Реклама. 18+ ООО «Секъюритм» ИНН 7820074059

Рекламодатель
ООО «СерчИнформ»
ИНН: 7704306397
searchinform.ru↗
ИИ-ассистент СерчИнформ