Perl regex сопоставляет необязательную фразу в более длинном предложении

Я пытаюсь сопоставить необязательную (возможно существующую) фразу в предложении:

perl -e '$_="word1 word2 word3"; print "1:$1 2:$2 3:$3\n" if m/(word1).*(word2)?.*(word3)/'

Вывод:

1:word1 2: 3:word3

Я знаю, что первый ". *" жадный и сопоставляющий все до слова "3". Сделать это не жадным не помогает:

perl -e '$_="word1 word2 word3"; print "1:$1 2:$2 3:$3\n" if m/(word1).*?(word2)?.*(word3)/'

Вывод:

1:word1 2: 3:word3 

Здесь, похоже, есть конфликт интересов. Я бы подумал, что Perl будет соответствовать (word2)? если возможно, и все еще насыщать неживые. *?. По крайней мере, мое понимание "?". На странице регулярных выражений Perl написано '?' составляет 1 или ноль раз, поэтому не следует ли ему выбирать одно совпадение, а не ноль?

Еще более запутанным является то, что я захватил. *?:

perl -e '$_="word1 word2 word3"; print "1:$1 2:$2 3:$3 4:$4\n" if m/(word1)(.*?)(word2)?.*(word3)/'

Вывод:

1:word1 2: 3: 4:word3

Все группы здесь собирают группы, поэтому я не знаю, почему они пусты.

Просто чтобы убедиться, что пространство между словами не захватывается:

perl -e '$_="word1_word2_word3"; print "1:$1 2:$2 3:$3 4:$4\n" if m/(word1)(.*?)(word2)?.*(word3)/'

Вывод:

1:word1 2: 3: 4:word3

Учитывая, что единственное совпадение, которое не захватывает, - это одно между word2 и word3, я могу только предположить, что он выполняет сопоставление. Конечно же:

perl -e '$_="word1_word2_word3"; print "1:$1 2:$2 3:$3 4:$4 5:$5\n" if m/(word1)(.*?)(word2)?(.*)(word3)/'

Вывод:

1:word1 2: 3: 4:_word2_ 5:word3

Таким образом, жадное совпадение работает в обратном направлении, и Perl рад соответствовать нулевому (а не одному) экземпляру word2. Сделать это не жадным также не помогает.

Итак, мой вопрос: как я могу написать свое регулярное выражение для сопоставления и захвата возможной фразы в предложении? Мои приведенные здесь примеры упрощены; фактическое предложение, которое я обрабатываю, намного длиннее со многими словами между теми, которые я сопоставляю, поэтому я не могу предположить длину или состав промежуточного текста.

Большое спасибо, Скотт

Ответ 1

ПРЕДПОСЫЛКА: КАК РАБОТАЕТ ЛАЗИРОВАННЫЕ И ПОСЛЕДНИЕ КОЛИЧЕСТВА

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

Ленивые шаблоны соответствия сначала соответствуют минимальным символам, а затем пытаются сопоставляться с остальными подшаблонами. С *? он соответствует нулевым символам, пустому пространству и затем проверяет, можно ли сопоставить следующий шаблон, и только если он не может, ленивый подшаблон будет "расширен", чтобы включить еще один символ, и поэтому на.

