Почему этот backreference не работает внутри lookbehind?

Совпадение повторяющегося символа в регулярном выражении просто с обратной репликацией:

(.)\1

Проверьте его здесь.

Однако, я хотел бы совместить символ после пары символов, поэтому я подумал, что могу просто добавить это в lookbehind:

(?<=(.)\1).

К сожалению, это ничего не соответствует.

Почему? В других вариантах я не удивлюсь, потому что есть серьезные ограничения на lookbehinds, но .NET обычно поддерживает произвольно сложные шаблоны внутри lookbehinds.

Ответ 1

Краткая версия: Lookbehinds сопоставляются справа налево. Это означает, что когда механизм regex встречает \1, он еще ничего не зафиксировал в этой группе, поэтому регулярное выражение всегда терпит неудачу. Решение довольно просто:

(?<=\1(.)).

Проверьте его здесь.

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

Руководство по чтению регулярных выражений в .NET

Во-первых, некоторые важные подтверждения. Человек, который научил меня, что lookbehind сопоставляются справа налево (и понял это самостоятельно через много экспериментов), был Kobi в этом ответе. К сожалению, вопрос, который я задал тогда, был очень запутанным примером, который не дает отличной справки для такой простой проблемы. Поэтому мы поняли, что было бы целесообразно создать новую и более каноническую должность для будущей справки и в качестве подходящей цели обмана. Но, пожалуйста, подумайте о том, чтобы дать Kobi преимущество для выяснения очень важного аспекта механизма регулярных выражений .NET, который практически не документирован (насколько я знаю, MSDN упоминает его в одном предложении на неочевидной странице).

Обратите внимание, что rexegg.com объясняет внутреннюю работу .NET lookbehinds по-разному (с точки зрения изменения строки, регулярного выражения и любого другого потенциальные захваты). Хотя это не повлияет на результат матча, я нахожу, что подход гораздо сложнее рассуждать, а от просмотра кода достаточно ясно, что это не то, что на самом деле реализуется.

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

(?<=a(?i)).

Однако как вы можете видеть здесь, похоже, что он вообще не использует модификатор. Действительно, если мы поместим модификатор впереди:

(?<=(?i)a).

... он работает.

Еще один пример, который может удивить с учетом соответствия справа налево, состоит в следующем:

(?<=\2(.)(.)).

Относится ли \2 к левой или правой группе захвата? Это относится к правильному, как показано в этом примере.

Последний пример: при сопоставлении с abc выполняется ли этот захват b или ab?

(?<=(b|a.))c

Он захватывает b. (Вы можете увидеть захваты на вкладке "Таблица".) Еще раз "lookbehinds применяются от справа налево" - это не полная история.

Следовательно, это сообщение пытается быть всеобъемлющей ссылкой на все вопросы относительно направленности регулярного выражения в .NET, так как я не знаю ни одного такого ресурса. Трюк для чтения сложного регулярного выражения в .NET делает это через три или четыре прохода. Все, кроме последнего прохода, слева направо, независимо от lookbehinds или RegexOptions.RightToLeft. Я считаю, что это так, потому что .NET обрабатывает их при разборе и компиляции регулярных выражений.

Первый проход: встроенные модификаторы

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

...a(b(?i)c)d...

Независимо от того, где в шаблоне или используете ли вы параметр RTL, c будет нечувствителен к регистру, а a, b и d не будут (если они не будут затронуты каким-либо другим предыдущим или глобальным модификатором). Это, вероятно, самое простое правило.

Второй проход: номера групп [неназванные группы]

Для этого прохода вы должны полностью игнорировать любые именованные группы в шаблоне, то есть в форме (?<a>...). Обратите внимание, что это не включает группы с явными номерами, такими как (?<2>...) (это что-то в .NET).

