Группы в регулярных выражениях и оператор или: как работают скобки, захват и обратные ссылки

1085
Группы в регулярных выражениях и оператор или: как работают скобки, захват и обратные ссылки

Одна лишняя пара скобок способна полностью изменить регулярное выражение. Например, шаблон http|https:// выглядит так, будто ищет два протокола, но на самом деле первая ветка заканчивается после http. Поэтому строка http://example.com даст совпадение только с http, а не со всем началом адреса.

Группы в регулярных выражениях и оператор или

После базового синтаксиса я считаю группы самым полезным следующим шагом в изучении regex. Круглые скобки позволяют обращаться с несколькими символами как с единым блоком, выбирать варианты через |, делать целые фрагменты необязательными, повторять их и сохранять найденный текст. Главное не путать две роли скобок. Они одновременно группируют шаблон и, если используются как (...), создают захватывающую группу.

Зачем regex нужны круглые скобки

Начну с адресов. Допустим, в тексте встречаются оба протокола.

http://example.com
 https://example.com

Можно написать

(http|https)://

Оператор | означает выбор между альтернативами. Regex сначала пробует левую ветку, затем следующую, если первая не подходит. Сам оператор имеет очень низкий приоритет, поэтому без скобок выражение

http|https://

фактически состоит из двух самостоятельных вариантов.

http
 
 или
 
 https://

Такое поведение легко пропустить, потому что глазами человек обычно читает шаблон иначе.

Для двух протоколов есть более аккуратная запись.

https?://

Квантификатор ? относится только к букве s и означает «ноль или один раз». Поэтому подходят http:// и https://.

Скобки становятся действительно нужны, когда необязательным должен стать целый фрагмент. Например, адрес может содержать www..

https?://(?:www.)?example.com

Группа (?:www.)? означает, что весь фрагмент www. может встретиться один раз или отсутствовать.

Сравним с ошибочным вариантом.

https?://www.?example.com

Здесь ? управляет только точкой. Буквы www остаются обязательными.

Квантификатор можно применять и к повторяющемуся фрагменту.

(?:ab){3}

Regex ищет

ababab

а не три буквы b. Скобки превратили последовательность ab в единый элемент, к которому применяется {3}.

Тот же приём пригодится для идентификаторов.

[A-Z]{2}(?:-d{3}){2}

Под шаблон подходит строка

AB-123-456

Группа -d{3} повторяется целиком два раза.

Есть ещё одна особенность оператора |, которая регулярно удивляет новичков. В распространённых движках с поиском слева направо порядок альтернатив имеет значение.

cat|catalog

На строке catalog первая ветка уже успешно совпадает с cat. Regex не обязан выбирать самую длинную альтернативу. Если нужен полный вариант, порядок лучше изменить.

catalog|cat

Подробное поведение групп, альтернатив и обратных ссылок хорошо описывает справочник MDN. Базовые конструкции похожи во многих современных движках, но конкретный синтаксис ссылок и замены может различаться.

Захватывающие группы и ссылки на найденный текст

Обычные круглые скобки (...) не только объединяют выражение. Regex запоминает подстроку, которая попала внутрь. Такой блок называют захватывающей группой.

Возьму дату.

11.08.2026

Шаблон можно разбить на три группы.

(d{2}).(d{2}).(d{4})
Группа Результат
1 11
2 08
3 2026

В JavaScript, Python и многих других движках обычные захватывающие группы нумеруются по расположению открывающих скобок слева направо. Нулевая группа в программных API обычно обозначает всё совпадение целиком, поэтому первая пара скобок получает номер 1.

Теперь найденные части можно переставлять. В JavaScript, .NET и многих текстовых редакторах поле замены поддерживает форму вроде

$3-$2-$1

и дата

11.08.2026

превращается в

2026-08-11

Здесь есть принципиальная деталь. $1 не является универсальным способом обращения к группе во всех контекстах.

Внутри самого регулярного выражения числовая обратная ссылка обычно выглядит так.

1

В строке замены JavaScript и ряда редакторов часто используется

$1

