Балансирующие группы в переменном размере 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