Захватывающие группы пронумерованы слева направо. Не имеет значения, насколько сложным является ваше регулярное выражение, используете ли вы вариант RTL или используете ли вы десятки lookbehinds и lookaheads. Когда вы используете только неназванные группы захвата, они пронумерованы слева направо в зависимости от положения их открывающей скобки. Пример:

(a)(?<=(b)(?=(.)).((c).(d)))(e)
└1┘    └2┘   └3┘  │└5┘ └6┘│ └7┘
                  └───4───┘

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

  • Если группа имеет явное число, ее число, очевидно, есть (и только это) число. Обратите внимание, что это может либо добавить дополнительный захват к уже существующему номеру группы, либо создать новый номер группы. Также обратите внимание, что когда вы даете явные номера групп, они не обязательно должны быть последовательными. (?<1>.)(?<5>.) - отлично действующее регулярное выражение с номером группы 2 до 4 не используется.
  • Если группа не маркирована, она берет первый неиспользованный номер. Из-за недостатков, которые я только что упомянул, это может быть меньше максимального количества, которое уже было использовано.

Вот пример (без вложенности, для простоты, не забудьте заказать их по их открывающим скобкам, когда они вложены):

(a)(?<1>b)(?<2>c)(d)(e)(?<6>f)(g)(h)
└1┘└──1──┘└──2──┘└3┘└4┘└──6──┘└5┘└7┘

Обратите внимание, как явная группа 6 создает пробел, тогда группа захвата g принимает этот неиспользованный разрыв между группами 4 и 6, тогда как группа захвата h принимает 7, потому что 6 уже используется. Помните, что там могут быть названы группы где-то между ними, которые мы сейчас полностью игнорируем.

Если вам интересно, какая цель повторяющихся групп, таких как group 1 в этом примере, вы можете прочитать о балансирующих группах.

Третий проход: номера групп [названные группы]

Конечно, вы можете пропустить этот проход полностью, если в регулярном выражении нет именованных групп.

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

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

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

         (?<a>.)(.)(.)(?<b>.)(?<a>.)(?<5>.)(.)(?<c>.)
Pass 2:  │     │└1┘└2┘│     ││     │└──5──┘└3┘│     │
Pass 3:  └──4──┘      └──6──┘└──4──┘          └──7──┘

Конечный проход: после двигателя регулярного выражения

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

.NET regex engine может обрабатывать регулярное выражение и строку в двух направлениях: обычный режим слева направо (LTR) и его уникальный режим справа налево (RTL). Вы можете активировать RTL-режим для всего регулярного выражения с помощью RegexOptions.RightToLeft. В этом случае движок начнет пытаться найти совпадение в конце строки и перейдет влево через регулярное выражение и строку. Например, простое регулярное выражение

a.*b

Сопоставлял бы b, тогда он попытался бы сопоставить .* слева от него (при необходимости возвращаясь назад), чтобы там a где-то слева от него. Разумеется, в этом простом примере результат между режимами LTR и RTL идентичен, но он помогает сделать сознательное усилие следить за движком в его обратном направлении. Это может повлиять на что-то столь же простое, как и на невращающиеся модификаторы. Рассмотрим регулярное выражение

a.*?b

вместо этого. Мы пытаемся сопоставить axxbxxb. В режиме LTR вы получите совпадение axxb, как и ожидалось, потому что нечеткий квантификатор удовлетворяется с помощью xx. Однако в режиме RTL вы фактически должны соответствовать всей строке, так как первый b находится в конце строки, но тогда .*? должен соответствовать всем xxbxx для a для соответствия.

И, очевидно, это также имеет значение для обратных ссылок, как показывает пример в вопросе и в верхней части этого ответа. В режиме LTR мы используем (.)\1 для соответствия повторяющимся символам и в режиме RTL мы используем \1(.), так как нам нужно убедиться, что механизм regex встречает захват, прежде чем он попытается ссылаться на него.

