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

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

Начать обучение
Урок 3 из 12 Начальный 30 мин 120 XP

Квантификаторы в регулярных выражениях: *, +, ? и {n,m}

Сколько раз повторить символ: ноль, один, много или ровно четыре? Четыре квантификатора, диапазоны повторений и две ловушки, из-за которых регулярка находит пустоту.

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

Шаблон \d\d.\d\d.\d\d\d\d из первого урока работает, но скажем честно: писать \d четыре раза подряд — путь к опечаткам. Для телефонных номеров, где цифры сгруппированы по три и по четыре, он и вовсе не годится. Нужен способ сказать «повтори» — и в регулярках их четыре: звёздочка, плюс, вопрос и фигурные скобки.

Квантификатор — знак, который ставится после элемента шаблона и говорит, сколько раз тот может повториться. Это второй по важности инструмент регулярки после символьных классов: почти каждый шаблон — это «класс + квантификатор».

КвантификаторСколько разЧитается как
*ноль или больше«может и не быть, а может быть много»
+один или больше«обязательно хотя бы один»
?ноль или один«возможно, один раз»
{n}ровно n«ровно столько»
{n,}n и больше«как минимум n»
{n,m}от n до m«не меньше n и не больше m»

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

Что делают квантификаторы * и + в регулярках?

Разница между звёздочкой и плюс-ом ровно одна: что делать, когда символа нет. Плюс требует хотя бы один повтор, звёздочка согласна и на пустоту. Из-за этого \d+ ищет числа, а \d* — «числа или пустоту», что почти никогда не то, что нужно.

\d без квантификатора и с плюсом
import re

text = "год 2025"

print(re.findall(r"\d", text))    # по одной цифре
print(re.findall(r"\d+", text))   # склеивает подряд идущие
Вывод
['2', '0', '2', '5']
['2025']

Без квантификатора каждая цифра — отдельная находка. Плюс склеивает подряд идущие: нашёл первую цифру — тянет следующие, пока они не кончатся. Шаблон \d+ станет вашим самым частым способом найти число в тексте.

вопросительный знак: символ может отсутствовать
import re

print(re.findall(r"colou?r", "color, colour, collar"))
Вывод
['color', 'colour']

Знак вопроса означает «ноль или один»: u? — буква u может быть, а может не быть. Классический пример из книг Питера Норвига и Кернигана: colou?r ловит британское colour и американское color, но не collar — там после colo идёт l, а шаблон требует r.

Как задать точное число повторений: {n}, {n,} и {n,m}?

Звёздочка и плюс слишком вольные для задач, где повторы известны: год — ровно четыре цифры, код города — три, номер карты — шестнадцать. Фигурные скобки задают диапазон: {4} — ровно четыре, {2,} — два и больше, {2,4} — от двух до четырёх.

точный диапазон повторений
import re

text = "1234-567-89"

print(re.findall(r"\d{4}", text))     # ровно четыре цифры
print(re.findall(r"\d{2,3}", text))   # от двух до трёх
Вывод
['1234']
['123', '567', '89']

Первый шаблон вытащил только четырёхзначный кусок. Второй продемонстрировал две привычки квантификатора: жадность (в «1234» он взял максимум — три цифры, а не две) и уважение к нижней границе (одинокая «4» не подошла — минимум два). Жадность разбираем со всех сторон в следующем уроке.

дата и время в логе
import re

log = "запрос 2025-09-21 в 14:03:59"

print(re.findall(r"\d{4}-\d{2}-\d{2}", log))   # дата
print(re.findall(r"\d{2}:\d{2}:\d{2}", log))   # время с секундами
Вывод
['2025-09-21']
['14:03:59']

Комбинация «класс + фигурные скобки» — основной кирпич реальных шаблонов: \d{4}-\d{2}-\d{2} — это дата в формате ISO, \d{2}:\d{2}:\d{2} — время с секундами. Никакой магии: четыре цифры, дефис, две цифры, дефис, две цифры. Обе находки из лога выше добыты без единого цикла — шаблон сам понял, где кончается дата и начинается время. Такие конструкции вы соберёте в настоящий проект в финальном уроке про парсер логов.

  • \d{4} — год: 2025, 1999, но не 99;
  • \d{2}:\d{2} — часы и минуты: 14:03;
  • [а-яё]{3,} — слово из трёх и более кириллических букв;
  • [0-9]{6} — почтовый индекс;
  • \d{3,} — число длиной хотя бы в три цифры: коды, артикулы, суммы.

