
Навигация по странице
Что такое регулярные выражения
Представьте себе ситуацию, в которой вам присылают текстовый документ, где нужно найти определённые повторяющиеся слова. Допустим, в этом документе ошибочно была написана не та почта: вместо [email protected] везде указана [email protected].
Как выйти из этой ситуации и заменить почтовый адрес на нужный? Конечно, вы можете просто воспользоваться уже знакомым поиском в любом текстовом редакторе и заменить все встреченные слова.
Но что будет, если этот документ составлен на английском, в котором слово support вполне может встречаться в тексте, а не только в почтовом адресе?
Или можем рассмотреть другую ситуацию. У нас есть большой документ с данными пользователей. Из него нужно извлечь все телефонные номера и сохранить в отдельный файл.
В таких случаях логично будет составить небольшую программу, которая и реализует весь функционал. Но как вы будете обрабатывать все эти последовательности символов? Не будете же вы в тексте циклично перебирать все цифры, чтобы найти телефонные номера?
Здесь вам на помощь придут регулярные выражения!
Под регулярными выражениями понимаются определённые последовательности символов, которые задают шаблон для поиска в тексте. Такие универсальные текстовые шаблоны регулярных выражений сами по себе напоминают миниатюрный язык программирования, предназначенный для описания и разбора текста.
При грамотном использовании регулярные выражения позволяют упросить задачи обработки текста, а также обеспечить решение многих задач, которые вообще не могли бы решиться без регулярных выражений.
Вам может потребоваться несколько десятков строк программного кода, чтобы извлечь, например, адреса электронной почты из файла. При этом такой код будет сложно сопровождать и как-либо менять, в случае смены даже малейших условий поиска. Однако с применением регулярных выражений решение такой задачи может быть реализовано всего за пару строк.
Для работы с регулярными выражениями в Python используется встроенный модуль re. В этой статье мы разберем некоторые наиболее полезные функции модуля re.
Но не переживайте, мы уже готовим подробнейший курс по регулярным выражениям в Python, где будут подробно описаны все возможности модуля re и продемонстрированы различные подходы к решению задач с использованием регулярных выражений.
Прежде чем перейти к изучению функций, давайте еще раз проговорим терминологию, которая будет использована далее. Итак, строкой мы будем называть какой-либо текст, в котором будет искать подстроку (символ или несколько символов), соответствующую заданному шаблону.

