我有一个表格的字符串:
一些文字[打开]真的很长文... [结束]更多文字[关闭]更多文字
我想从带有正则表达式的字符串中提取Really Really Long Text ...直到第一次[结束]。
如果我像这样做正则表达式:
$pMatch = "'\[Opening\](.+)\[Closing\]'si";
这让我:
真的很长文... [关闭]更多文字
我也可以这样做不贪心:
$pMatch = "'\[Opening\](.+?)\[Closing\]'si";
哪个有效,并给我正确的输出:
真的很长文......
但是,如果我用实际真正很长的文本替换“Really Really Long Text ...”,它不起作用,而是收到PREG_BACKTRACK_LIMIT_ERROR。如果我使用贪婪的正则表达式,我不会收到错误。我只是得到了错误的输出,如第一种情况。
我一直在使用正则表达式,但是这个让我难过。有没有办法让它与正则表达式一起使用,或者是不适合此任务的正则表达式?
以下是重现此问题的PHP代码:
<?php
$sShortString = "Some Text[Opening]Really Really Long Text...[Closing]More Text[Closing]Even More Text";
$sLongString = "Some Text[Opening]".str_repeat("BLAH", 1000000)."[Closing]More Text[Closing]Even More Text";
$pGreedyMatch = "'\[Opening\](.+)\[Closing\]'si";
$pNonGreedyMatch = "'\[Opening\](.+?)\[Closing\]'si";
header("Content-Type: text/plain");
if (preg_match($pGreedyMatch, $sShortString, $aMatch)) {
echo "Greedy Match:\n";
print_r($aMatch);
}
if (preg_match($pNonGreedyMatch, $sShortString, $aMatch)) {
echo "Non-Greedy Match:\n";
print_r($aMatch);
}
if (preg_match($pGreedyMatch, $sLongString, $aMatch)) {
echo "Greedy Match:\n";
echo "Length: ".strlen($aMatch[1])."\n";
}
if (preg_match($pNonGreedyMatch, $sLongString, $aMatch)) {
echo "Non-Greedy Match:\n";
echo strlen($aMatch[1]);
} else {
echo "Non-Greedy Doesn't Match!\n";
}
$iLastError = preg_last_error();
if ($iLastError == PREG_BACKTRACK_LIMIT_ERROR) {
echo "It's because the backtrack limit was exceeded!\n";
}
?>
我得到了输出:
Greedy Match:
Array
(
[0] => [Opening]Really Really Long Text...[Closing]More Text[Closing]
[1] => Really Really Long Text...[Closing]More Text
)
Non-Greedy Match:
Array
(
[0] => [Opening]Really Really Long Text...[Closing]
[1] => Really Really Long Text...
)
Greedy Match:
Length: 4000018
Non-Greedy Doesn't Match!
It's because the backtrack limit was exceeded!
我通过使用贪婪的正则表达式并使用其他代码从[Closing]向前剥离文本来实现它。我想更好地了解幕后发生的事情,为什么需要进行大量的回溯,以及是否有一种方法可以修改正则表达式以执行任务。
我非常感谢任何见解!
答案 0 :(得分:0)
非贪婪量词有一个成本,因为每次读取一个字符时,都必须检查该模式的结尾。
在上述模式中,每次.
中的(.+?)
匹配时,都会检查以下字符是否与[Closing]
匹配。每次发生这种情况并且它都不匹配时,它必须回溯并继续搜索。这就是回溯限制它用完的原因。
模式可以像这样重写:
'\[Opening\]([^\[]*(?:\[(?!Closing)[^\[]*)*)(*SKIP)\[Closing\]'si
让我们逐一检查这个模式以理解它。
1)我们以\[Opening\]
打开。此模式与开始标记匹配。
2)由于我们的模式本身并不重复,因此()(*SKIP)
指令用作进一步的优化。这意味着如果我们不匹配模式,那么我们将从我们正在寻找的位置结束时重新开始搜索。默认行为将开始再次搜索下一个字符。
为了更好地理解这一点,假设我们的字符串是sometimes we get [Close to matching
。当我们到达[
时,我们会先扫描[Clos
,然后才能得出结论,这实际上并不是我们想要的模式。通常情况下,我们会回溯,然后再次开始查看Close
。但是,(*SKIP)
允许我们继续在e to matc
进行搜索。
3)在我们的括号中,我们从模式[^\[]*
开始,它允许我们匹配尽可能多的不是[
的字符。 ^
表示不是,\[
表示[
,[]
表示字符集。 *
允许它重复多次。
4)现在,我们有(?:)*
。 ()
允许我们指定一个字符串,而?:
表示不会保存,*
允许它重复多次(根本不包括任何时间)
5)该字符串中的第一个字符是\[
或我们期望的[
作为结束标记的一部分。
6)接下来,我们有(?!Closing\])
。 (?!)
是negative lookahead。前瞻意味着解析器将查看下一个字符,并且匹配或不匹配而不消耗字符。这使得我们可以匹配某些内容,只要它不是Closing]
而不实际使用它。
7)我们还有另一个[^\[]*
,它允许我们在未能前瞻后继续吃人物。这允许我们在得到类似[Clos
之后继续使用字符串。
8)最后,我们的正则表达式以\[Closing\]
结束。