Квантификаторы в регулярных выражениях: *, +, ? и {n,m}
Сколько раз повторить символ: ноль, один, много или ровно четыре? Четыре квантификатора, диапазоны повторений и две ловушки, из-за которых регулярка находит пустоту.
Редакция Питоники
Шаблон \d\d.\d\d.\d\d\d\d из первого урока работает, но скажем честно: писать \d четыре раза подряд — путь к опечаткам. Для телефонных номеров, где цифры сгруппированы по три и по четыре, он и вовсе не годится. Нужен способ сказать «повтори» — и в регулярках их четыре: звёздочка, плюс, вопрос и фигурные скобки.
Квантификатор — знак, который ставится после элемента шаблона и говорит, сколько раз тот может повториться. Это второй по важности инструмент регулярки после символьных классов: почти каждый шаблон — это «класс + квантификатор».
| Квантификатор | Сколько раз | Читается как |
|---|---|---|
| * | ноль или больше | «может и не быть, а может быть много» |
| + | один или больше | «обязательно хотя бы один» |
| ? | ноль или один | «возможно, один раз» |
| {n} | ровно n | «ровно столько» |
| {n,} | n и больше | «как минимум n» |
| {n,m} | от n до m | «не меньше n и не больше m» |
Все шесть строк таблицы описывают одно и то же — количество повторов, поэтому выбор сводится к привычке. В живом коде приживаются три: плюс для «хотя бы один», вопрос для необязательного куска и фигурные скобки для точных диапазонов. Звёздочка остаётся для редких случаев, когда отсутствие элемента — честный ответ, и её мы ещё поругаем ниже.
Что делают квантификаторы * и + в регулярках?
Разница между звёздочкой и плюс-ом ровно одна: что делать, когда символа нет. Плюс требует хотя бы один повтор, звёздочка согласна и на пустоту. Из-за этого \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
Ошибка поднимается сразу при компиляции шаблона, ещё до встречи с текстом, — так что перепутанные границы не умеют тихо портить данные, за что спасибо. В старых статьях вы встретите имя 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 из урока про флаги, но не пробелами внутри диапазонов.
Как выглядят шаблоны из боевого кода?
Вот многострочный шаблон из настоящего парсера логов, показанный как есть. Не пытайтесь понять каждую строчку: заметьте лишь, что почти всё в нём — знакомые вам классы с квантификаторами.
LOG_PATTERN = r"""
^(?P<ip>\d{1,3}(?:\.\d{1,3}){3}) # адрес клиента: 4 числа через точки
\s+\S+\s+\S+\s+ # служебные поля
\[(?P<time>[^\]]+)\] # время в квадратных скобках
\s+"(?P<request>[^"]*)" # запрос в кавычках
"""
Практически каждая строка этого шаблона — то, что вы уже знаете: \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"))
1. Сколько раз может повториться символ с квантификатором +?
2. Что вернёт re.findall(r"\d{3}", "12 345 6789")?
3. К чему применяется квантификатор в шаблоне ab*?
4. Чем ? отличается от *?
5. Какой шаблон найдёт слово «файл» с необязательной цифрой и расширением .txt?
В строке чека перечислены покупки с ценами вида «250 руб». Соберите findall-ом все цены вместе со словом «руб», посчитайте итог циклом и выведите список находок и строку «Итого: ...».
Что означает * в регулярных выражениях?
Ноль или больше повторений предыдущего элемента. Из-за нуля звёздочка умеет совпадать с пустотой — в находках появляются пустые строки. Если символ обязателен, пишите + (один и больше), а точный минимум задавайте фигурными скобками: {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-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
re · Урок 2
Символы и классы символов в регулярках: \d, \w, \s и свои наборы
Цифры, буквы, пробелы и свои наборы в квадратных скобках: полный словарь символьных классов, точка и её аппетиты, экранирование точки в ценах и датах.
re · Урок 4
Жадные и ленивые квантификаторы: почему regex съедает лишнее
Жадный .* тянет от первого совпадения до последнего и съедает полстраницы HTML. Ленивый .*? останавливается у ближайшего ограничителя: два знака разницы — совсем другой результат.
re · Урок 5
Группы в регулярных выражениях: скобки, findall и имена (?P<name>)
Скобки в шаблоне вырезают из находки нужный кусок, а findall с несколькими группами возвращает кортежи — самый неожиданный поворот модуля re. Разбираем группы по номерам, по именам и без захвата.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
openpyxl · Урок 12
Диаграммы в Excel через openpyxl: BarChart и LineChart
Собираем настоящие диаграммы Excel из кода: BarChart по продажам недели и LineChart по выручке — Reference, add_data, категории, якорь и размеры в сантиметрах.
Reference openpyxl диапазон
BeautifulSoup / Scrapy · Урок 3
Поиск элементов: find, find_all и CSS-селекторы в BeautifulSoup
Два метода find и find_all плюс семейство select закрывают большинство задач поиска. Разбираем их отличия, ловушки с class_ и attrs и применяем к каталогу товаров.
beautifulsoup find find_allfind и find_all в beautifulsoup разница
re · Урок 10
Флаги в регулярных выражениях: re.IGNORECASE, MULTILINE, DOTALL, VERBOSE
Четыре флага, которые меняют поведение целого движка: поиск в любом регистре, якоря на каждой строке, точка с переводом строки и шаблоны с комментариями вместо «полотна».
флаги регулярных выражений pythonмногострочный режим regex