В дальнейшем при рассмотрении синтаксиса мы узнаем, что множество символов используют знак «\», который в Python экранирует символы строки. Чтобы избежать ненужного экранирования, мы будем использовать «сырые строки» (это которые с символом r в начале строки).
Функции модуля re
Рассмотрим первую и самую базовую функцию для работы с регулярными выражениями – match(). Данная функция находит совпадения только, если соответствующая шаблону подстрока находится в начале строки, по которой ведется поиск.
Функция match(), как и большинство функций в модуле re, имеет следующий синтаксис:
re.match(pattern, string, flags=0)
Давайте проверим работу этой функции и выведем на экран результат, который она вернет:
import re
#Строка
string = "Роза упала на лапу"
#Шаблон
pattern = r"Роза"
#Вызов функции match()
result = re.match(pattern, string)
print(result)
>>><re.Match object; span=(0, 4), match='Роза'>
Мы видим, что функция нам возвращает match-объект, то есть результат успешного сопоставления регулярного выражения с текстом. Эти объекты содержат информацию о найденном совпадении, включая позицию в тексте, найденные группы и другие детали.
Рассмотрим некоторые методы match-объектов. Для того чтобы вывести найденную подстроку, можно воспользоваться методом group().
import re
#Строка
string = "Роза упала на лапу"
#Шаблон
pattern = r"Роза"
#Вызов функции match()
result = re.match(pattern, string)
print(result.group())
>>>Роза
Можно сказать, работа метода group() аналогична __getitem__(), так что вы можете пользоваться синтаксисом обращения к элементу последовательности через квадратные скобки, а также перебирать все группы match-объекта в цикле (при их наличии).
import re
#Строка
string = "Роза упала на лапу"
#Шаблон
pattern = r"Роза"
#Вызов функции match()
result = re.match(pattern, string)
print(result[0])
>>>Роза
Ещё одним полезным методом является span(), который возвращает кортеж из индексов начала и конца вхождения найденной подстроки в исходную строку.
import re
#Строка
string = "Роза упала на лапу"
#Шаблон
pattern = r"Роза"
#Вызов функции match()
result = re.match(pattern, string)
print(f'Индексы найденной подстроки: {result.span()}')
>>>Индексы найденной подстроки: (0, 4)
Получить отдельно индекс начала или конца вхождения можно методами start() и end(), соответственно.
import re
#Строка
string = "Роза упала на лапу"
#Шаблон
pattern = r"Роза"
#Вызов функции match()
result = re.match(pattern, string)
print(f'Индексы найденной подстроки: {result.span()}')
print(f'Начало вхождения: {result.start()}')
print(f'Конец вхождения: {result.end()}')
>>>Индексы найденной подстроки: (0, 4)
>>>Начало вхождения: 0
>>>Конец вхождения: 4
Но нам не всегда требуется найти совпадение лишь с началом строки. В таком случае можно использовать функцию search(), которая ищет совпадения с шаблоном по всему тексту, а не только по началу строки. Она находит только первое вхождение шаблона в строку!
import re
string = "Роза упала на лапу и упала"
pattern = r"упала"
result = re.search(pattern, string)
print(f'Найдена подстрока: {result.group()}')
print(f'Начало вхождения: {result.start()}')
print(f'Конец вхождения: {result.end()}')
>>>Найдена подстрока: упала
>>>Начало вхождения: 5
>>>Конец вхождения: 10
Ещё одной полезной функцией, возвращающей match-объект, является finditer(). Функция finditer() ищет все непересекающиеся совпадения регулярного выражения в строке. Она возвращает итератор, содержащий в себе match-объекты каждого найденного совпадения.
Помните, что итераторы используются для «ленивых» вычислений: каждое совпадение в строке ищется «на лету» при итерации.
Давайте в строке «ABBAACABAABABCA» найдём все непересекающиеся вхождения подстрок «AB»:
import re
string = "ABBAACABAABABCA"
pattern = r"AB"
matches = re.finditer(pattern, string)
for match in matches:
print(f'Найдено совпадение: {match.group()}')
print(f'Позиция: {match.span()}')
>>>Найдено совпадение: AB
>>>Позиция: (0, 2)
>>>Найдено совпадение: AB
>>>Позиция: (6, 8)
>>>Найдено совпадение: AB
>>>Позиция: (9, 11)
>>>Найдено совпадение: AB
>>>Позиция: (11, 13)
Визуализируем это на изображении ниже:

