Регулярные выражения: группировка и обратная связь
Группировка с обратной связью
У нас есть группа символов, из которой мы выбираем либо ta
, либо tu
:
/(ta|tu)/
ta
-tu
ta
-ta
tu
-tu
Предположим, что мы хотим найти только те подстроки, в которых левая и правая части совпадают: ta-ta
и tu-tu
. Попробуем дополнить наше выражение еще одним условием «или» и увидим, что реализовать задуманное не удалось:
/(ta|tu)-(ta|tu)/
ta-tu
ta-ta
tu-tu
Здесь поможет группировка с обратной связью. Чтобы сделать её, мы используем специальное обозначение \1
. Оно указывает, что символы из первой группы нужно подставить вместо \1
.
Таким образом, совпадают подстроки с одинаковыми левыми и правыми частями:
/(ta|tu)-\1/
ta-tu
ta-ta
tu-tu
По умолчанию все созданные группы символов маркируются символами от \1
до \9
. Если бы мы использовали квантификацию, то это не повлияло бы на результат. Дело в том, что квантификация не участвует в обратной связи — берется только первое вхождение:
/(ta|tu)+-\1/
ta-tu
ta-ta
tu-tu
tuta-tu
Именованные группы
Если вы используете несколько групп, то не очень удобно запоминать их по номерам. Гораздо проще пользоваться именами. Для этого нужно добавить ?<имя>
после открытия скобки:
/(?<group_name>ta|tu)-\k<group_name>/
ta-tu
ta-ta
tu-tu
Теперь нам удобнее работать с группой в своем коде — можно ссылаться на группу по имени group_name
.
Символ k
используется для обратной ссылки на именованную группу. В данном случае, \k<group_name>
означает обратную ссылку на группу с именем group_name
. Это означает, что после символа - должно быть то же самое значение, что и в группе group_name
. Следует заметить, что невозможно использование одновременно и нумерованных, и именованных групп - используйте что-то одно.
Группировка без обратной связи
Мы можем отключить обратную связь, поставив ?:
внутри нашей группы:
/(?:ta|tu)-\1/
ta-tu ta-ta tu-tu
После этого группа перестанет сохраняться - при обращении к ней возникнет ошибка, потому что такой группы не существует. При таком подходе регулярное выражение становится очень сложно читать, однако оно работает быстрее.
Это вполне рабочий метод. Он особенно подходит, если вы хотите, чтобы лишние группы не занимали много места и не мешали заниматься дальнейшей группировкой.
Атомарная группировка
Еще одна интересная разновидность группировки без обратной связи — атомарная. Атомарная группировка не поддерживается некоторыми популярными языками программирования, в том числе JavaScript и Python. Тем не менее можно найти решения для их эмуляции на имеющихся конструкциях. В Ruby атомарная группировка прекрасно работает.
Для атомарной группировки вместо :
используется символ >
:
/a(?>bc|b|x)cc/
abcc
axcc
aacc
Когда мы добавляем символы атомарной группировки ?>
, происходит следующее:
- Сначала находится символ
a
- Затем —
bc
- Затем — идет поиск
cc
Давайте разберемся, как она работает. Если мы уберем символы ?>
, то регулярное выражение находит три подстроки — abcc
, abccc axcc
и :
/a(bc|b|x)cc/
abcc
abccc
axcc
Рассмотрим подробнее этот пример. В обычном случае поиск откатился бы до a
и продолжил бы проверку с b
, потому что стоит символ альтернативы |
. После этого мы бы дошли до cc
— и проверка бы сработала.
При атомарной группировке возврат по строке до символа a
отключается. Происходит дальнейшее движение по альтернативам bc
-> b
-> x
, а после x
— сопоставление cc
.
Когда найдено первое совпадение из атомарной группы (?>bc|b|x
), другие варианты из этой группы не рассматриваются. Дальше идет поиск со следующего символа из анализируемой строки с первого символа регулярного выражения.
Мы могли бы найти совпадение с подстрокой с атомарной группировкой, только если бы добавили к abcc
еще один символ c
:
/a(?>bc|b|x)cc/
abccc
axcc
abcc
На данном примере видно, что атомарная группировка отличается от поиска альтернатив - в случае с последовательностью abcc
будет найдено совпадение с bc
, далее поиск не продолжится - в итоге для последовательности a+bc
не будет найдено дальнейшего совпадения, т.к. последний символ в нашем случае c
.
Использование группировок в регулярных выражениях Ruby
Регулярные выражения в Ruby являются частью самого языка программирования, используются, например, в поиске и замене подстрок в строках. Ссылки на группы указываются точно так же, как и в самих регулярных выражениях:
str = "ta-tu tu-tu ta-ta" p str.gsub(/(ta|tu)-\1/, '[symmetry found: \1]') # => "ta-tu [symmetry found: tu] [symmetry found: ta]" p str.gsub(/(ta|tu)-\1\s+(ta|tu)-\2/, '[double symmetry found: \1, \2]') # => "ta-tu [double symmetry found: tu, ta]" p str.gsub(/(?<gn>ta|tu)-\k<gn>/, '[gn symmetry found: \k<gn>]') #=> "ta-tu [gn symmetry found: tu] [gn symmetry found: ta]" p str.gsub(/(?<gn>ta|tu)-\k<gn>\s+(?<gn2>ta|tu)-\k<gn2>/, '[gn double symmetry found: \k<gn>, \k<gn2>]') # => "ta-tu [gn double symmetry found: tu, ta]" # одновременное использование нумерованных и именованных групп - недопустимо # numbered backref/call is not allowed. (use name): /(?<gn>ta|tu)-\k<gn>\s+(ta|tu)-\1/ (SyntaxError) p str.gsub(/(?<gn>ta|tu)-\k<gn>\s+(ta|tu)-\1/, '[gn double symmetry found: \k<gn>, \1]') # использование группировки для замены p str.gsub(/(ta)/, '[sub: \1]') # => "[sub: ta]-tu tu-tu [sub: ta]-[sub: ta]" p str.gsub(/(?<g>tu)/, '[sub: \k<g>]') # => "ta-[sub: tu] [sub: tu]-[sub: tu] ta-ta"