PHP REGEX回溯问题

时间:2012-07-04 02:34:44

标签: php regex optimization pcre backtracking

我正在尝试在preg_match_all

中使用PHP中的reg exp
/\d+ (?:<[^>]+>)(?:<[^>]+>)(\S+.*\S+)(?:<[^>]+>)\s*(\S+) (?:L|R)\s*\w* \w*\s*(?:\w+\s*){14}(\d+)\s*(\d)\s*(\d*\xA0*\d{3}\xA0*\d{3})/is

有一些数据样本:

38 <A NAME="Philip McRae"><A HREF="xtrastats.html#Philip McRae">Philip McRae</A>            C L  OK    58 71 69 49 33 89 71 45 48 69 50 35 32 61   21   3    787 000
43 <A NAME="Alexander Nikulin"><A HREF="xtrastats.html#Alexander Nikulin">Alexander Nikulin</A>       C L  OK    41 68 71 40 28 90 67 29 31 60 31 37 34 50   26   0      0 000 <a href="http://www.hockeydb.com/ihdb/stats/pdisplay.php?pid=78680" target="_blank">HDB</a>
20 <A NAME="Christian Hanson"><A HREF="xtrastats.html#Christian Hanson">Christian Hanson</A>        C R  OK    57 72 71 54 33 79 70 42 45 71 46 40 36 60   25   1    875 000 <a href="http://www.hockeydb.com/ihdb/stats/pdisplay.php?pid=73824" target="_blank">HDB</a>

我有大约1500行。

我需要匹配这个:

Philip McRae, C, 21, 3, 787 000 (Name, Position, Age, Contract Lenght, Salary)

每次运行我的代码时,都会出现致命错误:30秒的最大执行时间超出错误。

经过一些搜索后,我在我的脚本顶部添加了这一行,但这并没有解决我的问题

ini_set("pcre.backtrack_limit",10000000);

任何人都可以帮我使用这个reg exp进行一些优化吗?

问候。

帕特里克

4 个答案:

答案 0 :(得分:3)

我不会尝试重写你的正则表达式,因为我们没有这些要求,但这里的主要问题是你的名字组:

(\S+.*\S+)

.*贪婪。这意味着它会消耗尽可能多的东西,包括你期望表达的其余部分匹配的内容,并且它并不止于此。由于您拥有/s模式修饰符,因此该点也会匹配换行符,从而允许.*在尝试匹配\S并开始其长回溯之旅之前使用整个文件。

一种解决方案是使.*?保持懒惰,即.*?,但由于您知道名称包含在元素中,因此您可以简单地使用否定字符类组:

([^<]*)

这应该可以解决您的问题,但您可能不希望在这种情况下使用/s模式修饰符,或者至少应该在行模式中添加行锚的起点和终点。您还应该尝试限制使用*

请参阅: Catastrophic backtrackingWatch out for greediness

答案 1 :(得分:1)

即使你有大约1 500行,你想要解决的问题是每一行。

如果您能够逐行处理输入,那么您已经将问题减少了相当多。

$file = new SplFileObject($path);
foreach ($file as $i => $line) {
    printf("#%'0-4d: %s\n", $i, $line);
}

这只是一个例子,当然,正则表达式引擎本身可以使用它的多线修改器(m)做类似的事情。但是,如果您执行上述操作,则可以break直接进行第一行测试:

foreach ($file as $i => $line) {
    printf("#%'0-4d: %s\n", $i, $line);
    $pattern = '(^\d++ <A NAME="([^"]++)"><A HREF="xtrastats.html#Philip McRae">Philip McRae</A>            C L  OK    58 71 69 49 33 89 71 45 48 69 50 35 32 61   21   3    787 000)$';
    $r = preg_match($pattern, $line, $matches);
    if (FALSE === $r) {
        throw new Exception(sprintf("Regex failed (%d)", preg_last_error());
    }
    if (!$r) {
        throw new Exception(sprintf("Pattern does not match."));
    }
    var_dump($matches);
    if ($i > 0) break; # exit foreach after X lines.
}
echo "Done.\n";

正如您在此示例中所看到的那样,模式尚未完成,但您可以逐步替换整行。

它还使用了一个锚点作为字符串开头(^)和字符串结尾($)。

它还使用占有量词(+),这样如果那些不匹配,就不会发生回溯(类似于原子分组,但更容易编写)。

继续逐步改进正则表达式模式。如果正则表达式不编译,则抛出异常。以及当一条线不匹配时。

你应该在一段时间后完成你的工作,从长远来看,改进错误处理并创建一些稳定的高效代码。

答案 2 :(得分:1)

@hakre和@bodhizero

根据您的输入和帮助,我将我的正则表达式修改为:

\d{1,2}+ (?:<[^>]++>)(?:<[^>]++>)([^<]*+)(?:<[^>]++>)\s*+(\S{1,2}+) (?:L|R)\s*+\w*+ \w*+\s*+(?:\w++\s*+){14}(\d{1,2}+)\s*+(\d)\s*(\d*+.*?\d{0,3}+.*?\d{3}+)(?: <[^>]++>[^<]*+<[^>]++>)*?

结果:在大约2秒内解析整个文件!!!

我使用Regexbuddy程序并帮助了我很多。

我希望我能够提出两个答案,但我不能

答案 3 :(得分:0)

您需要限制用于正则表达式匹配的数据量,或更改set_time_limitmemory_limit限制。

preg_match_all()非常占用CPU,根据服务器CPU的强大程度,它可能会导致执行时间和内存问题。

一个解决方案是将其添加到代码顶部:

set_time_limit(0);
ini_set('memory_limit', '128M');

您的另一个选择是将脚本限制为每页加载少preg_match_all()次匹配。