PHP正则表达式崩溃apache
Posted
技术标签:
【中文标题】PHP正则表达式崩溃apache【英文标题】:PHP regex crashing apache 【发布时间】:2012-08-04 17:54:15 【问题描述】:我有一个匹配模板系统的正则表达式,不幸的是,它似乎在一些适度琐碎的查找中使 apache(它在 Windows 上运行)崩溃。我已经研究了这个问题,并且有一些关于增加堆栈大小等的建议,但这些建议似乎都不起作用,而且我真的不喜欢通过增加限制来处理这些问题,因为它通常只是将 bug 推到了未来。
关于如何更改正则表达式以使其不太可能出错的任何想法?
我们的想法是捕捉最里面的块(在本例中为 block:testThis should be caught first!/block:test
),然后我将 str_replace 出开始/结束标签并通过正则表达式重新运行整个事情,直到没有剩下的块为止。
正则表达式:
~(?P<opening>(?P<inverse>[!])?block:(?P<name>[a-z0-9\s_-]+))(?P<contents>(?:(?!/?block:[0-9a-z-_]+).)*)(?P<closing>/block:\3)~ism
示例模板:
<div class="f_sponsors s_banners">
<div class="s_previous">«</div>
<div class="s_sponsors">
<ul>
block:sponsors
<li>
<a href="var:url" target="_blank">
<img src="image/160x126/var:image" title="var:name" />
</a>
block:testThis should be caught first!/block:test
</li>
/block:sponsors
</ul>
</div>
<div class="s_next">»</div>
</div>
我想这是一个长镜头。 :(
【问题讨论】:
听起来很奇怪,我曾经有一个正则表达式不断吃掉一个特定的 Apache 实例,而S
(大写!)标志修复了它。我猜这是一个未报告的内存泄漏或其他东西,研究过程导致它被避免。远射,但值得一试,我会说...
@DaveRandom,我曾经遇到过同样的问题,但修复方法相同!让我们看看这是否适用于 OP
另一个想法是转义正则表达式中的文字
字符可能会有所帮助。它们在技术上是元字符,虽然 PCRE 似乎对未转义的花括号相当宽容,但如果您正确地转义它们,它可能会减少工作量。另外为什么要使用命名的捕获组而不是在反向引用中使用名称? /block:\3
=> /block:(?P=name)
。这对于您的正则表达式尤其如此,因为<inverse>
是可选的,在这种情况下<name>
将是\2
,而不是\3
你的意思是在 ~ism 部分的 s 标志?因为如果结束标签与没有 s 的开始标签在不同的行上,它就不起作用?不过后面的参考名称很好。
感谢大家迄今为止在这方面的帮助!
【参考方案1】:
您可以使用atomic group: (?>...)
或possessive quantifiers: ?+ *+ ++..
来抑制/限制回溯并通过unrolling loop
技术加速匹配。我的解决方案:
\block:(\w++)\([^<]++(?:(?!\\/?block:\1\b)[<][^<]*+)*+)\/block:\1\
我已经从 http://regexr.com?31p03 测试过。
匹配block:sponsors.../block:sponsors
:\block:(sponsors)\([^<]++(?:(?!\\/?block:\1\b)[<][^<]*+)*+)\/block:\1\
http://regexr.com?31rb3
匹配block:test.../block:test
:\block:(test)\([^<]++(?:(?!\\/?block:\1\b)[<][^<]*+)*+)\/block:\1\
http://regexr.com?31rb6
另一种解决方案:
在 PCRE 源代码中,您可以删除 config.h
的注释:/* #undef NO_RECURSE */
从config.h
复制以下文本:
PCRE 使用递归函数调用来处理匹配时的回溯。这有时可能是堆栈大小有限的系统的问题。
定义 NO_RECURSE 以获取在 match() 函数中不使用递归的版本;相反,它通过使用 pcre_recurse_malloc() 从堆中获取内存来创建自己的堆栈。
或者您可以将 pcre.backtrack_limit
和 pcre.recursion_limit
更改为 php.ini
(http://www.php.net/manual/en/pcre.configuration.php)
【讨论】:
但是,当您将块放入块中时,它只会捕获最外面的块,而不是最里面的块。我已经更新了一些问题!【参考方案2】:试试这个:
'~(?P<opening>\(?P<inverse>[!])?block:(?P<name>[a-z0-9\s_-]+)\)(?P<contents>[^]*(?:\(?!/block:(?P=name)\)[^]*)*)(?P<closing>\/block:(?P=name)\)~i'
或者,以可读的形式:
'~(?P<opening>
\
(?P<inverse>[!])?
block:
(?P<name>[a-z0-9\s_-]+)
\
)
(?P<contents>
[^]*(?:\(?!/block:(?P=name)\)[^]*)*
)
(?P<closing>
\
/block:(?P=name)
\
)~ix'
最重要的部分在(?P<contents>..)
组中:
[^]*(?:\(?!/block:(?P=name)\)[^]*)*
一开始,我们唯一感兴趣的字符是左大括号,所以我们可以用[^]*
吞下任何其他字符。只有在我们看到 之后,我们才会检查它是否是
/block
标记的开头。如果不是,我们继续使用它并开始扫描下一个,并根据需要重复。
使用 RegexBuddy,我通过将光标放在 block:sponsors
标记的开头并进行调试来测试每个正则表达式。然后我从结束 /block:sponsors
标记中删除了结束括号以强制匹配失败并再次调试它。你的正则表达式成功了 940 步,失败了 2265 步。我的成功了 57 步,失败了 83 步。
在旁注中,我删除了 s
修饰符,因为我没有使用点 (.
) 和 m
修饰符,因为它从来不需要。根据@DaveRandom 的优秀建议,我还使用了命名的反向引用(?P=name)
而不是\3
。我避开了所有大括号( 和
),因为我发现这样更容易阅读。
编辑:如果你想匹配 innermost 命名块,改变正则表达式的中间部分:
(?P<contents>
[^]*(?:\(?!/block:(?P=name)\)[^]*)*
)
...对此(正如@Kobi 在他的评论中所建议的那样):
(?P<contents>
[^]*(?:\(?!/?block:[a-z0-9\s_-]+\)[^]*)*
)
最初,(?P<opening>...)
组会抓取它看到的第一个开始标签,然后(?P<contents>..)
组会消耗任何东西——包括其他标签——只要它们不是匹配找到的结束标签由(?P<opening>...)
组。 (然后(?P<closing>...)
组将继续使用它。)
现在,(?P<contents>...)
组拒绝匹配 any 标记,打开或关闭(注意开头的 /?
),无论名称是什么。因此,正则表达式最初开始匹配 block:sponsors
标记,但当它遇到 block:test
标记时,它会放弃匹配并返回搜索开始标记。它再次从block:test
标记处开始,这一次在找到/block:test
结束标记时成功完成匹配。
这样描述它听起来效率低下,但事实并非如此。我之前描述的技巧,啜饮非牙套,淹没了这些错误开始的影响。在几乎每个位置都进行负前瞻的地方,现在只有在遇到 时才会这样做。正如@godspeedlee 建议的那样,您甚至可以使用所有格量词:
(?P<contents>
[^]*+(?:\(?!/?block:[a-z0-9\s_-]+\)[^]*+)*+
)
...因为你知道它永远不会消耗它以后必须回馈的任何东西。这会加快一些速度,但实际上并不是必需的。
【讨论】:
我已经稍微更新了这个问题,因为它只捕获了最外层的块。但是你的仍然不需要 ~sm 部分,所以我认为它在正确的轨道上! 非常好!我认为我理解OP想要什么......我认为“嵌套块”是指任何名称的嵌套块,而不仅仅是同名,它们被迭代替换。所以1 2 /2 /1
应该在1
之前捕获2
。如果是这种情况,您可以轻松地将中间部分从[^]*(?:\(?!/block:(?P=name)\)[^]*)*
更改为[^]*(?:\(?!/?block:[a-z0-9\s_-]+\)[^]*)*
- regexr.com?31qsf
与 html 【参考方案3】:
解决方案必须是单个正则表达式吗?一种更有效的方法可能是简单地查找/block:
的第一次出现(可能是简单的字符串搜索或正则表达式),然后从该点向后搜索以找到其匹配的开始标签,适当地替换跨度并重复直到没有更多的块。如果每次都从模板顶部开始查找 first 结束标记,那么这将为您提供嵌套最深的块。
镜像算法也可以工作 - 查找最后一个开始标签,然后从那里向前搜索相应的结束标签:
<?php
$template = //...
while(true)
$last_open_tag = strrpos($template, 'block:');
$last_inverted_tag = strrpos($template, '!block:');
// $block_start is the index of the '' of the last opening block tag in the
// template, or false if there are no more block tags left
$block_start = max($last_open_tag, $last_inverted_tag);
if($block_start === false)
// all done
break;
else
// extract the block name (the foo in block:foo) - from the character
// after the next : to the character before the next , inclusive
$block_name_start = strpos($template, ':', $block_start) + 1;
$block_name = substr($template, $block_name_start,
strcspn($template, '', $block_name_start));
// we now have the start tag and the block name, next find the end tag.
// $block_end is the index of the '' of the next closing block tag after
// $block_start. If this doesn't match the opening tag something is wrong.
$block_end = strpos($template, '/block:', $block_start);
if(strpos($template, $block_name.'', $block_end + 8) !== $block_end + 8)
// non-matching tag
print("Non-matching tag found\n");
break;
else
// now we have found the innermost block
// - its start tag begins at $block_start
// - its content begins at
// (strpos($template, '', $block_start) + 1)
// - its content ends at $block_end
// - its end tag ends at ($block_end + strlen($block_name) + 9)
// [9 being the length of '/block:' plus '']
// - the start tag was inverted iff $block_start === $last_inverted_tag
$template = // do whatever you need to do to replace the template
echo $template;
【讨论】:
以上是关于PHP正则表达式崩溃apache的主要内容,如果未能解决你的问题,请参考以下文章