Python применяет другой синтаксис. Например, в re.sub() можно писать 1, но для сложных замен безопаснее и понятнее форма g<1>. Различия подробно описывает документация Python.

Поэтому я никогда не переношу строку замены между редактором, JavaScript, Python и другим инструментом вслепую. Сам regex может работать одинаково, а ссылки в поле замены окажутся другими.

Если скобки нужны только для структуры, лишний захват лучше не создавать. Для такого случая существует незахватывающая группа.

(?:...)

Сравним.

(http|https)://

Протокол сохраняется как отдельная группа.

(?:http|https)://

Альтернатива работает точно так же, но отдельного результата для http или https уже нет.

Незахватывающие группы уменьшают путаницу с номерами. Представим выражение

(d{2})-(d{2})-(d{4})

День находится в группе 1, месяц в группе 2, год в группе 3. Если позднее добавить в начало обычную группу

(date|created): (d{2})-(d{2})-(d{4})

все прежние номера съедут. День станет группой 2, месяц группой 3, год группой 4.

Если значение date|created не нужно сохранять, безопаснее написать

(?:date|created): (d{2})-(d{2})-(d{4})

Нумерация полезных групп останется прежней.

В больших выражениях ещё удобнее именованные группы. Здесь универсального синтаксиса уже нет. JavaScript использует конструкцию вроде (?<day>d{2}), Python традиционно использует (?P<day>d{2}). Имена снижают риск ошибок, когда шаблон меняют и номера групп сдвигаются.

Практика с URL и датами

Теперь соберу несколько конструкций в реальные шаблоны.

Для учебной задачи с адресами достаточно такого выражения.

b(https?)://([^/s]+)(?:/[^s]*)?

Для строки

Откройте https://example.com/articles/regex

полное совпадение содержит весь адрес, первая группа содержит протокол

https

а вторая группа содержит хост

example.com

Путь сгруппирован через (?:...), потому что отдельно сохранять его мне в данном примере не требуется.

Не стоит превращать такой учебный шаблон в «валидатор URL». Реальные URL допускают гораздо больше вариантов, включая порты, IPv6, кодирование символов, параметры, фрагменты, международные доменные имена и другие конструкции. Для разбора URL в программе лучше использовать штатный URL-парсер языка или платформы, а regex оставить для поиска адресоподобных фрагментов в контролируемом тексте.

С датами интереснее. Пусть документ содержит три формы.

11.08.2026
 12/09/2026
 13-10-2026

Самый очевидный шаблон выглядит так.

(d{2})[./-](d{2})[./-](d{4})

Символьный класс [./-] разрешает точку, слеш или дефис. Точку внутри такого класса экранировать не требуется. Дефис я поставил в конец, чтобы regex воспринимал его как обычный символ, а не как обозначение диапазона.

Проблема проявляется на странной строке.

11.08/2026

Шаблон тоже её примет, потому что оба разделителя проверяются независимо.

Если разделители должны совпадать, первый можно захватить и потребовать повторить его.

b(d{2})([./-])(d{2})2(d{4})b

Здесь группы распределены так.

  • (d{2}) день, группа 1
  • ([./-]) первый разделитель, группа 2
  • (d{2}) месяц, группа 3
  • 2 тот же текст, который совпал с группой 2
  • (d{4}) год, группа 4

Поэтому regex принимает

11.08.2026
 11/08/2026
 11-08-2026

но отвергает

11.08/2026

Именно здесь хорошо видна разница между группой и обратной ссылкой. Конструкция ([./-]) запоминает символ. Конструкция 2 не означает «ещё один символ из списка». Она требует повторить конкретный символ, который уже нашла группа 2.

Для унификации даты в среде с заменой через $1 можно использовать

$4-$3-$1

и получить

2026-08-11
 2026-09-12
 2026-10-13

Regex при этом проверяет только форму. Значение

99.99.2026

тоже соответствует конструкции d{2}. Можно построить гораздо более сложное выражение с диапазонами дней и месяцев, но я редко считаю такой подход оправданным. Если нужна настоящая проверка календарной даты, лучше сначала извлечь части регулярным выражением, а затем передать их библиотеке работы с датами.