Что будет, если перепутать минимум и максимум?

В диапазоне сначала минимум, потом максимум: {2,4} читается «от двух до четырёх». Записать наоборот можно — и это не опечатка, которую питон простит:

ошибка: минимум больше максимума
import re

re.findall(r"\d{3,2}", "123")   # минимум больше максимума - ошибка
Вывод
re.PatternError: min repeat greater than max repeat at position 3
Блок не запускается намеренно: он вызывает ошибку при создании шаблона, до всякого поиска. Перед этим выводом в реальном traceback будут ещё две служебные строки — суть всегда в последней.

Ошибка поднимается сразу при компиляции шаблона, ещё до встречи с текстом, — так что перепутанные границы не умеют тихо портить данные, за что спасибо. В старых статьях вы встретите имя re.error: начиная с Python 3.13 исключение переименовано в re.PatternError, поведение то же.

Как поймать растянутое слово: повтор одной буквы?

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

повтор одной буквы внутри слова
import re

print(re.findall(r"ко+т", "кот кооот кт"))   # о повторяется от одного раза
Вывод
['кот', 'кооот']

Плюс после буквы о растягивает её от одной до бесконечности: и «кот», и «кооот» пойманы, а «кт» без единой о — нет. Тот же трюк работает с классом: [а-яё]+ — это «сколько угодно букв», то есть целое слово. Осталось выяснить, к чему именно цепляется квантификатор, когда элементов несколько.

К чему применяется квантификатор — к одному символу или к группе?

Квантификатор прикрепляется к предыдущему элементу — и только к нему. Перед ним буква — повторяется буква, класс — повторяется класс, скобки — повторяется всё, что внутри скобок. Отсюда самая коварная ловушка новичков.

звёздочка при букве и при скобках
import re

print(re.findall(r"ab*", "a ab abbb"))        # звёздочка относится к b
print(re.search(r"(ab)+", "xabab!").group())  # скобки объединяют ab в один токен
Вывод
['a', 'ab', 'abbb']
abab

Верхняя строка — звёздочка при b: находки от «a» до «abbb», но никогда два слога подряд. Нижняя — скобки сделали из ab один элемент, и плюс повторил его дважды. Скобки в регулярках умеют гораздо больше — целое владение группами ждёт в пятом уроке, здесь нам хватает их объединяющей роли.

Почему регулярка с a* находит пустые строки?

Звёздочка допускает ноль повторений — и движок честно пользуется этой лазейкой там, где буквы нет. Запустите шаблон a* по слову mama и посмотрите на вывод:

звёздочка находит и пустоту
import re

print(re.findall(r"a*", "mama"))
print(re.findall(r"a+", "mama"))
Вывод
['', 'a', '', 'a', '']
['a', 'a']

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

Как вытащить из текста только числа нужной длины?

Практическая задача: в трёх записях много чисел, а нужны группы из двух-четырёх цифр — артикулы, коды, части дат. Диапазон {2,4} отсеет одиночные цифры сам, без дополнительных проверок.

фильтр по длине числа
import re

notes = [
    "Артикул: 77-0043-B",
    "Телефон: +7 912 345-67-89",
    "Дата поставки: 03.12.2025",
]

for n in notes:
    print(re.findall(r"\d{2,4}", n))
Вывод
['77', '0043']
['912', '345', '67', '89']
['03', '12', '2025']

Обратите внимание на телефон: одинокая «7» после плюс-а не прошла нижнюю границу и пропала, а «67» и «89» — остались. Никакой дополнительной логики: диапазон повторений сам отсёк лишнее. findall здесь, как и в уроке про классы символов, просто показывает все находки сразу.

У диапазона есть ещё одна занятная особенность: пробел внутри скобок лишает их силы квантификатора. Шаблон a{2, 4} ищет не «от двух до четырёх букв a», а буквальный текст «a{2, 4}» — с фигурными скобками, запятой и пробелом.

пробел внутри скобок ломает квантификатор
import re