Некоторые функции рассматриваемого модуля не возвращают match-объект. К таким относится, например, функция findall(). Суть её работы очень похожа на функцию finditer(). Только в отличие от последней, findall() возвращает список строк или кортежей, содержащих найденные совпадения.
С этим связаны и другие особенности работы данной функции:
- Нет возможности получать информацию позиции совпадения
- С готовым списком совпадений проще работать, но его формирование занимает определённое время, в отличие от «ленивых» вычислений у finditer()
Применим эту функцию к строке из прошлого примера:
import re
string = "ABBAACABAABABCA"
pattern = r"AB"
matches = re.findall(pattern, string)
print(matches)
>>>['AB', 'AB', 'AB', 'AB']
В модуле re есть некоторые функции, работа которых схожа с методами строк в Python. Например, функция sub() позволяет осуществлять замену подстроки, соответствующую заданному шаблону, в тексте.
Функция имеет такой синтаксис:
re.sub(pattern, repl, string, count=0, flags=0)
Используемые параметры обозначают следующее:
- pattern: шаблон регулярного выражения
- repl: строка или функция для замены
- string: строка для поиска
- count: максимальное количество замен
- flags: дополнительные флаги
Для примера давайте заменим в строке ниже два первых слова «шаг» на три символа «*»:
import re
text = "шаг в настоящее, шаг в прошлое, шаг в будущее"
pattern = 'шаг'
repl = '***'
results = re.sub(pattern, repl, text, count = 2)
print(results)
>>>*** в настоящее, *** в прошлое, шаг в будущее
Во всех примерах выше мы искали подстроки, составленные из обычных символов. Но мы же раньше говорили про универсальность регулярных выражений, где же она скрывается?
Вся суть регулярных выражений скрыта в особом синтаксисе, который состоит из метасимволов, квантификаторов и скобочных групп. Давайте подробнее познакомимся с ними.
Метасимволы
Подобно строкам в Python, регулярные выражения могут содержать метасимволы «.^$*+?{}[]\|()». Также существует ряд букв, которые при экранировании знаком «\» превращаются в определённые шаблоны.
Символ «.»
Поставив точку внутри регулярного выражения, вы обозначите один любой символ, кроме символа новой строки «\n».
В примерах далее, подстрока, соответствующая регулярному выражению, будет отмечаться жирным шрифтом. Метасимвол внутри регулярного выражения также будет обозначаться жирным. Найденные подстроки будем возвращать функцией findall(), а для наглядности, функций sub() будем заменять их на «***».
Например, выражению «к.т» будут соответствовать: «кот, кит, каток, Скотт»
import re
text = "кот кит каток Скотт"
pattern = r'к.т'
matches = re.findall(pattern, text)
print(f'Найденные подстроки: {matches}')
new_text = re.sub(pattern, '***', text)
print(f'Строка с заменой: {new_text}')
>>>Найденные подстроки: ['кот', 'кит', 'кат', 'кот']
>>>Строка с заменой: *** *** ***ок С***т
Символ «\d»
Символом «\d» обозначается любая цифра (арабская, то есть от 0 до 9).
Например, шаблону «ab\d\d» будут соответствовать: «ab12, ab56», но не «bc34».
import re
text = "ab12 bc34 ab56 "
pattern = r'ab\d\d'
matches = re.findall(pattern, text)
print(f'Найденные подстроки: {matches}')
new_text = re.sub(pattern, '***', text)
print(f'Строка с заменой: {new_text}')
>>>Найденные подстроки: ['ab12', 'ab56']
>>>Строка с заменой: *** bc34 ***
Символ «\D»
Символ «\D» исключает все цифры, то есть обозначает любой символ кроме цифры.
Например, выражению «1\D2» будет соответствовать: «1!2, 121+212, ab1c2d, a1 2b».
import re
text = " 1!2 121+212 ab1c2d a1 2b"
pattern = r"1\D2"
matches = re.findall(pattern, text)
print(f"Найденные подстроки: {matches}")
new_text = re.sub(pattern, '***', text)
print(f'Строка с заменой: {new_text}')
>>>Найденные подстроки: ['1!2', '1+2', '1c2', '1 2']
>>>Строка с заменой: *** 12***12 ab***d a***b
Символ «\s»
Данный символ обозначает любой пробельный символ: пробел, табуляция, конец строки.
Например, выражению «1\s2» будет соответствовать: «1 2, a1 2b, 1\n2, 1\t2».
import re
text = "1 2 a1 2b 1\n2 1\t2"
pattern = r"1\s2"
matches = re.findall(pattern, text)
print(f"Найденные подстроки: {matches}")
new_text = re.sub(pattern, '***', text)
print(f'Строка с заменой: {new_text}')
>>>Найденные подстроки: ['1 2', '1 2', '1\n2', '1\t2']
>>>Строка с заменой: *** a***b *** ***
Символ «\S»
Этот символ, наоборот, соответствует любому не пробельному символу. Так, выражению «\S123» будет соответствовать: «abc123, 01234, !!!123».
import re
text = "abc123, 01234, !!!123"
pattern = r"\S123"
matches = re.findall(pattern, text)
print(f"Найденные подстроки: {matches}")
new_text = re.sub(pattern, '***', text)
print(f'Строка с заменой: {new_text}')
>>>Найденные подстроки: ['c123', '0123', '!123']
>>>Строка с заменой: ab***, ***4, !!***
Символ «\w»
Данный символ служит для обозначения любой буквы, цифры или символа нижнего подчёркивания.
Например, выражению «\w\w» будут соответствовать две идущие подряд буквы или цифры или знаки нижнего подчеркивания: «шаг, ask, __u, 12-, abc».
import re
text = "шаг ask __u 12- abc"
pattern = r"\w\w"
matches = re.findall(pattern, text)
print(f"Найденные подстроки: {matches}")
new_text = re.sub(pattern, '***', text)
print(f'Строка с заменой: {new_text}')
>>>Найденные подстроки: ['ша', 'as', '__', '12', 'ab']
>>>Строка с заменой: ***г, ***k, ***u, ***-, ***c
Символ «\b»
Символ «\b» позволяет указать границу слова. Если поставить его с обоих концов регулярного выражения, то это будет означать, что требуется найти только подстроку, которая содержит в себе этот шаблон, а слева и справа от неё пусто или не буква (знак препинания, нижнего подчеркивания и т.д.).
Вспомните наш пример для символа точки. Тогда вместо ожидаемого кота и кита мы еще получили каток и какого-то Скотта. Давайте ограничим шаблон «к.т» с обеих сторон символами «\b» чтобы найти только трёхбуквенные слова, первая буква которых – к, а третья – т.
import re
text = "кот кит каток Скотт"
pattern = r'\bк.т\b'
matches = re.findall(pattern, text)
print(f'Найденные подстроки: {matches}')
new_text = re.sub(pattern, '***', text)
print(f'Строка с заменой: {new_text}')
>>>Найденные подстроки: ['кот', 'кит']
>>>Строка с заменой: *** *** каток Скотт
Символ «\B»
Символ «\B», напротив, указывает не границу слова. То есть шаблон, с обеих сторон которого находится символ «\B», должен быть именно внутри слова (окружён не пробельными символами).
Например, найдём все подстроки «по», которые находятся внутри слова:
import re
text = "помощь запомни по-нашему спор"
pattern = r'\Bпо\B'
matches = re.findall(pattern, text)
print(f'Найденные подстроки: {matches}')
new_text = re.sub(pattern, '***', text)
print(f'Строка с заменой: {new_text}')
>>>Найденные подстроки: ['по', 'по']
>>>Строка с заменой: помощь за***мни по-нашему с***р
Далее рассмотрим работу диапазонов и квантификаторов. Хоть они тоже являются метасимволами, но мы выделим их в отдельный раздел для удобства.
Шаблоны с диапазонами
Рассмотренные ранее метасимволы позволяют определять любой из допустимых символов: буквы, цифры, пробельные символы и так далее. Но в синтаксисе регулярных выражений также предусмотрена возможность указывать целый диапазон символов, которые необходимо определить.
Для определения диапазона символа используются квадратные скобки. Так, например, выражение «[0-5]» позволяет определить любую цифру от 0 до 5 (0, 1, 2, 3, 4 и 5).
Представим ситуацию, когда вам необходимо найти все файлы, у которых в конце может быть несколько цифр. Например, файлы к заданию 24 могут быть пронумерованы в таком стиле: «task-24-1». То есть последнее число определяет номер файла (нумерация файлов начинается с 1), а первая часть «task-24-» – одинакова для всех.
Для того чтобы найти все файлы к 24-му заданию среди всех имён файлов, можем воспользоваться такой конструкцией:
import re
text = "task-1-1 task-24-1 task-24-2 task-2-2"
pattern = r"task-24-[1-9]"
matches = re.findall(pattern, text)
print(f'Найденные подстроки: {matches}')
new_text = re.sub(pattern, '***', text)
print(f'Строка с заменой: {new_text}')
>>>Найденные подстроки: ['task-24-1', 'task-24-2']
>>>Строка с заменой: task-1-1 *** *** task-2-2
Чтобы исключить какие-то символы из диапазона, необходимо поставить перед такими символами знак «^».
Например, в строке «A1 a2 A2 A3 A4 A5 a6 A6 A7 A8» найдём все подстроки, которые содержат только прописную букву А и любые цифры, кроме 1, 3, 5, 7:
import re
text = "A1 a2 A3 A4 A5 a6 A7 A8"
pattern = r"A[^1357]"
matches = re.findall(pattern, text)
print(f'Найденные подстроки: {matches}')
new_text = re.sub(pattern, '***', text)
print(f'Строка с заменой: {new_text}')
>>>Найденные подстроки: ['A4', 'A8']
>>>Строка с заменой: A1 a2 A3 *** A5 a6 A7 ***
Вы уже могли заметить, что некоторые метасимволы эквивалентны диапазонам. Такие метасимволы с диапазонами перечислены в таблице ниже.