Два неочевидных поведения групп

Первое связано с повторением захватывающей группы. Допустим, группа совпадает несколько раз.

(d{2}-)+

На строке

12-34-56-

нельзя рассчитывать, что обычный результат группы 1 превратится в список 12-, 34- и 56-. В JavaScript и Python доступное значение такой группы соответствует последнему успешному повторению, то есть 56-. Некоторые движки дают дополнительные API для истории захватов, но переносимого правила «повторил группу и получил массив» нет.

Поэтому для извлечения каждого элемента я либо запускаю отдельный глобальный поиск, либо перестраиваю шаблон и разбираю совпадение после поиска.

Вторая ловушка связана с альтернативами. Regex не ищет «самый умный» вариант среди веток.

a|ab

На строке abc распространённый движок с последовательным перебором альтернатив сначала успешно находит a. До ветки ab дело уже не доходит, если остальная часть выражения не заставляет движок вернуться назад.

Поэтому в альтернативе я обычно ставлю более специфичные варианты раньше.

https|http

Хотя для конкретного примера с протоколами запись https? всё равно проще.

Что означают круглые скобки в regex?

Круглые скобки объединяют несколько элементов регулярного выражения в единый фрагмент. Обычная конструкция (...) также создаёт захватывающую группу и сохраняет совпавший текст.

Как работает оператор или в регулярных выражениях?

Оператор | разделяет альтернативы. Выражение cat|dog ищет cat или dog. Если альтернатива входит в более крупный фрагмент, её обычно заключают в скобки.

Чем захватывающая группа отличается от незахватывающей?

Конструкция (...) сохраняет совпавшую подстроку. Конструкция (?:...) только группирует шаблон и не создаёт отдельный захваченный результат.

Что означает 1 в regex?

1 обычно означает обратную ссылку на текст первой захватывающей группы внутри самого регулярного выражения. 2 ссылается на вторую группу и так далее.

Что означает $1 при замене текста?

Во многих редакторах, JavaScript и .NET запись $1 в строке замены вставляет содержимое первой захватывающей группы. Python использует другой синтаксис, поэтому формат замены нужно проверять для конкретного инструмента.

Как сделать целую часть regex необязательной?

Фрагмент нужно объединить в группу и поставить после неё ?. Например, (?:www.)? означает, что вся последовательность www. может присутствовать или отсутствовать.

Как найти даты с разными разделителями?

Шаблон b(d{2})([./-])(d{2})2(d{4})b принимает даты с точкой, слешем или дефисом и за счёт 2 требует одинаковый разделитель между всеми частями.

После групп regex перестаёт быть набором загадочных символов и становится небольшим конструктором. Скобки собирают элементы в блоки, | задаёт альтернативы, квантификаторы управляют целыми блоками, захватывающие группы сохраняют нужные части, а обратные ссылки позволяют использовать уже найденный текст ещё раз.

Для повседневной работы я бы сначала закрепил пять конструкций (...), (?:...), |, ? после группы и 1. С таким набором уже можно уверенно разбирать даты, URL, идентификаторы и другие структурированные строки. Следующий шаг в серии логично посвятить поиску и замене регулярными выражениями, потому что именно там захваченные группы превращаются из синтаксической особенности в инструмент массового редактирования.

Alt text
Обращаем внимание, что все материалы в этом блоге представляют личное мнение их авторов. Редакция SecurityLab.ru не несет ответственности за точность, полноту и достоверность опубликованных данных. Вся информация предоставлена «как есть» и может не соответствовать официальной позиции компании.
QUANTUM
Сегодня в канале
В Пекине бар наливает пиво и бесплатный ИИ
IBM собирает квантовый компьютер из «холодильников»
Обычный день в «Изобретая будущее»
Подписаться
Реклама

Реклама 18+
Рекламодатель
Реклама. АО «Позитив Текнолоджиз». ИНН 7718668887. 18+
Сайт рекламодателя: ptsecurity.com
Реклама