Группы в регулярных выражениях: скобки, findall и имена (?P<name>)
Скобки в шаблоне вырезают из находки нужный кусок, а findall с несколькими группами возвращает кортежи — самый неожиданный поворот модуля re. Разбираем группы по номерам, по именам и без захвата.
Редакция Питоники
До сих пор ты доставал из находки всё сразу: m.group() возвращал весь совпавший кусок целиком. Но в реальных задачах совпадение редко нужно целиком. Из строки «Заказ №1234 от 20.09.2026» тебе нужен только номер заказа, из лога — IP-адрес, из даты — год и месяц по отдельности. Целиком совпадение — балласт, а ценность в кусочках.
Для этого в регулярных выражениях есть группы: круглые скобки внутри шаблона. Одна пара скобок — один извлекаемый кусок. В этом уроке ты научишься нумеровать группы, доставать их по одной и пачкой, обнаружишь главную ловушку модуля re — поведение findall с группами — и познакомишься с именованными группами, которые превращают находку в словарь.
Что делает скобка в шаблоне?
Скобки не находят новый текст — они вырезают часть того, что шаблон уже нашёл. Шаблон №(\d+) находит ровно то же самое, что и №\d+: решётку и все цифры после неё. Но наличие скобок меняет то, что возвращает объект Match: находка делится на «всё совпадение» и «то, что попало в скобки».
import re
text = "Заказ №1234 от 20.09.2026, сумма 4500 руб."
m = re.search(r"№(\d+)", text)
print(m.group(0)) # вся находка
print(m.group(1)) # только то, что в скобках
print(m.groups()) # кортеж всех групп
№1234
1234
('1234',)Три способа достать содержимое. m.group(0) — вся находка целиком: то же, что возвращает m.group() без аргумента. m.group(1) — содержимое первой пары скобок. m.groups() — кортеж со всеми группами сразу; здесь группа одна, поэтому кортеж из одного элемента, и запятая в ('1234',) это подчёркивает. Нумерация начинается с единицы: ноль зарезервирован за целой находкой.
Как нумеруются группы и что возвращает groups()?
Если скобок несколько, они нумеруются по порядку открывающих скобок в шаблоне: первая слева — группа 1, вторая — группа 2 и так далее. Порядок не зависит от того, что внутри скобок и в каком порядке части шаблона совпали. Разберём на дате из трёх частей.
import re
m = re.search(r"(\d{2})\.(\d{2})\.(\d{4})", "от 20.09.2026")
print(m.group(0))
print(m.group(1), m.group(2), m.group(3))
print(m.group(2))
print(m.groups())
20.09.2026
20 09 2026
09
('20', '09', '2026')Шаблон нашёл одну дату, но скобки разрезали её на три куска: день, месяц, год. Теперь каждый кусок — самостоятельная строка, с которой можно делать что угодно: превращать в число, сравнивать, складывать в словарь. Именно так парсеры достают данные из логов и платежей — и именно поэтому в финальном проекте курса группы будут в каждом шаблоне.
Скобки можно вкладывать друг в друга. Нумерация при этом считается по открывающим скобкам слева направо: внешняя скобка получает номер раньше внутренних. Это звучит абстрактно, поэтому смотри на живом примере.
import re
m = re.search(r"((\d+)-(\d+))", "ids: 12-34")
print(m.groups())
print(m.group(0))
print(m.group(1))
print(m.group(2), m.group(3))
('12-34', '12', '34')
12-34
12-34
12 34В шаблоне три открывающие скобки, поэтому групп три. Группа 1 — внешняя: она захватила весь диапазон 12-34. Группы 2 и 3 — вложенные: левая и правая половины. Заметь, что group(1) и group(0) здесь совпали по содержанию: внешняя скобка охватывает весь шаблон. Вкладывать группы без нужды не стоит — нумерация быстро становится головоломкой, и читатель шаблона запутается, что где лежит.
Что возвращает findall, если есть группы?
Самая коварная тема урока. re.findall возвращает не объекты Match, а список находок — но его содержимое зависит от числа групп в шаблоне. Три случая, и лучше увидеть их на одной строке рядом.
| Шаблон | Что вернёт findall | Пример результата |
|---|---|---|
| без групп | список найденных подстрок | ['912', '345'] |
| одна группа | список содержимого группы | ['912', '345'] |
| две и больше групп | список кортежей | [('912', '345', '67', '89')] |
import re
text = "телефоны: +7 912 345-67-89 и +7 921 111-22-33"
# без групп: список найденных подстрок
print(re.findall(r"\d{3}", text))
# одна группа: список содержимого группы
print(re.findall(r"(\d{3})", text))
# несколько групп: список кортежей
print(re.findall(r"(\d{3}) (\d{3})-(\d{2})-(\d{2})", text))
['912', '345', '921', '111']
['912', '345', '921', '111']
[('912', '345', '67', '89'), ('921', '111', '22', '33')]Первые два вызова вернули одинаковые списки — и это совпадение вводит в заблуждение. Без групп findall вернул найденные куски \d{3}. С одной группой он вернул содержимое этой группы — а она захватывает тот же текст, поэтому разницы не видно. Третий вызов всё расставил по местам: с несколькими группами findall возвращает список кортежей, по одному элементу на каждую группу.
Что с этим делать. Вариант первый — работать с кортежами честно: распаковывать их в цикле for day, month, year in .... Вариант второй — обернуть лишние скобки в (?:...), о которой ниже. Вариант третий — перейти на finditer с объектами Match, об этом расскажет следующий урок. Выбор зависит от того, что нужнее: кусочки по отдельности или находка целиком.
Зачем нужны именованные группы (?P<name>)?
Обращаться к группам по номерам можно, но номер ничего не говорит о содержании: m.group(2) — это месяц или день? В шаблоне с пятью-шестью группами начинаешь пересчитывать скобки на пальцах. Решение — именованные группы: синтаксис (?P<имя>шаблон) захватывает текст и одновременно даёт ему имя.
import re
line = "2026-09-20: выручка 5370"
m = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})", line)
print(m.group("year"), m.group("month"), m.group("day"))
print(m.groupdict())
print(m["day"]) # доступ как по ключу словаря
2026 09 20
{'year': '2026', 'month': '09', 'day': '20'}
20Читать такой шаблон легко вслух: «четыре цифры — это year, потом дефис, две цифры — month…». Доставать содержимое можно тремя способами: m.group("year"), короче — m["year"], и всё сразу — m.groupdict(), который возвращает словарь «имя — содержимое». Словарь удобно передавать дальше по коду: в базу, в функцию, в конструктор класса.
Одно разочарование: имена групп не меняют поведение findall. Для него важен только их количество, поэтому шаблон с тремя именованными группами вернёт те же кортежи, что и без имён.
import re
text = "2026-09-20 2025-12-01"
print(re.findall(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})", text))
[('2026', '09', '20'), ('2025', '12', '01')]Когда скобки нужны только для логики: невладеющая группа (?:...)
Скобки в регулярках делают две разные вещи: группируют части шаблона и захватывают текст. Проблема в том, что обычные скобки делают обе сразу. Возьмём чередование: шаблон http|https без скобок сломается в составе большего шаблона — чередование «съест» всё направо. Скобки его ограничивают: (http|https):// — но теперь у нас появилась лишняя группа, и findall вместо адресов вернёт одни протоколы.
import re
text = "http://site.ru https://pythonika.ru"
# скобки для логики чередования захватывают лишнее
print(re.findall(r"(http|https)://\S+", text))
# (?:...) - группирует без захвата
print(re.findall(r"(?:http|https)://\S+", text))
['http', 'https'] ['http://site.ru', 'https://pythonika.ru']
Разница в одном вопросике... вернее, в трёх символах ?: после открывающей скобки. Невладеющая группа (?:...) группирует, но не захватывает: у неё нет номера, она не попадает в groups() и не влияет на вывод findall. Используй её по умолчанию везде, где скобки нужны для структуры шаблона — чередования, применения квантификатора к целому куску вроде (?:ab)+, — и оставляй владеющие скобки только для того, что действительно хочешь достать. Кстати, квантификаторы к группам применяются так же, как к символам — это ты уже знаешь из урока про квантификаторы.
Посмотри, как всё это собирается в шаблон настоящего парсера логов. Строка веб-сервера выглядит так: IP, дата в квадратных скобках, запрос в кавычках, код ответа, размер. Шаблон с именованными группами для такой строки:
import re
LOG = (
r"(?P<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) - - "
r"\[(?P<when>[^\]]+)\] "
r'"(?P<method>\w+) (?P<path>\S+) \S+" '
r"(?P<code>\d{3}) (?P<size>\d+)"
)
line = '10.0.0.5 - - [20/Sep/2026:10:12:33] "GET /catalog/ HTTP/1.1" 200 5120'
m = re.search(LOG, line)
print(m.groupdict())
{'ip': '10.0.0.5', 'when': '20/Sep/2026:10:12:33', 'method': 'GET', 'path': '/catalog/', 'code': '200', 'size': '5120'}Читай по кускам: (?P<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) — IP из четырёх чисел по одному-три разряда; [^\]]+ — всё до закрывающей квадратной скобки (набор «всё, кроме ]», приём из урока про классы символов); \S+ — непробельный кусок пути. Каждая скобка подписана именем, и через месяц ты без пересчёта номеров поймёшь, что где лежит. Ровно этот шаблон станет ядром парсера логов в финальном проекте.
Практика: собираем данные со строки товара
Соберём урок в маленькую, но настоящую задачу. Есть прайс в виде строк «товар=Кофе; цена=340; остаток=12» — такой формат выдаёт старая учётка. Нужно превратить строки в данные: название, цену, остаток — и посчитать, на какую сумму лежит товара на складе. Именованные группы + groupdict + int: три приёма этого урока в четырёх строках.
import re
rows = [
"товар=Кофе; цена=340; остаток=12",
"товар=Чай; цена=150; остаток=31",
]
pattern = r"товар=(?P<name>\w+); цена=(?P<price>\d+); остаток=(?P<stock>\d+)"
for row in rows:
data = re.search(pattern, row).groupdict()
money = int(data["price"]) * int(data["stock"])
print(data["name"], money)
Кофе 4080 Чай 4650
Обрати внимание: groupdict() вернул все значения строками, поэтому перед умножением потребовался int() — регулярки всегда возвращают текст, числа из них доставай сам. А если бы вместо прайса была таблица в Pandas, те же группы сработали бы через str.extract и разложились бы по столбцам — этот мост между регулярками и таблицами построен в уроке про новые столбцы.
Что дальше
Теперь ты умеешь не только находить текст, но и вырезать из находки кусочки: по номеру, по имени, целиком через groups() и словарём через groupdict(). Главный вывод про findall запомни накрепко: количество групп меняет тип результата. В следующем уроке мы наведём порядок в четырёх методах поиска — match, search, findall и fullmatch, — и ты наконец перестанешь пробовать их наугад. После этого методы понадобятся в деле: замена и разрезание текста строятся на группах из этого урока.
Группа — это мост между «текст подходит под шаблон» и «данные извлечены»: поиск превратился в парсинг.
Сначала предскажи ответ в голове — это главный навык программиста.
import re
text = "цена: 340 руб"
print(re.findall(r"(\d+)", text))
import re
m = re.search(r"(\w+)@(\w+)\.(\w+)", "mail: ivan@mail.ru")
print(m.group(0), m.group(2))
import re
text = "12-34 56-78"
print(re.findall(r"(\d+)-(\d+)", text))
1. Что вернёт re.findall(r"(\d+)-(\d+)", "12-34 56-78")?
2. Что возвращает m.group(0) у объекта Match?
3. Сколько групп и в каком порядке получится у шаблона r"((\d+)-(\d+))"?
4. Что делает невладеющая группа (?:...)?
5. Что вернёт m.groupdict() после поиска шаблоном r"(?P<a>\d)(?P<b>\d)" по строке "78"?
В редакторе — прайс из двух строк формата «товар=Имя; цена=N; остаток=M». Извлеки из каждой строки данные именованными группами, преврати цену и остаток в числа и выведи название товара и стоимость остатка (цена умножить на остаток). Формат вывода — из заготовки.
Что возвращает re.findall, если в шаблоне есть группы?
Это зависит от количества групп. Без групп findall возвращает список найденных подстрок; с одной группой — список содержимого этой группы; с двумя и больше — список кортежей, по элементу на каждую группу. Имена групп на формат вывода не влияют: findall с (?P<year>) вернёт те же кортежи, что и с безымянными скобками.
Чем group(1) отличается от group(0)?
group(0) — вся найденная подстрока целиком, как и m.group() без аргумента. group(1) — содержимое первой пары скобок шаблона, group(2) — второй и так далее. Вложенные скобки нумеруются по порядку открывающих: внешняя группа получает номер раньше внутренних.
Зачем нужна невладеющая группа (?:...) в Python?
Обычные скобки группируют шаблон и захватывают текст одновременно. Когда захват не нужен — например, скобки стоят ради чередования (?:http|https) или квантификатора (?:ab)+ — невладеющая группа ограничивает логику, не добавляя нумерованную группу. Это спасает вывод re.findall, который при лишних группах начинает возвращать кортежи вместо строк.
Как превратить совпадение регулярки в словарь Python?
Именовать группы синтаксисом (?P<имя>шаблон) и вызвать m.groupdict(): он вернёт словарь «имя — содержимое». Все значения будут строками, поэтому числа приводи через int(). А если данные лежат в таблице Pandas, те же группы разложатся по столбцам через str.extract — удобно для чистки колонок.
Понравился урок? Сошлитесь на него
«Скобки не находят новый текст — они вырезают часть того, что шаблон уже нашёл.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
re · Урок 4
Жадные и ленивые квантификаторы: почему regex съедает лишнее
Жадный .* тянет от первого совпадения до последнего и съедает полстраницы HTML. Ленивый .*? останавливается у ближайшего ограничителя: два знака разницы — совсем другой результат.
re · Урок 6
re.match, re.search, re.findall и re.fullmatch: как выбрать метод
Четыре метода поиска на одной и той же строке дают четыре разных результата. Разбираем, где match останавливается, зачем нужен fullmatch и когда компилировать шаблон.
Pandas · Урок 8
Новые столбцы в Pandas: apply, map и векторные операции над колонками
Добавляем в DataFrame выручку, скидки и категории: векторные операции, np.where, map и apply — и разбираемся, почему apply по строкам почти всегда лишний.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
sqlite3 · Урок 6
Агрегаты и GROUP BY в SQL: COUNT, SUM, AVG и HAVING
Считаем, суммируем и усредняем по всей таблице и по группам: COUNT, SUM, AVG, GROUP BY и фильтр групп HAVING.
sql group by countgroup by примеры
pytest · Урок 8
Параметризация: @pytest.mark.parametrize
Десять одинаковых тестов для десяти случаев — это десять копипаст. parametrize сжимает их в один тест и список кортежей, а pytest разворачивает обратно в десять отчётных строк.
pytest кортежи параметров
re · Урок 3
Квантификаторы в регулярных выражениях: *, +, ? и {n,m}
Сколько раз повторить символ: ноль, один, много или ровно четыре? Четыре квантификатора, диапазоны повторений и две ловушки, из-за которых регулярка находит пустоту.
квантификаторы регулярных выраженийрегулярные выражения python