【问题标题】:Perl regex matching optional phrase in longer sentencePerl 正则表达式匹配较长句子中的可选短语
【发布时间】:2016-04-21 13:16:07
【问题描述】:

我正在尝试匹配句子中的可选(可能存在)短语:

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

输出:

1:word1 2: 3:word3

我知道第一个 '.*' 是贪婪的,并且将所有内容都匹配到 'word3'。让它不贪婪并没有帮助:

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 次或 0 次,所以它不应该更喜欢一次匹配而不是零次吗?

如果我捕获了 .*?:

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 的零个(而不是一个)实例。让它不贪婪也无济于事。

所以我的问题是:如何编写正则表达式来匹配和捕获句子中可能的短语?我在这里给出的例子很简单。我正在解析的实际句子要长得多,在我匹配的句子之间有很多单词,所以我不能假设任何长度或中间文本的组成。

非常感谢, 斯科特

【问题讨论】:

  • 乍一看,您为第一个不分青红皂白的模式.*选择了非贪婪匹配,然后是一个可选模式((word2)?)和另一个包罗万象的表达式(.* ),后者再次匹配贪婪并获取您的输入,这就是为什么您的捕获组 2 (3) 永远不会被填充。
  • 不要使用通配符“.*”,而是尝试使用“\s”字符类作为带有“+”量词的空格。像这样的“\s+”。
  • 贪婪匹配正在倒退 - 你知道,带有子模式的贪婪量词会导致回溯。真正的答案是:使用带有否定可选模式的缓和贪婪令牌直到可选模式:(word1)(?:(?!word2).)*(word2)?.*?(word3)
  • @WiktorStribiżew 您是否有机会将您的评论转换为答案?我想投票。
  • @Scott:我添加了一个答案。

标签: regex perl


【解决方案1】:

背景:懒惰和贪婪的量词是如何工作的

您需要了解贪婪和惰性量词的工作原理。贪婪的会立即抓取他们的模式可以匹配的文本,然后引擎将回溯,即它会尝试回到贪婪量化的子模式匹配子字符串的地方,尝试检查是否可以匹配下一个子模式。

惰性匹配模式首先匹配最少的字符,然后尝试匹配其余的子模式。使用*?,它匹配个字符,一个空格,然后继续检查下一个模式是否可以匹配,只有不能匹配时,惰性子模式才会“扩展”为再包含一个字符,以此类推。

所以,(word1).*(word2)?.*(word3) 将匹配word2 与第一个.*(第二个.* 将匹配一个空白空间,因为第一个.* 是贪婪的。虽然你可以认为(word2)? 是贪婪,因此必须回溯到,答案是否定的,因为第一个.*抓住了所有字符串,然后引擎向后寻找匹配。由于(word2)?匹配一个空字符串,它总是匹配,而@ 987654339@ 从字符串末尾开始首先匹配。请参阅this demo 并检查 regex 调试器 部分。

你想,让我们对第一个.\*?使用惰性匹配(word1).*?(word2)?.*(word3) 的问题(匹配word2 和第二个贪婪的.*)有点不同,因为它可以匹配可选组。如何?第一个.*? 匹配零个字符,然后尝试匹配所有后续子模式。因此,它找到了word1,然后是一个空字符串,并且word1 之后没有找到word2如果word2 紧跟在word1 之后,则会与第一个.*? 匹配。参见this demo

解决方案

目前我看到了两种解决方案,它们都包括使第二个可选组对模式的其余部分“独占”,以便正则表达式引擎在找到时无法跳过它。

【讨论】:

    【解决方案2】:

    您可以使用 branch reset 构造作为解决方法:

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

    分支重置组(?|...()...()|...()...()) 的主要兴趣在于捕获组在每个分支中具有相同的编号。除了将组 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
      

      您需要的是一种结构,可以防止 catch-all 匹配包含 word2 的字符串。幸运的是,perl 的正则表达式语法支持负向回溯:对于 catch-all 子表达式匹配中的每个字符,确保它前面没有 word2

      在 perl 中:

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

      注意事项

      1. 这可能是性能问题。
      2. 请注意,word2 必须是文字,因为正则表达式引擎仅支持先验已知匹配长度的模式。

      替代解决方案

      鉴于警告,您可能会尝试更改控制逻辑:

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

      【讨论】:

      • 感谢@collapsar,但是我无法让您提供的正则表达式工作:perl -e '$_="word1_stuff_word3"; print "1:$1 2:$2 3:$3 4:$4 5:$5\n" if m/(word1).*(word2).*(word3)|word1((?&lt;!word2).)*word3/' 输出:1: 2: 3: 4:_ 5:。不幸的是,我将正则表达式作为参数传递给另一个脚本(样本),所以我不能使用你的替代解决方案,但我知道你来自哪里!
      • 正则表达式确实有效,您只需为第二种选择中感兴趣的部分添加捕获组。这条线可以解决问题:perl -e '$_="word1_stuff_word3"; print "1:$1 2:$2 3:$3 4:$4 5:$6\n" if m/(word1).*(word2).*(word3)|(word1)((?&lt;!word2).)*(word3)/。请注意,lookbehind 表达式周围的括号不会建立捕获组。
      • “这只是对非贪婪的古怪实现”这根本没有帮助。 (word2)? 是一个 greedy 匹配。 (word2)?? 不会贪心
      • 知道了。虽然我没有在 OP 中规定,但捕获组保持一致会很好。匹配“word2”时,结果位于第 1、2 和 3 组中,而不匹配的结果位于第 4 组和第 5 组中。不过,我感谢您的回答和解释。
      • @Borodin 我已更正并更改了以前不正确的分析。我还将删除我之前的评论,其中我对一个实际上不支持我的主张的测试用例应用了相同的错误推理。感谢您的洞察力。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-04-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多