Итак, (word1).*(word2)?.*(word3) будет соответствовать word2 с первым .* (а второй .* будет соответствовать пустующему пространству, поскольку первый .* является жадным. Хотя вы можете думать, что (word2)? жадный и, следовательно, должен быть отброшен, ответ отрицательный, потому что первый .* схватил всю строку, а затем движок пошел назад, ища совпадение. Поскольку (word2)? соответствует пустой строке, она всегда соответствует, и word3 был сопоставлен сначала с конца строки. См. эту демонстрацию и проверьте раздел отладчика регулярных выражений.

Вы подумали, пусть ленивое совпадение с первым .\*?. Проблема с (word1).*?(word2)?.*(word3) (которая соответствует word2 со вторым .*, жадным) немного отличается, поскольку она может соответствовать необязательная группа. Как? Первый .*? соответствует нулевым символам, затем пытается сопоставить все последующие подшаблоны. Таким образом, он нашел word1, затем пустую строку и не нашел word2 сразу после word1. Если word2 были сразу после word1, было бы совпадение с первым .*?. См. это демо.

РЕШЕНИЕ

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

  • A ветвь reset решение, предоставленное Casimir выше. Его недостатком является то, что он не может быть перенесен на многие другие ароматы регулярных выражений, которые не поддерживают ветвь reset. См. Описание в исходном ответе.
  • Используйте умеренный жадный токен: (word1)(?:(?!word2).)*(word2)?.*?(word3). Он менее эффективен, чем решение ветки reset, но может быть перенесено на JS, Python и большинство других вариантов регулярных выражений, поддерживающих lookaheads. Как это работает? (?:(?!word2).)* соответствует 0+ вхождениям любого символа, отличного от новой строки (с /s, даже включая новую строку), которая не запускает буквенную последовательность символов word2. Если соответствие w не может сопровождаться ord2 для соответствующей конструкции. Таким образом, когда он достигает word2, он останавливается и позволяет последующему подшаблону - (word2)? - сопоставлять и записывать следующие word2. * Чтобы сделать этот подход более эффективным **, используйте развернуть метод цикла: (word1)[^w]*(?:w(?!ord2)[^w]*)*(word2)?.*?(word3).

Ответ 2

Вы можете использовать конструкцию ветки reset как обходной путь:

 (word1)(?|.*?(word2).*?(word3)|().*?(word3))
#^            ^         ^       ^    ^---- group 3
#|            |         |       '--------- group 2
#|            |         '----------------- group 3
#|            '--------------------------- group 2
#'---------------------------------------- group 1

Основной интерес ветки reset group (?|...()...()|...()...()) заключается в том, что группы захвата имеют одинаковые числа в каждой ветки. Вместо того, чтобы сделать группу 2 необязательной, вы можете использовать первую ветвь, где группа является обязательной, а вторая, где она пуста (или вы можете заполнить ее всегда неудачным шаблоном и добавить после нее ?).

Ответ 3

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

 (word1).*(word2)?.*(word3)
        --
         ^--- this subexpression matches _all_ material between `word1` and `word3` in the test string, in particular `word2` if it is present

 (word1).*? (word2)? .*(word3)
        ---+--------+--
         ^       ^   ^-- this subexpression matches _all_ material between `word1` and `word3` in the test string, in particular `word2` if it is present
         |       |
         |       +------ this subexpression is empty, even if `word2` is present:
         |               - the preceding subexpression `.*?` matches minimally (ie. the empty string)
         |               - `(word2)?` cannot match for the preceding blank.
         |               - the following subexpression `.*` matches everything up to `word3`, including `word2`.
         |
         |               -> the pattern matches _as desired_ for test strings
         |                  where `word2` immediately follows `word1` without  
         |
         +-------------- this subexpression will always be empty

Вам нужна конструкция, которая предотвращает привязку всех элементов, содержащих word2. К счастью, синтаксис regex perl поддерживает отрицательный lookbehind, который служит цели: для каждого символа в совпадении полного подвыражения catch-all убедитесь, что ему не предшествует word2.

В perl:

/(word1).*(word2).*(word3)|word1((?<!word2).)*word3/

Предостережение

  • Это может быть hog производительности.
  • Обратите внимание, что word2 должен быть литералом, так как механизм regex поддерживает только шаблоны с длиной совпадения, известные априори.

Альтернативное решение

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

$teststring = $_;
if ($teststring =~ m/(word1).*(word2).*(word3)/) {
    print \"1:$1 2:$2 3:$3\n\";
}
else {
    # You know by now that there is no word2 between any word1, word3 occurrences 
    if ($teststring =~ m/(word1).*(word3)/) {
        print \"1:$1 2:- 3:$2\n\";
    }
}