Обратите внимание, что буквы ё и Ё не входят в диапазоны [А-Я] и [а-я]! Поэтому их надо отдельно включать, иначе регулярное выражение будет работать с ошибками.
Квадратные скобки помогают перечислить несколько вариантов одного символа. Если же необходимо перечислить слова, то тогда можно записать их через символ «|».
Например, когда необходимо найти разное написание одного слова. Тогда регулярное выражение «кеш|кэш» будет искать оба слова в строке:
import re
text = "Так кеширование или кэширование??"
pattern = r"кеш|кэш"
matches = re.findall(pattern, text)
print(f'Найденные подстроки: {matches}')
new_text = re.sub(pattern, '***', text)
print(f'Строка с заменой: {new_text}')
>>>Найденные подстроки: ['кеш', 'кэш']
>>>Строка с заменой: Так ***ирование или ***ирование??
Но также можно использовать символ «|» для определения нескольких вариантов одного символа, даже внутри слова. Тогда эту вариативную букву помещаем в квадратные скобки, а между различными вариантами этой буквы ставим знак «|».
Следовательно, такой код будет работать аналогично предыдущему:
import re
text = "Так кеширование или кэширование??"
pattern = r"к[е|э]ш"
matches = re.findall(pattern, text)
print(f'Найденные подстроки: {matches}')
new_text = re.sub(pattern, '***', text)
print(f'Строка с заменой: {new_text}')
>>>Найденные подстроки: ['кеш', 'кэш']
>>>Строка с заменой: Так ***ирование или ***ирование??
Квантификаторы
Квантификаторы в регулярных выражениях – это специальные символы, которые указывают, сколько раз определённый элемент (символ, группа или класс символов) должен повторяться в рассматриваемой строке.
Квантификаторы могут быть представлены как отдельными метасимволами, так и диапазонами значений внутри фигурных скобок.
Например, если требуется найти ровно 4 повторения любой цифры, то можно использовать такой шаблон: «\d{4}». По нему в строке «1 12 123 1234 12345» будут найдены две подстроки.
import re
text = "1 12 123 1234 12345"
pattern = r"\d{4}"
matches = re.findall(pattern, text)
print(f'Найденные подстроки: {matches}')
new_text = re.sub(pattern, '***', text)
print(f'Строка с заменой: {new_text}')
>>>Найденные подстроки: ['1234', '1234']
>>>Строка с заменой: 1 12 123 *** ***5
Количество символов можно указывать в виде диапазона «{m, n}», что будет значить от m до n повторений предыдущего символа включительно.
Соответственно, запись «{m,}» будет означать «не менее m повторений», а запись «{,n}» — «не более n повторений». Пробел между значениями в таких диапазонах не ставится!
Для обозначения одного или более вхождения подстроки можно использовать символ «+», что будет аналогично записи {1,}.
Например, найдем все подстроки, состоящие хотя бы из одной буквы A:
import re
text = "AA A AAA BBC"
pattern = r"A+"
matches = re.findall(pattern, text)
print(f'Найденные подстроки: {matches}')
new_text = re.sub(pattern, '***', text)
print(f'Строка с заменой: {new_text}')
>>>Найденные подстроки: ['AA', 'A', 'AAA']
>>>Строка с заменой: *** *** *** BBC
Если необходимо обозначить 0 или более вхождений, то можно использовать символ «*» (аналогично {0,}).
Например, это можно использовать, когда в конце имени файла может быть очередной номер, но у самого первого, его может и не быть, хотя этот файл все равно нам нужен:
import re
text = "test, test1, test999"
pattern = r"test\d*"
matches = re.findall(pattern, text)
print(f'Найденные подстроки: {matches}')
new_text = re.sub(pattern, '***', text)
print(f'Строка с заменой: {new_text}')
>>>Найденные подстроки: ['test', 'test1', 'test999']
>>>Строка с заменой: ***, ***, ***
Бывают ситуации, когда нужно найти подстроку, которая может входить либо 0, либо 1 раз.
Например, это может произойти при работе с окончаниями у глаголов мужского и женского рода: когда в тексте нужно найти все слова «сдал» и «сдала».
Для таких случаев можно использовать символ «?».
import re
text = "Он сдал. Она сдала"
pattern = r"сдала?"
matches = re.findall(pattern, text)
print(f'Найденные подстроки: {matches}')
new_text = re.sub(pattern, '***', text)
print(f'Строка с заменой: {new_text}')
>>>Найденные подстроки: ['сдал', 'сдала']
>>>Строка с заменой: Он ***. Она ***
По умолчанию все квантификаторы в регулярных выражениях являются жадными. Это означает, что они пытаются захватить как можно больше символов, соответствующих шаблону.
К примеру, шаблон «a.*b» ищет строку, которая начинается с a и заканчивается на b. Между этими символами может быть любое количество символов. Следовательно, в строке «axxxxxbaxxxb» такому шаблону будет соответствовать вся строка, так как это максимально возможное совпадение.
import re
text = "axxxxxbaxxxb"
pattern = r"a.*b"
matches = re.findall(pattern, text)
print(f'Найденные подстроки: {matches}')
>>>Найденные подстроки: ['axxxxxbxxxb']
Но как быть, если мы не хотим захватывать всю строку целиком? Например, в предыдущей строке нам нужны подстроки «axxxxxb» и «axxxb». Для решения этой задачи нам необходимо сделать квантификатор «*» ленивым.
Ленивые квантификаторы ищут минимально возможное совпадение. Чтобы сделать квантификатор ленивым, нужно добавить знак «?» после него.
import re
text = "axxxxxbaxxxb"
pattern = r"a.*?b"
matches = re.findall(pattern, text)
print(f'Найденные подстроки: {matches}')
>>>Найденные подстроки: ['axxxxxb', 'axxxb']
Таким образом, поставив вопросительный знак после «*» мы потребовали, чтобы регулярное выражение захватывало минимально возможное количество символов между a и b.
Из всего вышесказанного про ленивые и жадные квантификаторы можно сделать такой вывод:
- Жадные квантификаторы полезны, когда нужно захватить как можно больше данных. Например, для поиска самого длинного совпадения в тексте.
- Ленивые квантификаторы полезны, когда нужно найти минимальное совпадение. Например, для извлечения отдельных элементов (тегов, слов и т.д.).
Скобочные группы
Скобочные группы в регулярных выражениях в Python используются для группировки частей шаблона и извлечения соответствующих подстрок. Это полезно для применения квантификаторов к части выражения, захвата подстрок для дальнейшего использования и более сложного поиска.
Группы обозначаются круглыми скобками «(…)» и имеют такой же смысл, как в математических выражениях.
Например, шаблону «(\d+)-(\d+)» будут соответствовать подстроки формата «<одна и более цифра>-<одна и более цифра>».
Если в шаблоне есть группирующие скобки, то функцией findall() вместо списка найденных подстрок будет возвращён список кортежей, в каждом из которых будет строка, соответствующая каждой группе.
import re
text = "123-456 911-112"
pattern = r"(\d+)-(\d+)"
match = re.findall(pattern, text)
print(f'Все совпадения: {match}')
print(f'Первое совпадение: {match[0]}')
print(f'Первая группа: {match[0][0]}')
print(f'Вторая группа: {match[0][1]}')
print(f'Второе совпадение: {match[1]}')
print(f'Первая группа: {match[1][0]}')
print(f'Вторая группа: {match[1][1]}')
>>>Все совпадения: [('123', '456'), ('911', '112')]
>>>Первое совпадение: ('123', '456')
>>>Первая группа: 123
>>>Вторая группа: 456
>>>Второе совпадение: ('911', '112')
>>>Первая группа: 911
>>>Вторая группа: 112
Также сформированные группы можно вызвать методами group() и groups() match-объекта, например, при использовании функции search():
import re
text = "123-456 911-112"
pattern = r"(\d+)-(\d+)"
match = re.search(pattern, text)
print(f"Всё совпадение: {match.group(0)}")
print(f"Первая группа: {match.group(1)}")
print(f"Вторая группа: {match.group(2)}")
print(f"Кортеж: {match.groups()}")
>>>Всё совпадение: 123-456
>>>Первая группа: 123
>>>Вторая группа: 456
>>>Кортеж: ('123', '456')
Скобочные выражения группируют вместе части регулярного выражения, и к каждой группе может быть применен квантификатор. К примеру, составим шаблон для определения телефонного номера вида: «12-34-56». То есть у нас есть группа, где любая цифра повторяется 2 раза, далее идет дефис и снова повторяется эта группа, и так еще один раз.
Можем составить такую программу для извлечения телефонного номера из строки:
import re
text = "Тел.: 12-34-56, 78-99-00"
pattern = r"(\d{2})-(\d{2})-(\d{2})"
matches = re.finditer(pattern, text)
for match in matches:
print(f'Номер: {match.group(0)}')
>>>Номер: 12-34-56
>>>Номер: 78-99-00
Давайте подробней разберем работу с группами на следующем примере. Допустим, нам необходимо найти подстроку формата «<текст> = <число>», которую с обеих сторон может окружать любое количество пробельных символов.
Тогда шаблон будет состоять из следующих частей:
- Символы «\s*» для определения любого количества пробельных символов в начале строки.
- Первая группа «([a-zA-Z]+)», которая определяет 1 и более латинских букв подряд.
- Символ «=».
- Вторая группа «(\d+)», которая определяет 1 и более цифр.
- Символы «\s*» для определения любого количества пробельных символов в конце строки.
Напишем такую программу:
import re
text = "- value=12 -"
pattern = r'\s*([a-zA-Z]+)=(\d+)\s*'
match = re.search(pattern, text)
print(f'Найдена подстрока "{match[0]}"')
print(f'C позиции {match.start(0)} до {match.end(0)}')
print(f'Первая группа "{match[1]}"')
print(f'C позиции {match.start(1)} до {match.end(1)}')
print(f'Вторая группа "{match[2]}"')
print(f'C позиции {match.start(2)} до {match.end(2)}')
>>>Найдена подстрока " value=12 "
>>>C позиции 1 до 13
>>>Первая группа "value"
>>>C позиции 3 до 8
>>>Вторая группа "12"
>>>C позиции 9 до 11
Все захваченные группы и их индексы проиллюстрированы на изображении ниже.

