Балансирующие группы в переменном размере lookbehind
TL; DR: Использование захвата (и, в частности, балансирующих групп) внутри .NET lookbehind изменяет полученные захваты, хотя это не должно иметь значения. Что происходит с .NET lookbehinds, который нарушает ожидаемое поведение?
Я пытался найти ответ на этот другой вопрос, как повод поиграть с группами балансировки .NET. Однако я не могу заставить их работать внутри переменной длины.
Прежде всего, обратите внимание, что я не намерен использовать это конкретное решение продуктивно. Это больше по академическим причинам, потому что я чувствую, что что-то происходит с переменным размером lookbehind, о котором я не знаю. И зная, что это может пригодиться в будущем, когда мне действительно нужно использовать что-то вроде этого, чтобы решить проблему.
Рассмотрим этот вход:
~(a b (c) d (e f (g) h) i) j (k (l (m) n) p) q
Цель состоит в том, чтобы сопоставить все буквы, которые находятся в круглых скобках, которым предшествует ~
, не важно, как глубоко вниз (так что все от a
до i
). Моя попытка состояла в том, чтобы проверить правильное положение в lookbehind, чтобы я мог получить все буквы в одном вызове Matches
. Вот мой шаблон:
(?<=~[(](?:[^()]*|(?<Depth>[(])|(?<-Depth>[)]))*)[a-z]
В lookbehind я пытаюсь найти ~(
, а затем я использую названный стек группы Depth
для подсчета посторонних открывающих круглых скобок. Пока скобка, открытая в ~(
, никогда не закрывается, lookbehind должен совпадать. Если достигнута закрывающая скобка, (?<-Depth>...)
не может ничего вытащить из стека, и lookbehind должен завершиться ошибкой (то есть для всех букв из j
). К сожалению, это не работает. Вместо этого я сопоставляю a
, b
, c
, e
, f
, g
и m
. Итак, только эти:
~(a b (c) _ (e f (g) _) _) _ (_ (_ (m) _) _) _
Это похоже на то, что lookbehind не может сравниться ни с чем, как только я закрыл одну круглую скобку, , если я не вернусь к самому высокому уровню вложенности, к которому я был раньше.
Хорошо, это может означать, что с моим регулярным выражением есть что-то странное, или я не правильно понял балансирующие группы. Но потом я попробовал это без взгляда. Я создал строку для каждой буквы:
~(z b (c) d (e f (x) y) g) h (i (j (k) l) m) n
~(a z (c) d (e f (x) y) g) h (i (j (k) l) m) n
~(a b (z) d (e f (x) y) g) h (i (j (k) l) m) n
....
~(a b (c) d (e f (x) y) g) h (i (j (k) l) z) n
~(a b (c) d (e f (x) y) g) h (i (j (k) l) m) z
И использовал этот шаблон для каждого из них:
~[(](?:[^()]*|(?<Depth>[(])|(?<-Depth>[)]))*z
И при желании все случаи совпадают, где z
заменяет букву между a
и i
, а все случаи после этого терпят неудачу.
Итак, что делает (переменная длина) lookbehind, что нарушает это использование балансирующих групп? Я пытался исследовать это весь вечер (и нашел такие страницы, как этот), но я не мог найти ни одного использования этого в lookbehind.
Я также был бы рад, если бы кто-то мог связать меня с некоторой подробной информацией о том, как механизм .NET regex обрабатывает специфичные для .NET функции. Я нашел эту удивительную статью, но, похоже, она не выглядит, например, в (переменной длины) lookbehind.
Ответы
Ответ 1
Я думаю, что понял.
Во-первых, как я упоминал в одном из комментариев, (?<=(?<A>.)(?<-A>.))
никогда не совпадает.
Но потом я подумал, а как насчет (?<=(?<-A>.)(?<A>.))
? Это действительно так!
А как насчет (?<=(?<A>.)(?<A>.))
? При сопоставлении с "12"
, A
отображается "1"
, и если мы смотрим на коллекцию Captures
, это {"2", "1"}
- первые два, затем одно - оно отменяется.
Итак, находясь внутри lookbehind,.net соответствует и захватывает справа налево.
Теперь, как мы можем сделать это захват слева направо? Это довольно просто, на самом деле - мы можем обмануть двигатель, используя lookahead:
(?<=(?=(?<A>.)(?<A>.))..)
Применительно к вашему оригинальному patten, самый простой вариант, который я придумал, заключался в следующем:
(?<=
~[(]
(?=
(?:
[^()]
|
(?<Depth>[(])
|
(?<-Depth>[)])
)*
(?<=(\k<Prefix>)) # Make sure we matched until the current position
)
(?<Prefix>.*) # This is captured BEFORE getting to the lookahead
)
[a-z]
Задача здесь заключалась в том, что теперь сбалансированная часть может закончиться где угодно, поэтому мы доводим ее до текущей позиции (что-то вроде \G
или \Z
было бы полезно здесь, но я не думаю .NET имеет это)
Очень возможно, что это поведение документировано где-то, я постараюсь найти его.
Вот еще один подход. Идея проста -.net хочет совместить справа налево? Отлично! Возьмите это:
(подсказка: начать чтение снизу - вот как это делает .net)
(?<=
(?(Depth)(?!)) # 4. Finally, make sure there are no extra closed parentheses.
~\(
(?> # (non backtracking)
[^()] # 3. Allow any other character
|
\( (?<-Depth>)? # 2. When seeing an open paren, decreace depth.
# Also allow excess parentheses: '~((((((a' is OK.
|
(?<Depth> \) ) # 1. When seeing a closed paren, add to depth.
)*
)
\w # Match your letter
Ответ 2
Я думаю, что проблема связана с данными, а не с шаблоном. В данных есть элементы "Post", которые необходимо сопоставить, например
(a b (c) d e f)
где d e и f необходимы для соответствия. Более сбалансированные данные будут
(a b (c) (d) (e) (f))
Таким образом, для того, что я взял в этом примере, потребовалась ситуация после матча после скобок:
~ (a b (c) d (e f (g) h) i) j k
где j и k следует игнорировать... мой шаблон не удался и захватил их.
Интересно, что я назвал группы захватов, чтобы узнать, куда они вошли, и j и k вошли в захват трех. Я оставляю вас, а не ответ, но пытаюсь понять, можете ли вы улучшить его.
(~ # Anchor to a Tilde
( # Note that \x28 is ( and \x29 is )
( # --- PRE ---
(?<Paren>\x28)+ # Push on a match into Paren
((?<Char1>[^\x28\x29])(?:\s?))*
)+ # Represents Sub Group 1
( #---- Closing
((?<Char2>[^\x28\x29])(?:\s?))*
(?<-Paren>\x29)+ # Pop off a match from Paren
)+
(
((?<Char3>[^\x28\x29])(?:\s?))* # Post match possibilities
)+
)+
(?(Paren)(?!)) # Stop after there are not parenthesis
)
Вот матч разразился инструментом, который я создал самостоятельно (возможно, в один прекрасный день я опубликую). Обратите внимание, что ˽ показывает, где было сопоставлено пространство.
Match #0
[0]: ~(a˽b˽(c)˽d˽(e˽f˽(g)˽h)˽i)˽j˽k
["1"] → [1]: ~(a˽b˽(c)˽d˽(e˽f˽(g)˽h)˽i)˽j˽k
→1 Captures: ~(a˽b˽(c)˽d˽(e˽f˽(g)˽h)˽i)˽j˽k
["2"] → [2]: (e˽f˽(g)˽h)˽i)˽j˽k
→2 Captures: (a˽b˽(c)˽d˽, (e˽f˽(g)˽h)˽i)˽j˽k
["3"] → [3]: (g
→3 Captures: (a˽b˽, (c, (e˽f˽, (g
["4"] → [4]: g
→4 Captures: a˽, b˽, c, e˽, f˽, g
["5"] → [5]: ˽i)
→5 Captures: ), ), ˽h), ˽i)
["6"] → [6]: i
→6 Captures: ˽, h, ˽, i
["7"] → [7]:
→7 Captures: ˽d˽, , ˽j˽k,
["8"] → [8]: k
→8 Captures: ˽, d˽, ˽, j˽, k
["Paren"] → [9]:
["Char1"] → [10]: g
→10 Captures: a, b, c, e, f, g
["Char2"] → [11]: i
→11 Captures: ˽, h, ˽, i
["Char3"] → [12]: k
→12 Captures: ˽, d, ˽, j, k