Имея это в виду, мы можем видеть образы в новом свете. Когда двигатель регулярных выражений встречает lookbehind, он обрабатывает его следующим образом:

  • Он запоминает текущую позицию x в целевой строке, а также ее текущее направление обработки.
  • Теперь он обеспечивает режим RTL, независимо от режима, в котором он сейчас находится.
  • Затем содержимое lookbehind сопоставляется справа налево, начиная с текущей позиции x.
  • После того, как lookbehind будет обработано полностью, если он прошел, позиция двигателя регулярных выражений сбрасывается в положение x и восстанавливается исходное направление обработки.

В то время как просмотр выглядит намного более безобидным (поскольку мы почти никогда не сталкиваемся с такими проблемами, как вопрос с ними), его поведение фактически практически одинаково, за исключением того, что оно обеспечивает режим LTR. Конечно, в большинстве моделей, которые являются только LTR, это никогда не замечается. Но если само регулярное выражение соответствует RTL-режиму, или мы делаем что-то безумное, как вставляем взгляд в lookbehind, тогда lookahead изменит направление обработки так же, как выглядит lookbehind.

Итак, как вы на самом деле читаете регулярное выражение, которое делает такие забавные вещи? Первый шаг состоит в том, чтобы разбить его на отдельные компоненты, которые обычно являются индивидуальными токенами вместе с соответствующими кванторами. Затем, в зависимости от того, является ли регулярное выражение LTR или RTL, начните переходить сверху вниз или снизу вверх, соответственно. Всякий раз, когда вы сталкиваетесь с поиском в процессе, проверьте, в какую сторону он обращается, и пропустите к правильному концу и прочитайте оттуда оттуда. Когда вы закончите поиск, продолжите с окружающим рисунком.

Конечно, есть еще один улов... когда вы сталкиваетесь с чередованием (..|..|..), альтернативы всегда проверяются слева направо даже во время соответствия RTL. Конечно, в каждой альтернативе двигатель движется справа налево.

Вот несколько надуманный пример, чтобы показать это:

.+(?=.(?<=a.+).).(?<=.(?<=b.|c.)..(?=d.|.+(?<=ab*?))).

И вот как мы можем разделить это. Цифры слева показывают порядок чтения, если регулярное выражение находится в режиме LTR. Номера справа показывают порядок чтения в режиме RTL:

LTR             RTL

 1  .+          18
    (?=
 2    .         14
      (?<=
 4      a       16
 3      .+      17
      )
 5    .         13
    )
 6  .           13
    (?<=
17    .         12
      (?<=
14      b        9
13      .        8
      |
16      c       11
15      .       10
      )
12    ..         7
      (?=
 7      d        2
 8      .        3
      |
 9      .+       4
        (?<=
11        a      6
10        b*?    5
        )
      )
    )
18  .            1

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

Расширенный раздел: балансирующие группы

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

Существует три типа группового синтаксиса, которые важны для балансирующих групп.

  • Явно названные или нумерованные группы, такие как (?<a>...) или (?<2>...) (или даже неявно пронумерованные группы), о которых мы говорили выше.
  • Группы, которые появляются из одного из стеков захвата, таких как (?<-a>...) и (?<-2>...). Они ведут себя так, как вы ожидали. Когда они встречаются (в правильном порядке обработки, описанном выше), они просто появляются из соответствующего стека захвата. Возможно, стоит отметить, что они не получают неявных номеров групп.
  • "правильные" балансировочные группы (?<b-a>...), которые обычно используются для захвата строки с последнего из b. Их поведение становится странным при смешивании с режимом "справа налево" и в том, что касается этого раздела.

Вынос - это функция (?<b-a>...), которая эффективно не работает с режимом "справа налево". Однако после многих экспериментов (странное) поведение, по-видимому, соответствует некоторым правилам, которые я описываю здесь.

Во-первых, давайте посмотрим на пример, который показывает, почему образы осложняют ситуацию. Мы сопоставляем строку abcde...wvxyz. Рассмотрим следующее регулярное выражение:

(?<a>fgh).{8}(?<=(?<b-a>.{3}).{2})

Чтение регулярного выражения в приведенном выше порядке, мы можем видеть, что:

  • Регулярное выражение захватывает fgh в группу a.
  • Затем движок перемещает 8 символов вправо.
  • Функция lookbehind переключается в режим RTL.
  • .{2} перемещает два символа влево.
  • Наконец, (?<b-a>.{3}) - это балансирующая группа, которая выталкивает группу захвата a и толкает что-то в группу b. В этом случае группа соответствует lmn, и мы нажимаем ijk на группу b, как ожидалось.

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

Оказывается, есть три случая, чтобы различать.

Случай 1: (?<a>...) соответствует слева от (?<b-a>...)

Это нормальный случай. Верхний захват выносится из a, и все между подстроками, соответствующими двум группам, помещается на b. Рассмотрим две следующие подстроки для двух групп:

abcdefghijklmnopqrstuvwxyz
   └──<a>──┘  └──<b-a>──┘

Что вы можете получить с регулярным выражением

(?<a>d.{8}).+$(?<=(?<b-a>.{11}).)

Затем mn будет нажата на b.

Случай 2: (?<a>...) и (?<b-a>...) пересекаются

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

Вот несколько примеров, чтобы показать это:

        Example:              Pushes onto <b>:    Possible regex:

abcdefghijklmnopqrstuvwxyz    ""                  (?<a>d.{8}).+$(?<=(?<b-a>.{11})...)
   └──<a>──┘└──<b-a>──┘

abcdefghijklmnopqrstuvwxyz    "jkl"               (?<a>d.{8}).+$(?<=(?<b-a>.{11}).{6})
   └──<a>┼─┘       │
         └──<b-a>──┘

abcdefghijklmnopqrstuvwxyz    "klmnopq"           (?<a>k.{8})(?<=(?<b-a>.{11})..)
      │   └──<a>┼─┘
      └──<b-a>──┘

abcdefghijklmnopqrstuvwxyz    ""                  (?<=(?<b-a>.{7})(?<a>.{4}o))
   └<b-a>┘└<a>┘

abcdefghijklmnopqrstuvwxyz    "fghijklmn"         (?<a>d.{12})(?<=(?<b-a>.{9})..)
   └─┼──<a>──┼─┘
     └─<b-a>─┘

abcdefghijklmnopqrstuvwxyz    "cdefg"             (?<a>c.{4})..(?<=(?<b-a>.{9}))
│ └<a>┘ │
└─<b-a>─┘

Случай 3: (?<a>...) соответствует праву (?<b-a>...)

Этот случай я действительно не понимаю и рассмотрю ошибку: когда подстрока, соответствующая (?<b-a>...), правильно оставлена ​​от подстроки, соответствующей (?<a>...) (по крайней мере с одним символом между ними, t имеет общую границу), ничего не нажата b. Тем самым я ничего не имею в виду, даже пустая строка - сам стек захвата остается пустым. Однако сопоставление группы по-прежнему выполняется успешно, и соответствующий захват удаляется из группы a.

Что особенно неприятно в этом, так это то, что этот случай, вероятно, будет намного более распространенным, чем случай 2, так как это происходит, если вы пытаетесь использовать балансировочные группы так, как они должны были использоваться, to-left regex.

Обновление на примере 3: После некоторого тестирования, проведенного Kobi, выясняется, что что-то происходит в стеке b. Похоже, что ничего не толкается, потому что m.Groups["b"].Success будет False, а m.Groups["b"].Captures.Count будет 0. Однако в регулярном выражении условный (?(b)true|false) теперь будет использовать ветвь true. Кроме того, в .NET, возможно, после этого можно сделать (?<-b>) (после чего обращение к m.Groups["b"] будет генерировать исключение), тогда как Mono генерирует исключение сразу же при сопоставлении регулярного выражения. Ошибка.