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\";
}
}