Все предыдущие примеры скобочных групп были с захватом содержимого, то есть когда регулярное выражение с такой группой находит совпадение, оно сохраняет подстроку, соответствующую этой группе, что позволяет извлекать её позднее с помощью методов group() или groups().
Но бывают ситуации, когда все же нужно использовать группу для сбора части регулярного выражения, но вы не заинтересованы в извлечении содержимого группы и не планируете далее работать с методами group() и groups(). В таком случае можно использовать группы без захвата содержимого.
Синтаксис таких групп похож на синтаксис групп с захватом содержимого, отличие лишь в том, что внутри скобок, перед выражением ставятся знаки «?:».
Давайте сравним работу обеих групп. В следующем примере первая группа будет без захвата, а вторая – с захватом содержимого:
import re
text = "123-456"
pattern = r'(?:\d{3})-(\d{3})'
match = re.search(pattern, text)
print(f"Всё совпадение: {match.group(0)}")
print(f"Кортеж с группой: {match.groups()}")
print(f"Одна группа: {match.group(1)}")
>>>Всё совпадение: 123-456
>>>Кортеж с группой: ('456',)
>>>Одна группа: 456
То есть при поиске совпадения учитываются как группы с захватом содержимого, так и без. Но извлечь мы можем только группу с захватом содержимого.
Позиционные проверки
Позиционные проверки в регулярных выражениях — это специальные конструкции, которые позволяют проверять положение символов в строке, не захватывая их.
Обычно они используются в ситуациях, когда нужно проверить, что подстрока находится до или после определённого сочетания символов. Причем захватываться будет только строка, без этих проверяемых символов.
Различают опережающие и ретроспективные проверки.
Опережающая проверка (lookahead assertion) позволяет проверить, что за текущим шаблоном следует (или не следует) определённый текст, без включения этого текста в результат.
Опережающие проверки бывают положительными и отрицательными:
- Положительные опережающие проверки «(?=…)»
- Отрицательные опережающие проверки «(?!…)»
Положительные опережающие проверки проверяют, что за текущей позицией в строке следует подстрока, соответствующая шаблону внутри «(?=…)», но не захватывают её в результатах.
Для примера давайте найдём подстроку максимальной длины (выделена зелёным), после которой находятся символы «cba» (выделены оранжевым) в строке «abcabccbabaccab».

