Группы в строках Python — Захватывающие группы в Regex
Как работают захватывающие группы в Python regex: group(), groups(), именованные группы, незахватывающие группы и обратные ссылки.
Когда модуль re в Python находит совпадение в строке, он возвращает объект совпадения. Объект совпадения — это не просто ответ «да/нет»: он запоминает каждый подшаблон, обёрнутый в круглые скобки, — такие подшаблоны называются захватывающими группами. Методы .group() и .groups() позволяют извлечь эти захваченные фрагменты.
В этой главе рассмотрено всё необходимое для уверенной работы с группами: нумерованные группы, именованные группы, незахватывающие группы, обратные ссылки в подстановках и типичные ошибки.
Что такое захватывающая группа?
Захватывающая группа — это часть шаблона регулярного выражения, заключённая в обычные круглые скобки (...). При совпадении шаблона Python отдельно сохраняет текст, соответствующий каждой паре скобок, в дополнение к сохранению всего совпадения целиком.
import re
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2024-03-15')
print(m.group(0)) # 2024-03-15 — the whole match
print(m.group(1)) # 2024 — first group
print(m.group(2)) # 03 — second group
print(m.group(3)) # 15 — third groupГруппы нумеруются слева направо начиная с 1, в порядке появления открывающей скобки. group(0) (или просто group() без аргумента) всегда возвращает всё совпадение целиком.
Метод .group()
.group(n) возвращает текст, захваченный n-й группой. Можно передать несколько индексов сразу — тогда метод вернёт кортеж результатов.
import re
m = re.search(r'(\w+)@(\w+)\.(\w+)', '[email protected]')
# Individual groups
print(m.group(1)) # alice
print(m.group(2)) # example
print(m.group(3)) # com
# Multiple at once
print(m.group(1, 3)) # ('alice', 'com')Если группа присутствует в шаблоне, но не участвовала в совпадении (например, находилась внутри необязательной секции), group(n) возвращает None.
import re
# group(1) is optional — it may not match
m = re.search(r'(\d+)?-(\d+)', 'abc-456')
print(m.group(1)) # None (the optional \d+ did not match)
print(m.group(2)) # 456Метод .groups()
.groups() возвращает кортеж, содержащий текст, захваченный каждой группой, по порядку. Это удобный способ получить все группы сразу.
import re
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2024-03-15')
print(m.groups()) # ('2024', '03', '15')Значение по умолчанию для незафиксированных групп
Если группа не участвовала в совпадении, в кортеже она представлена как None. Можно передать аргумент default, чтобы заменить такие значения None на что-то более удобное.
import re
m = re.search(r'(\d+)?-(\d+)', 'abc-456')
print(m.groups()) # (None, '456')
print(m.groups(default='0')) # ('0', '456')Именованные группы с (?P<name>...)
В сложных шаблонах нумерованные группы сложно отслеживать. Именованные группы позволяют присвоить каждой группе понятную метку с помощью синтаксиса (?P<name>...), а затем обращаться к значению по имени, а не по позиции.
import re
pattern = r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'
m = re.search(pattern, '2024-03-15')
print(m.group('year')) # 2024
print(m.group('month')) # 03
print(m.group('day')) # 15Именованные группы по-прежнему имеют номер, поэтому к ним можно обращаться любым из двух способов.
Метод .groupdict()
При наличии именованных групп метод .groupdict() возвращает словарь, в котором каждому имени сопоставлен захваченный текст. Это удобно, когда нужно распаковать совпадение в структурированный объект.
import re
m = re.search(
r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})',
'2024-03-15'
)
print(m.groupdict())
# {'year': '2024', 'month': '03', 'day': '15'}Результат можно сразу использовать:
date_parts = m.groupdict()
print(f"{date_parts['day']}/{date_parts['month']}/{date_parts['year']}")
# 15/03/2024Незахватывающие группы с (?:...)
Иногда нужно сгруппировать часть шаблона, чтобы применить квантификатор или чередование, но не хочется, чтобы эта группа попадала в результаты совпадения. Для этого используется (?:...) — незахватывающая группа.
import re
# Without non-capturing group: both parts appear in groups()
m1 = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2024-03-15')
print(m1.groups()) # ('2024', '03', '15')
# Year grouped but not captured — month and day are groups 1 and 2
m2 = re.search(r'(?:\d{4})-(\d{2})-(\d{2})', '2024-03-15')
print(m2.groups()) # ('03', '15')Незахватывающие группы — это рекомендуемая практика, когда конкретное подсовпадение не нужно: они сохраняют чистоту индексов групп и не тратят лишнюю память.
Вложенные группы
Группы могут быть вложены друг в друга. Нумерация всегда определяется по позиции открывающей скобки, считая слева направо.
import re
# ((\d{4})-(\d{2}))-(\d{2})
# group 1 = the outer (year-month pair)
# group 2 = year
# group 3 = month
# group 4 = day
m = re.search(r'((\d{4})-(\d{2}))-(\d{2})', '2024-03-15')
print(m.group(1)) # 2024-03
print(m.group(2)) # 2024
print(m.group(3)) # 03
print(m.group(4)) # 15
print(m.groups()) # ('2024-03', '2024', '03', '15')Группы с re.findall()
Если в шаблоне, переданном в re.findall(), содержится ровно одна захватывающая группа, функция возвращает список строк — по одной на каждое совпадение.
Если захватывающих групп две и более, она возвращает список кортежей.
import re
# One group → list of strings
emails = '[email protected], [email protected]'
usernames = re.findall(r'(\w+)@\w+\.\w+', emails)
print(usernames) # ['alice', 'bob']
# Two groups → list of tuples
pairs = re.findall(r'(\w+)@(\w+)\.com', emails)
print(pairs) # [('alice', 'gmail'), ('bob', 'yahoo')]Группы с re.finditer()
re.finditer() возвращает итератор объектов совпадений, поэтому на каждом из них можно вызывать .group() и .groups() по отдельности. Это наиболее гибкий подход, когда нужны полные детали каждого совпадения.
import re
text = '[email protected], [email protected]'
for m in re.finditer(r'(?P<user>\w+)@(?P<domain>\w+)\.com', text):
print(m.group('user'), '->', m.group('domain'))
# alice -> gmail
# bob -> yahooОбратные ссылки в re.sub()
Захватывающие группы также используются в обратных ссылках в re.sub(). В строке замены \1, \2, … ссылаются на текст, захваченный группой 1, группой 2 и т. д. Для именованных групп используется \g<name>.
import re
# Swap first and last name
result = re.sub(r'(\w+)\s(\w+)', r'\2 \1', 'John Smith')
print(result) # Smith John
# Same using named groups
result2 = re.sub(
r'(?P<first>\w+)\s(?P<last>\w+)',
r'\g<last> \g<first>',
'Jane Doe'
)
print(result2) # Doe JaneПозиция группы: .start(), .end(), .span()
Объекты совпадений предоставляют позицию каждой группы, а не только её текст. Передайте номер (или имя) группы в .start(), .end() или .span().
import re
m = re.search(r'(\d{4})-(\d{2})', '2024-03')
print(m.span(1)) # (0, 4) — year occupies indices 0..3
print(m.start(2)) # 5 — month starts at index 5
print(m.end(2)) # 7 — month ends before index 7Краткий справочник
| Метод | Возвращает | Примечания |
|---|---|---|
m.group() или m.group(0) | Всё совпадение целиком | Всегда доступен |
m.group(n) | Текст группы n | None, если группа не совпала |
m.group(n, m, ...) | Кортеж групп | Несколько индексов за один вызов |
m.groups() | Кортеж всех групп | Необязательный default= для несовпавших |
m.groupdict() | Словарь именованных групп | Содержит только именованные группы |
m.start(n) / m.end(n) | Начальный / конечный индекс группы n | Передайте 0 для всего совпадения |
m.span(n) | Кортеж (start, end) | Сокращение для обоих значений |
Типичные ошибки
Вызов .group() без проверки на None. re.search() возвращает None, если совпадений нет, поэтому прямой вызов .group() на таком результате вызовет AttributeError. Всегда проверяйте результат:
import re
m = re.search(r'(\d+)', 'no digits here')
if m:
print(m.group(1))
else:
print('no match')
# no digits hereПутаница между .group() и .groups(). .group(1) возвращает string; .groups() всегда возвращает кортеж. Их смешение приводит к трудноуловимым ошибкам типов.
Забывать, что re.findall() меняет форму результата в зависимости от групп. Без групп функция возвращает плоский список строк; с группами — список кортежей. Добавление или удаление захватывающей группы может незаметно сломать код, обрабатывающий результат.
Когда использовать каждый подход
- Используйте нумерованные группы для простых коротких шаблонов с небольшим числом групп.
- Используйте именованные группы, если шаблон сложный или результат будет использоваться в нескольких строках кода — имена служат самодокументацией.
- Используйте незахватывающие группы
(?:...), когда группировка нужна только для применения квантификатора или чередования, а не для извлечения значения. - Используйте
.groups(), чтобы сразу распаковать все захваченные значения в кортеж. - Используйте
.groupdict(), когда все группы именованы и нужен словарь.
Для общего обзора модуля re и синтаксиса шаблонов смотрите главу Python RegEx. Для операций со строками, не требующих регулярных выражений, смотрите Modify Strings и Python String Methods.