正则表达式PREG_BACKTRACK_LIMIT_ERROR在提取真正长文本非贪婪时

时间:2018-05-26 06:16:16

标签: php regex limit pcre backtracking

我有一个表格的字符串:

  

一些文字[打开]真的很长文... [结束]更多文字[关闭]更多文字

我想从带有正则表达式的字符串中提取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]向前剥离文本来实现它。我想更好地了解幕后发生的事情,为什么需要进行大量的回溯,以及是否有一种方法可以修改正则表达式以执行任务。

我非常感谢任何见解!

1 个答案:

答案 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\]结束。