import re
text = "abcabccbabaccab"
pattern = r'\w+(?=cba)'
match = re.findall(pattern, text)
print(match)
>>>['abcabc']
В случае с отрицательными опережающими проверками всё наоборот. Они проверяются, что за текущей позицией в строке не следует подстрока, соответствующая шаблону внутри «(?!…)».
Например, выберем все буквы A, после которых не следует буква C:
import re
text = "AB AC DC"
pattern = r'A(?!C)'
match = re.findall(pattern, text)
print(match)
>>>['A']
Теперь перейдём к ретроспективным проверкам. Ретроспективная проверка (lookbehind assertion) позволяет проверить, что перед текущим шаблоном находится (или не находится) определённый текст, без включения этого текста в результат.
Так же, как и опережающие, ретроспективные проверки делятся на положительные и отрицательные.
Положительные ретроспективные проверки «(?<=…)» проверяют, что перед текущей позицией в строке существует подстрока, соответствующая шаблону внутри «(?<=…)», но не захватывают её в результатах.
Обратите внимание, что ретроспективные проверки должны иметь фиксированную длину. Шаблоны переменной длины (например, с использованием квантификаторов) в ретроспективных проверках не поддерживаются.
Давайте теперь найдём максимальную подстроку, перед которой находятся символы «cba» во все той же строке: «abcabccbabaccab»

