在Perl中使用正则表达式从最后一个匹配项开始匹配

12

我有一个像这样的文本:

hello world /* select a from table_b
*/ some other text with new line cha
racter and there are some blocks of 
/* any string */ select this part on
ly 
////RESULT rest string

这段文本跨越多行,我需要提取从最后一个出现的 "*/" 到 "////RESULT" 之间的内容。在这种情况下,结果应该是:

 select this part on
ly 

如何在Perl中实现这个功能?

我尝试了\\\*/(.|\n)*////RESULT,但这将从第一个“*/”开始匹配。


你尝试过什么? - squiguy
我尝试过 \/(.|\n)////RESULT 但这将从第一个 "*/" 开始。 - Peiti Li
3个回答

21
在这种情况下一个有用的技巧是使用贪婪模式 .* 来修饰正则表达式,这样会尝试在匹配余下的模式前匹配尽可能多的字符。因此,可以这样写:
my ($match) = ($string =~ m!^.*\*/(.*?)////RESULT!s);

让我们将这个模式分解为其组成部分:

  • ^.*从字符串开头开始匹配尽可能多的字符。(修饰符s允许.匹配换行符)。虽然起始位置锚点^不是必须的,但它确保如果匹配失败,正则表达式引擎不会浪费太多时间回溯。

  • \*/只匹配文字字符串*/

  • (.*?)匹配并捕获任意数量的字符;?使其非贪婪,因此它优先选择尽量少的字符来匹配,以防存在多个位置可以让剩余的正则表达式匹配。

  • 最后,////RESULT只匹配它自己。

由于该模式包含大量斜线,并且由于我想避免leaning toothpick syndrome(倾斜牙签综合症),我决定使用替代的正则表达式分隔符。感叹号(!)是一个受欢迎的选择,因为它们不会与任何正常的正则表达式语法冲突。


编辑:根据下面ikegami的讨论,如果您想在更长的正则表达式中使用此正则表达式作为子模式,并且如果您想保证由(.*?)匹配的字符串永远不会包含////RESULT,那么您应该将正则表达式的这些部分包装在独立的(?>)子表达式中,就像这样:

my $regexp = qr!\*/(?>(.*?)////RESULT)!s;
...
my $match = ($string =~ /^.*$regexp$some_other_regexp/s);

(?>)会导致其内部的正则表达式失败,而不是接受次优匹配(即延伸到第一个子字符串匹配////RESULT之外), 即使这意味着后续的正则表达式将无法匹配。


1
我讨厌 .*?。它不能保证不匹配你希望不匹配的内容。在这个特定的模式中,它运行得很好,除了如果 ////RESULT 后面出现 */,它就无法匹配。不过这也许是件好事。 - ikegami
@ikegami:实际上,.*?的行为是完全定义良好的;请参见perlre中的“Combining RE Pieces” - Ilmari Karonen
我从未说过它没有定义。我非常熟悉它的作用和用途,但这两者并不相同。给我一个带有两个 .*? 的模式,我会向你展示一个错误或无用的 ?(除了性能方面)。 - ikegami
尽管我认为你的答案难以维护,但我还是给了你的解释一个+1。 - ikegami
啊,所以你只是觉得 .*? 不直观?如果你希望它做的是“匹配除了紧接在它后面的模式之外的任何内容”,那么不,它并不会这样做(尽管你可以通过将其和其后面的模式包装在 (?>) 中来获得这种效果)。我并不是真的不同意你的看法,我只是认为对于这个问题的正确答案不是“不要使用 .*?”,而是“除非你理解它的作用,否则不要使用 .*?”。 - Ilmari Karonen
不直观?我想“它被用来做一件事,但实际上什么也没做”可以归为不直观的定义,但我发现在翻译中失去了某些东西。 - ikegami

5
(?:(?!STRING).)*

通配符 * 匹配不包含 STRING 的任意数量字符。它类似于字符集合 [^a],但是用于字符串。

如果你知道某些输入不会出现(比如 Kenosis 和 Ilmari Karonen 所做的),则可以采取捷径,但这就是匹配指定内容的方法:

my ($segment) = $string =~ m{
    \*/
    ( (?: (?! \*/ ). )* )
    ////RESULT
    (?: (?! \*/ ). )*
    \z
}xs;

如果您不介意在////RESULT之后出现*/,则以下方法是最安全的:
my ($segment) = $string =~ m{
    \*/
    ( (?: (?! \*/ ). )* )
    ////RESULT
}xs;

您没有指定如果在最后一个*/之后有两个////RESULT应该发生什么。上面的匹配一直持续到最后一个。如果您想匹配到第一个,可以使用

my ($segment) = $string =~ m{
    \*/
    ( (?: (?! \*/ | ////RESULT ). )* )
    ////RESULT
}xs;

4
这里有一个选项:
use strict;
use warnings;

my $string = <<'END';
hello world /* select a from table_b
*/ some other text with new line cha
racter and there are some blocks of 
/* any string */ select this part on
ly 
////RESULT
END

my ($segment) = $string =~ m!\*/([^/]+)////RESULT$!s;

print $segment;

输出:

 select this part on
ly 

谢谢,你能解释一下m后面的!和[^/]是什么吗? - Peiti Li
这不允许在 */////RESULT 之间有任何 /。(也不能保证与最后一个 */ 匹配,但这可能不是问题。) - ikegami
@PeitiPeterLi - 由于字符串中含有斜杠,“m!!”用于匹配。请注意,ikegami使用了“m {}”。'[]'表示字符集,“[^/ ]”表示不是('^')正斜杠。 - Kenosis
@ikegami - 是的 - 你提到正则表达式在捕获时排除了任何/是一个很好的观点。我过于简化了这个问题。非常感谢您的评论。谢谢。 - Kenosis

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接