Возможно ли совместить вложенные скобки с регулярным выражением без использования рекурсивных или балансировочных групп?

StackOverflow поощряет ответы на свои вопросы, поэтому я решил создать этот пост, чтобы поделиться тем, что я недавно обнаружил.

Проблема. Сопоставьте произвольно вложенную группу скобок в стиле регулярного выражения, например Java java.util.regex, который не поддерживает ни рекурсии, ни балансировки. Т.е., сопоставьте 3 внешние группы в:

(F (i (r (s) t))) ((S) (e) ((c) (o)) (n) d) ((((((Третий)))))) )

Это упражнение является чисто академическим, поскольку все мы знаем, что регулярные выражения не должны использоваться для соответствия этим вещам, так же как и Q-подсказки не должны использоваться для очистки ушей.

Ответ 1

В самом деле! Возможно использование прямых ссылок:

(?=\()(?:(?=.*?\((?!.*?\1)(.*\)(?!.*\2).*))(?=.*?\)(?!.*?\2)(.*)).)+?.*?(?=\1)[^(]*(?=\2$)

Доказательство

Et voila; вот оно. Это прямо соответствует полной группе вложенных круглых скобок от начала до конца. Две подстроки за матч обязательно фиксируются и сохраняются; это бесполезно для вас. Просто сосредоточьтесь на результатах основного матча.

Нет, нет предела по глубине. Нет, там нет рекурсивных конструкций. Просто простые образы, с всплеском прямой ссылки. Если ваш вкус не поддерживает передовые ссылки (я смотрю на вас, JavaScript), то мне жаль. Я действительно. Хотел бы я помочь тебе, но я не чудовищный рабочий.

Это здорово и все, но я хочу также совместить внутренние группы!

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

(?=\()(?=((?:(?=.*?\((?!.*?\2)(.*\)(?!.*\3).*))(?=.*?\)(?!.*?\3)(.*)).)+?.*?(?=\2)[^(]*(?=\3$))) 

Точно так же, как и предыдущее выражение, за исключением того, что я обернул большую часть его в виде, чтобы избежать употребления символов, добавил группу захвата и изменил индексы backreference, чтобы они играли хорошо со своим новым другом. Теперь выражение совпадает в позиции непосредственно перед следующей скобкой, а интересующая подстрока сохраняется как \1.

Итак... как, черт возьми, это действительно работает?

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

пробой регулярного выражения

Заключение

Итак, вот оно. Способ сопоставления сбалансированных вложенных структур с использованием прямых ссылок в сочетании со стандартными (расширенными) функциями регулярных выражений - без рекурсивных или сбалансированных групп. Это неэффективно, и это, конечно, не очень, но это возможно. И это никогда не делалось раньше. Для меня это очень интересно.

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

Ответ 2

Краткая

Коррекции ввода

Прежде всего, ваш ввод неверен, поскольку есть дополнительная скобка (как показано ниже)

(F(i(r(s)t))) ((S)(e)((c)(o))n)d) (((((((Third)))))))
                                ^

Внесение соответствующих изменений для включения или исключения дополнительных скобок может привести к одной из следующих строк:

Удалены дополнительные скобки

(F(i(r(s)t))) ((S)(e)((c)(o))n)d (((((((Third)))))))
                                ^

Добавлены дополнительные скобки для соответствия закрывающейся закрывающей скобке

((F(i(r(s)t))) ((S)(e)((c)(o))n)d) (((((((Third)))))))
^

Возможности регулярных выражений

Во-вторых, это действительно действительно возможно в ароматах регулярных выражений, которые включают в себя возможность рекурсии, поскольку любой другой метод не будет правильно соответствовать открывающим/закрывающим скобкам (как видно из решения OP, он соответствует дополнительной скобке с неправильного ввода как отмечено выше).

Это означает, что для ароматизаторов регулярных выражений, которые в настоящее время не поддерживают рекурсию (Java, Python, JavaScript и т.д.), рекурсия (или попытки имитации рекурсии) в регулярных выражениях возможна не. p >


Ввод

Учитывая, что исходный ввод действительно недействителен, мы будем использовать следующие входы для тестирования.

(F(i(r(s)t))) ((S)(e)((c)(o))n)d) (((((((Third)))))))
(F(i(r(s)t))) ((S)(e)((c)(o))n)d (((((((Third)))))))
((F(i(r(s)t))) ((S)(e)((c)(o))n)d) (((((((Third)))))))

Тестирование на эти входы должно давать следующие результаты:

  • INVALID (нет совпадения)
  • VALID (совпадение)
  • VALID (совпадение)

Код

Существует несколько способов сопоставления вложенных групп. Решения, представленные ниже, зависят от регулярных выражений, которые включают возможности рекурсии (например, PCRE).

См. здесь выражение regex

Использование блока DEFINE

(?(DEFINE)
  (?<value>[^()\r\n]+)
  (?<groupVal>(?&group)|(?&value))
  (?<group>(?&value)*\((?&groupVal)\)(?&groupVal)*)
)
^(?&group)$

Примечание. В этом регулярном выражении используются флаги gmx

Без блока DEFINE

См. здесь выражение regex

^(?<group>
  (?<value>[^()\r\n]+)*
  \((?<groupVal>(?&group)|(?&value))\)
  (?&groupVal)*
)$

Примечание. В этом регулярном выражении используются флаги gmx

Без модификатора x (однострочный)

См. здесь выражение regex

^(?<group>(?<value>[^()\r\n]+)*\((?<groupVal>(?&group)|(?&value))\)(?&groupVal)*)$

Без имени (группы и ссылки)

См. здесь выражение regex

^(([^()\r\n]+)*\(((?1)|(?2))\)(?3)*)$

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


Объяснение

Я объясню последнее регулярное выражение, так как это упрощенный и минимальный пример всех других регулярных выражений над ним.

  • ^ Позиция подтверждения в начале строки
  • (([^()\r\n]+)*\(((?1)|(?2))\)(?3)*) Захват в группу захвата 1
    • ([^()\r\n]+)* Захват следующего в группу захвата 2 любое количество раз
      • [^()\r\n]+ Сопоставьте любой символ, отсутствующий в наборе ()\r\n один или несколько раз
    • \( Соответствует символу скобки влево/в начале ( буквально
    • ((?1)|(?2)) Захват одного из следующих в группу захвата 3
      • (?1) Восстановить первый подшаблон (1)
      • (?2) Восстановить второй подшаблон (2)
    • \) Соответствует символу правой или закрывающей круглой скобки ) буквально
    • (?3)* Зарезервировать третий подшаблон (3) любое количество раз
  • $ Позиция подтверждения в конце строки