import re
text = "abcabccbabaccab"
pattern = r'(?<=cba)\w+'
match = re.findall(pattern, text)
print(match)
>>>['baccab']
Отрицательные же ретроспективные проверки проверяют, что перед текущей позицией в строке не существует подстрока, соответствующая шаблону внутри «(?<!…)».
Немного усложним наши примеры. У нас есть строка, состоящая из пар символов: «AB AC DC BB CA AD». Нужно вывести вторую букву (выделены зелёным) только тех пар, которые не начинаются с А (выделены оранжевым).

В коде это будет выглядеть так:
import re
text = "AB AC DC BB CA AD"
pattern = r'((?<!A)\w){2}'
match = re.findall(pattern, text)
print(match)
>>>['C', 'B', 'A']
Ну и, конечно, проверки можно совмещать друг с другом. Например, в последовательности чисел найдем то, что находится после четного и перед нечётным:
import re
text = "1292894"
even = r'[2468]'
odd = r'[13579]'
pattern = rf' (?<={even})\d(?={odd})'
match = re.findall(pattern, text)
print(f"Найдено число: {match}")
new_text = re.sub(pattern, '*', text)
print(f"Cтрока с заменой: {new_text}")
>>>Найдено число: ['8']
>>>Cтрока с заменой: 1292*94