PHP正则表达式崩溃apache

时间:2012-08-07 16:58:01

标签: php regex windows apache

我有一个匹配模板系统的正则表达式,不幸的是,它似乎会在一些简单易懂的查找中崩溃apache(它在Windows上运行)。我已经研究了这个问题,并提出了一些提高堆栈大小的建议,其中没有一个似乎有用,我也不喜欢通过提高限制来处理这些问题,因为它通常只是推动了bug走向未来。

无论如何,任何关于如何改变正则表达式以使其不太可能被污染的想法?

我的想法是捕获最里面的块(在这种情况下为{block:test}This should be caught first!{/block:test}),然后我将str_replace out起始/结束标记并通过正则表达式重新运行整个事务直到没有块左

正则表达式:

~(?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">&laquo;</div>
    <div class="s_sponsors">
        <ul>
            {block:sponsors}
            <li>
                <a href="{var:url}" target="_blank">
                    <img src="image/160x126/{var:image}" alt="{var:name}" title="{var:name}" />
                </a>
            {block:test}This should be caught first!{/block:test}
            </li>
            {/block:sponsors}
        </ul>
    </div>
    <div class="s_next">&raquo;</div>
</div>

我想这是一个很长的镜头。 :(

3 个答案:

答案 0 :(得分:4)

您可以使用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()通过steam创建自己的堆栈,以从堆中获取内存。

或者您可以从pcre.backtrack_limit更改pcre.recursion_limitphp.ini(http://www.php.net/manual/en/pcre.configuration.php)

答案 1 :(得分:4)

试试这个:

'~(?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。我逃脱了所有大括号({}),因为我觉得这样更容易阅读。


编辑:如果您想匹配最里面的命名块,请更改正则表达式的中间部分:

(?P<contents>
  [^{]*(?:\{(?!/block:(?P=name)\})[^{]*)*
)

......对此(正如@Kobi在评论中所建议的那样):

(?P<contents>
  [^{]*(?:\{(?!/?block:[a-z0-9\s_-]+\})[^{]*)*
)

最初,(?P<opening>...)组会抓住它看到的第一个开始标记,然后(?P<contents>..)组会消耗任何内容 - 包括其他标记 - 只要它们不是结束标记匹配(?P<opening>...)组找到的匹配项。 (然后(?P<closing>...)组会继续使用它。)

现在,(?P<contents>...)群组拒绝匹配任何标记,无论打开或关闭(请注意开头的/?),无论名称是什么。因此,正则表达式最初开始匹配{block:sponsors}标记,但是当它遇到{block:test}标记时,它会放弃该匹配并返回搜索开始标记。它会在{block:test}标记处重新开始,这次在找到{/block:test}结束标记时成功完成匹配。

这听起来效率低下,但事实并非如此。我之前描述的技巧,摒弃了非支撑,淹没了这些错误开始的效果。你几乎在每个位置都做了负面的预测,现在你只在遇到{时才做一个。你甚至可以使用占有量词,就像@godspeedlee建议的那样:

(?P<contents>
  [^{]*+(?:\{(?!/?block:[a-z0-9\s_-]+\})[^{]*+)*+
)

...因为你知道它永远不会消耗它后来必须回馈的东西。这样可以加快速度,但实际上并不是必需的。

答案 2 :(得分:4)

解决方案是否必须是单个正则表达式?更有效的方法可能只是查找{/block:的第一次出现(可能是简单的字符串搜索或正则表达式),然后从该点向后搜索以找到其匹配的开始标记,适当地替换范围重复,直到没有更多的块。如果每次都从模板顶部开始寻找第一个结束标记,那么这将为您提供最深层嵌套的块。

镜像算法也可以正常工作 - 查找最后一个开始标记,然后从那里搜索相应的结束标记:

<?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;