PHP正则表达式导致Apache崩溃

10

我有一个正则表达式用于匹配模板系统,但不幸的是,在某些相对简单的查找情况下,它似乎会导致 apache 崩溃(在 Windows 上运行)。我已经研究了这个问题,并有一些建议可以提高堆栈大小等,但似乎都没有效果,而且我不喜欢通过提高限制来处理这样的问题,因为这通常只会将错误推到未来。

无论如何,你们有什么想法来改变正则表达式,使其更不容易出错吗?

我的想法是捕获最内部的块(在这种情况下是{block:test}This should be caught first!{/block:test}),然后将起始/结束标记替换掉,并通过正则表达式再次运行整个过程,直到没有剩余块为止。

正则表达式:

~(?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>

我猜这只是徒劳的希望。 :(


4
听起来很奇怪,我曾经有一个正则表达式一直在影响一个特定的Apache实例,而S(大写!)标志解决了这个问题。我猜测这可能是一个未报告的内存泄漏或其他问题,而研究过程导致了它被避免。虽然可能性很小,但值得一试,我想说... - DaveRandom
1
@DaveRandom,我曾经遇到过同样的问题,使用了相同的解决方法!让我们看看这是否适用于OP。 - Mike Mackintosh
2
另一个想法是对正则表达式中的文字 {} 进行转义可能会有所帮助。虽然 PCRE 似乎对未转义的花括号相当宽容,但如果您正确转义它们,可能会减轻其负担。此外,为什么要使用命名捕获组而不是在反向引用中使用名称?/block:\3 => /block:(?P=name)。这对于您的正则表达式尤其重要,因为 <inverse> 是可选的,此时 <name> 将为 \2,而不是 \3。 - DaveRandom
你是指针对 s 标志的 ~ism 部分吗?如果没有 s,如果结束标记与开始标记不在同一行,则无法正常工作。但是,回溯名称方面很好地发现了问题。 - Meep3D
1
只是想感谢大家迄今为止对此事的帮助! - Meep3D
在我看来,如果你使用类似于\{/?block:(\w+)\}的模式(例如-但是带有更多组),并使用正则表达式进行词法分析而不是完整解析,那么你将会更容易。然后,您可以在代码中匹配适当的开始/结束标记,我认为这将更容易。这也可以使您摆脱迭代替换的需要-您将能够在单个扫描中完成它。只是一个想法。 - Kobi
3个回答

4
这个解决方案必须是单个正则表达式吗?更有效的方法可能只是查找第一个出现的{/block:(可以是简单字符串搜索或正则表达式),然后从该点向后搜索以找到其匹配的开放标签,适当地替换span并重复,直到没有更多块。 如果每次你从模板顶部开始寻找第一个关闭标签,那么这将给你最深嵌套的块。
镜像映像算法同样适用 - 查找最后一个开放标记,然后从那里向前搜索相应的关闭标记。
<?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;

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 修饰符,因为它从未被使用过。我还使用了命名反向引用 (?P=name),而不是 \3,这是 @DaveRandom 的建议。我也转义了所有括号({}),因为我觉得这样更容易阅读。


编辑: 如果要匹配最内层的命名块,请将正则表达式的中间部分更改为:

(?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_-]+\})[^{]*+)*+
)

因为您知道它永远不会消耗任何将来必须归还的东西。这会加速一些操作,但实际上并不必要。


我稍微更新了一下问题,因为它只捕获最外层的块。但是你的代码仍然不需要 ~sm 部分,所以我认为它走在了正确的轨道上! - Meep3D
2
非常好!我认为我理解了OP想要什么... 我认为他们所说的“嵌套块”是指任何名称的嵌套块,而不仅仅是相同名称的嵌套块,这些块是迭代替换的。因此,{1} {2} {/2} {/1}应该在1之前捕获2。如果是这样的话,你可以很容易地将中间部分从[^{]*(?:\{(?!/block:(?P=name)\})[^{]*)*更改为[^{]*(?:\{(?!/?block:[a-z0-9\s_-]+\})[^{]*)* - http://regexr.com?31qsf - Kobi
类似的HTML<select>匹配问题:#<select(?:\s[^>]*)?>(?:(?!</select>).)*</select>#s崩溃了,而#<select(?:\s[^>]*)?>[^<]*(?:<(?!/select>)[^<]*)*</select>#s却可以工作。虽然添加U非贪婪修饰符也可以防止崩溃;但我不确定为什么,因为我认为这会导致更多的回溯和低效率——如果不是这样的话,我可能会写#<select(?:\s.*)?>.*</select>#Us并完成它... - Jake

4
您可以使用原子组:(?>...)占有量词:?+ *+ ++..来抑制/限制回溯并通过展开循环技术加快匹配速度。 我的解决方案:
\{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()从堆中获取内存来创建自己的堆栈。
或者您可以从php.ini(http://www.php.net/manual/en/pcre.configuration.php)更改pcre.backtrack_limitpcre.recursion_limit

然而,当您将一个块放在另一个块中时,它只会捕获最外层的块,而不是最内层的块。我已经稍微更新了问题! - Meep3D

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接