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

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

Начать обучение
Урок 5 из 12 Средний 40 мин 120 XP

Группы в регулярных выражениях: скобки, 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')]
findall и три случая с группами
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. Для него важен только их количество, поэтому шаблон с тремя именованными группами вернёт те же кортежи, что и без имён.

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))
Проверь себя
0 / 5

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"?

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

В редакторе — прайс из двух строк формата «товар=Имя; цена=N; остаток=M». Извлеки из каждой строки данные именованными группами, преврати цену и остаток в числа и выведи название товара и стоимость остатка (цена умножить на остаток). Формат вывода — из заготовки.

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

Что возвращает 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

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

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