# пробел внутри скобок: квантификатор стал обычным текстом
print(re.findall(r"a{2, 4}", "aaa{2, 4} bbb"))
print(re.findall(r"a{2,4}", "aaa bbb"))    # без пробела всё работает
Вывод
['a{2, 4}']
['aaa']

Ошибка при этом не поднимается: шаблон компилируется и честно ищет фигурные скобки в тексте — просто находит их никогда. Если шаблон длинный и хочется отформатировать его красиво, переносите строки и добавляйте комментарии флагом re.VERBOSE из урока про флаги, но не пробелами внутри диапазонов.

Как выглядят шаблоны из боевого кода?

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

шаблон строки доступа веб-сервера (Common Log Format)
LOG_PATTERN = r"""
    ^(?P<ip>\d{1,3}(?:\.\d{1,3}){3})    # адрес клиента: 4 числа через точки
    \s+\S+\s+\S+\s+                     # служебные поля
    \[(?P<time>[^\]]+)\]                # время в квадратных скобках
    \s+"(?P<request>[^"]*)"             # запрос в кавычках
"""
Этот шаблон можно запустить — он показан как есть, без обвязки: он настоящий и почти без изменений вернётся в проекте двенадцатого урока. Скобки со знаком вопроса (?:...) и имена (?P<name>) — материал пятого урока о группах; сегодня достаточно увидеть, что страшные шаблоны собраны из маленьких понятных кирпичей.

Практически каждая строка этого шаблона — то, что вы уже знаете: \d{1,3} — от одной до трёх цифр, \s+ — пробелы, [^\]]+ — всё до закрывающей скобки. Останутся непонятными только скобочные конструкции — и они разбираются в следующем же уроке про группы.

Что дальше

Вы умеете повторять элементы ровно столько раз, сколько нужно: * + ? и диапазоны в фигурных скобках. Остался последний штрих к портрету квантификатора — его характер: по умолчанию он жадный и забирает максимум, из-за чего регулярки «съедают лишнее». Почему так происходит и как включить ленивый режим — четвёртый урок. После него логично идти к группам и четырём методам поиска.

Квантификатор прикрепляется к предыдущему элементу: в шаблоне ab* звёздочка повторяет букву b, а не сочетание ab.

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

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

import re

print(re.findall(r"\d{2,3}", "1 22 333 4444"))
import re

print(re.findall(r"ab*", "a ab abbb"))
import re

print(re.findall(r"colou?r", "color colour colouur"))
Проверь себя
0 / 5

1. Сколько раз может повториться символ с квантификатором +?

2. Что вернёт re.findall(r"\d{3}", "12 345 6789")?

3. К чему применяется квантификатор в шаблоне ab*?

4. Чем ? отличается от *?

5. Какой шаблон найдёт слово «файл» с необязательной цифрой и расширением .txt?

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

В строке чека перечислены покупки с ценами вида «250 руб». Соберите findall-ом все цены вместе со словом «руб», посчитайте итог циклом и выведите список находок и строку «Итого: ...».

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

Что означает * в регулярных выражениях?

Ноль или больше повторений предыдущего элемента. Из-за нуля звёздочка умеет совпадать с пустотой — в находках появляются пустые строки. Если символ обязателен, пишите + (один и больше), а точный минимум задавайте фигурными скобками: {2,}.

Чем ? отличается от * в регулярке?

Оба допускают отсутствие символа, но вопросительный знак ограничен одним повтором: colou?r поймает color и colour, а colou*r — ещё и colouur со сколькими угодно u. Знак вопроса также включает ленивый режим, но это отдельная роль — о ней в уроке 4.

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

Фигурными скобками: {4} — ровно четыре, {2,} — два и больше, {2,4} — от двух до четырёх. Классика: \d{4} находит год, \d{2}:\d{2} — часы и минуты. Диапазон со спасённой жадностью берёт максимум — об этом следующий урок.

Почему регулярка со звёздочкой находит пустые строки?

Звёздочка разрешает ноль повторений, и движок честно отмечает совпадение там, где символа нет. В выводе findall это пустые строки в списке. Лечение — заменить * на + или задать нижнюю границу: {1,}. Пустая находка почти всегда означает, что квантификатор слишком щедрый.

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

«Квантификатор прикрепляется к предыдущему элементу: в шаблоне ab* звёздочка повторяет букву b, а не сочетание ab.»

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

TelegramVK

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

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