Почему эта обратная ссылка не работает внутри просмотра назад?

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

(.)\1

Протестируйте здесь.

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

(?<=(.)\1).

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

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


person Martin Ender    schedule 16.03.2016    source источник
comment
Я добавил этот вопрос в ссылку под заголовком: Как читать регулярное выражение .NET с предпросмотром, просмотром позади, группы захвата и обратные ссылки, смешанные вместе?   -  person nhahtdh    schedule 17.03.2016
comment
Эта же проблема и такое же объяснение применимы и к RE JavaScript.   -  person CertainPerformance    schedule 02.11.2020


Ответы (1)


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

(?<=\1(.)).

Протестируйте здесь.

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

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

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

Обратите внимание, что rexegg.com по-разному объясняет внутреннюю работу .NET lookbehinds (в ​​терминах обращения строки, регулярного выражения и любых потенциальных захватов). Хотя это не повлияет на результат матча, я считаю, что такой подход гораздо труднее обосновать, и .Text.RegularExpressions/src/System/Text/RegularExpressions" rel="noreferrer">из кода совершенно ясно, что это не то, что на самом деле делает реализация.

Так. Первый вопрос: почему это на самом деле тоньше, чем предложение, выделенное жирным шрифтом выше. Давайте попробуем сопоставить символ, которому предшествует 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 независимо от текущего режима.
  • Затем содержимое ретроспективы сопоставляется справа налево, начиная с текущей позиции 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

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

Расширенный раздел: группы балансировки

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

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

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

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

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

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

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

  1. Регулярное выражение захватывает fgh в группу a.
  2. Затем движок перемещается на 8 символов вправо.
  3. Lookbehind переключается в режим RTL.
  4. .{2} перемещает на два символа влево.
  5. Наконец, (?<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: после еще нескольких тестов, проведенных Коби, выяснилось, что что-то происходит в стеке b. Похоже, что ничего не проталкивается, потому что m.Groups["b"].Success будет False, а m.Groups["b"].Captures.Count будет 0. Однако в регулярном выражении условное выражение (?(b)true|false) теперь будет использовать ветвь true. Также в .NET кажется возможным сделать (?<-b>) впоследствии (после чего доступ к m.Groups["b"] вызовет исключение), тогда как Mono сразу же выдает исключение при сопоставлении с регулярным выражением. Ошибка действительно.

person Martin Ender    schedule 16.03.2016
comment
Судя по моему экрану, вы оба спросили и одновременно дали этот очень длинный ответ (17 секунд назад). Как тебе это удалось? - person Quantic; 17.03.2016
comment
@Quantic При задании вопроса есть флажок Ответить на свой вопрос — поделитесь своими знаниями в стиле вопросов и ответов, что дает вам возможность написать ответ вместе с вопросом. - person Martin Ender; 17.03.2016
comment
@WiktorStribiżew По сути, это одно и то же, и гораздо сложнее рассуждать о том, как отменить регулярное выражение, чем рассуждать о его применении справа налево. То, действительно ли регулярное выражение и строка перевернуты в коде, является деталью реализации. - person Martin Ender; 17.03.2016
comment
@WiktorStribiżew: переворачивать строку было бы глупо с точки зрения производительности, в то время как вы можете просто изменить направление движения текущего указателя. Исходный код показывает именно это: github.com/dotnet/corefx/blob/master/src/ - person nhahtdh; 17.03.2016
comment
Просто любопытно - почему вы называете чередования еще одним уловом, а не просто частью общей идеи о том, что регулярное выражение разбирается слева направо, а затем запускается LTR/ RTL в соответствии с глобальным флагом и смотрите ли вы вперед/назад? - person Rawling; 02.08.2016
comment
@Rawling Хотя вы правы в том, что это основная причина, можно предположить, что запуск чередования RTL также будет пробовать альтернативы справа налево, поэтому я решил, что на это стоит обратить особое внимание. - person Martin Ender; 02.08.2016