Почему эта обратная ссылка не работает внутри взгляда назад?
Совпадение повторяющегося символа в регулярном выражении просто с обратной ссылкой:
(.)\1
Тем не менее, я хотел бы сопоставить символ после пары символов, поэтому я подумал, что я мог бы просто поместить это в обзор:
(?<=(.)\1).
К сожалению, это ничего не соответствует.
Это почему? В других вариантах я не был бы удивлен, потому что существуют строгие ограничения для lookbehinds, но.NET обычно поддерживает произвольно сложные шаблоны внутри lookbehind.
1 ответ
Краткая версия: Lookbehinds сопоставляются справа налево. Это означает, что когда механизм регулярных выражений встречает \1
он еще ничего не включил в эту группу, поэтому регулярное выражение всегда терпит неудачу. Решение довольно простое:
(?<=\1(.)).
К сожалению, полная история, когда вы начнете использовать более сложные шаблоны, будет намного более тонкой. Так вот...
Руководство по чтению регулярных выражений в.NET
Сначала несколько важных подтверждений. Человек, который научил меня, что взгляды за спиной совпадают справа налево (и сам это выяснил, много экспериментируя), был Коби в этом ответе. К сожалению, вопрос, который я задал тогда, был очень замысловатым примером, который не может служить отличным ориентиром для такой простой проблемы. Таким образом, мы решили, что было бы целесообразно создать новый и более канонический пост для использования в будущем и в качестве подходящей цели. Но, пожалуйста, подумайте над тем, чтобы побудить Коби выяснить очень важный аспект движка регулярных выражений.NET, который практически недокументирован (насколько мне известно, MSDN упоминает об этом в одном предложении на неочевидной странице).
Обратите внимание, что rexegg.com по-разному объясняет внутреннюю работу внешнего вида.NET (с точки зрения обращения к строке, регулярному выражению и любым потенциальным захватам). Хотя это и не повлияет на результат сопоставления, я считаю, что этот подход гораздо сложнее рассуждать, и из рассмотрения кода становится ясно, что это не то, что на самом деле делает реализация.
Так. Первый вопрос: почему это на самом деле более тонко, чем жирное предложение выше? Давайте попробуем сопоставить символ, которому предшествует либо a
или же A
используя локальный без учета регистра модификатор. Учитывая поведение сопоставления справа налево, можно ожидать, что это сработает:
(?<=a(?i)).
Однако, как вы можете видеть здесь, похоже, что модификатор вообще не используется. Действительно, если мы поместим модификатор впереди:
(?<=(?i)a).
Другой пример, который может быть удивительным при сопоставлении справа налево, заключается в следующем:
(?<=\2(.)(.)).
Ли \2
обратитесь к левой или правой группе захвата? Это относится к правильному, как показывает этот пример.
Последний пример: при сопоставлении с abc
Захватывает ли это захват b
или же ab
?
(?<=(b|a.))c
Захватывает b
, (Вы можете увидеть снимки на вкладке "Таблица".) Еще раз "взгляды применяются справа налево" - не полная история.
Следовательно, этот пост пытается быть исчерпывающим справочником по всем вопросам, касающимся направленности регулярных выражений в.NET, так как я не знаю ни одного такого ресурса. Хитрость в чтении сложного регулярного выражения в.NET заключается в том, чтобы сделать это за три или четыре прохода. Все, кроме последнего прохода, слева направо, независимо от вида сзади или RegexOptions.RightToLeft
, Я считаю, что это так, потому что.NET обрабатывает их при разборе и компиляции регулярных выражений.
Первый проход: встроенные модификаторы
Это в основном то, что показывает приведенный выше пример. Если где-то в вашем регулярном выражении, у вас был этот фрагмент:
...a(b(?i)c)d...
Независимо от того, где в паттерне есть или используете ли вы опцию RTL, c
будет без учета регистра a
, b
а также d
не будет (при условии, что они не затронуты каким-либо другим предшествующим или глобальным модификатором). Это, наверное, самое простое правило.
Второй проход: номера групп [неназванные группы]
Для этого прохода вы должны полностью игнорировать любые именованные группы в шаблоне, т.е. группы (?<a>...)
, Обратите внимание, что это не включает группы с явными номерами, такими как (?<2>...)
(которые есть в.NET).
Захватывающие группы нумеруются слева направо. Неважно, насколько сложным является ваше регулярное выражение, используете ли вы опцию RTL или вкладываете ли вы десятки видовых и прогнозирующих элементов. Когда вы используете только неназванные группы захвата, они нумеруются слева направо в зависимости от положения их открывающей скобки. Пример:
(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
уже используется. Помните, что где-то между ними могут быть именованные группы, которые мы пока полностью игнорируем.
Если вам интересно, какова цель повторных групп, таких как группа 1
В этом примере вы можете прочитать о балансировке групп.
Третий проход: номера групп [именованные группы]
Конечно, вы можете полностью пропустить этот проход, если в регулярном выражении нет именованных групп.
Это малоизвестная особенность, что именованные группы также имеют (неявные) номера групп в.NET, которые можно использовать в обратных ссылках и шаблонах подстановки для Regex.Replace
, Они получают свои номера в отдельном проходе после обработки всех неназванных групп. Правила присвоения им номеров следующие:
- Когда имя появляется впервые, группа получает первый неиспользованный номер. Опять же, это может быть пробел в используемых числах, если регулярное выражение использует явные числа, или это может быть на единицу больше, чем наибольшее число групп на данный момент. Это навсегда связывает этот новый номер с текущим именем.
- Следовательно, когда имя снова появляется в регулярном выражении, группа будет иметь тот же номер, который использовался для этого имени в последний раз.
Более полный пример со всеми тремя типами групп, явно показывающий проходы два и три:
(?<a>.)(.)(.)(?<b>.)(?<a>.)(?<5>.)(.)(?<c>.)
Pass 2: │ │└1┘└2┘│ ││ │└──5──┘└3┘│ │
Pass 3: └──4──┘ └──6──┘└──4──┘ └──7──┘
Последний проход: после двигателя регулярных выражений
Теперь, когда мы знаем, какие модификаторы применяются к каким токенам и какие группы имеют какие номера, мы наконец-то добрались до той части, которая фактически соответствует выполнению движка регулярных выражений, и с чего мы начинаем переходить туда-сюда.
Движок регулярных выражений.NET может обрабатывать регулярные выражения и строки в двух направлениях: обычный режим слева направо (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(.)
, поскольку нам нужно убедиться, что механизм регулярных выражений обнаруживает захват, прежде чем он попытается обратиться к нему.
Имея это в виду, мы можем увидеть обходные пути в новом свете. Когда механизм регулярных выражений встречает взгляд назад, он обрабатывает его следующим образом:
- Он помнит свою текущую позицию
x
в целевой строке, а также ее текущее направление обработки. - Теперь он обеспечивает режим RTL, независимо от того, в каком режиме он находится в данный момент.
- Затем содержимое lookhehind сопоставляется справа налево, начиная с текущей позиции
x
, - После того, как взгляд полностью обработан, если он пройден, положение механизма регулярных выражений сбрасывается в положение
x
и первоначальное направление обработки восстанавливается.
В то время как предвидение выглядит намного более безобидным (поскольку мы почти никогда не сталкиваемся с проблемами, подобными тем, которые обсуждались с ними), его поведение фактически практически такое же, за исключением того, что он обеспечивает режим LTR. Конечно, в большинстве моделей, которые являются только LTR, это никогда не замечается. Но если само регулярное выражение сопоставляется в режиме RTL, или мы делаем что-то такое же безумное, как установка заглядывания внутрь взгляда назад, тогда просмотрщик изменит направление обработки так же, как это делает просмотр.
Так как же вам на самом деле читать регулярные выражения, которые делают такие забавные вещи, как это? Первый шаг - разделить его на отдельные компоненты, которые обычно представляют собой отдельные токены вместе с соответствующими квантификаторами. Затем, в зависимости от того, является ли регулярное выражение 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 символов вправо.
- Вид сзади переключается в режим 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>...)
(хотя бы с одним символом между ними, так что они не разделяют общую границу), ничего не выдвигается b
, Под этим я действительно ничего не имею в виду, даже пустая строка - сам стек захвата остается пустым. Тем не менее, сопоставление группы все еще успешно, и соответствующий захват извлекается из a
группа.
Что особенно раздражает в этом, так это то, что этот случай, вероятно, будет гораздо более распространенным, чем случай 2, поскольку именно это происходит, если вы пытаетесь использовать балансировочные группы так, как они должны были использоваться, но в обычном порядке справа налево. регулярное выражение.
Обновление в случае 3: после еще одного тестирования, проведенного Kobi, выясняется, что что- то происходит в стеке b
, Похоже, что ничего не толкается, потому что m.Groups["b"].Success
будет False
а также m.Groups["b"].Captures.Count
будет 0
, Однако в рамках регулярного выражения (?(b)true|false)
теперь будет использовать true
ветка. Также в.NET, кажется, можно сделать (?<-b>)
потом (после чего доступ m.Groups["b"]
вызовет исключение), в то время как Mono генерирует исключение немедленно при сопоставлении с регулярным выражением. Ошибка действительно.