PHP PREG_JIT_STACKLIMIT_ERROR - 正则表达式效率低下的错误

5
我在使用 preg_replace_callback() 函数处理较长字符串时,出现了 PREG_JIT_STACKLIMIT_ERROR 错误。当匹配的字符数超过 2000 个时,该函数将无法正常工作(不是指字符串长度)。我已经阅读了一些资料,发现这是由于正则表达式效率低下导致的,但我无法简化我的正则表达式。以下是我的正则表达式: /\{@([a-z0-9_]+)-((%?[a-z0-9_]+(:[a-z0-9_]+)*)+)\|(((?R)|.)*)@\}/Us 它应该匹配如下字符串:
1) {@if-statement|echo this|echo otherwise@} 2) {@if-statement:sub|echo this|echo otherwise@} 3) {@if-statement%statament2:sub|echo this@} 以及像这样嵌套的:
4) {@if-statement|echo this| {@if-statement2|echo this|echo otherwise@} @} 我尝试将其简化为: /\{@([a-z0-9_]+)-([a-z0-9_]+)\|(((?R)|.)*)@\}/Us 但似乎错误是由 (((?R)|.)*) 部分引起的。有什么建议吗?
以下是测试代码:
$string = '{@if-is_not_logged_homepage|
<header id="header_home">
    <div class="in">
        <div class="top">
            <h1 class="logo"><a href="/"><img src="/img/logo-home.png" alt=""></a></h1>
            <div class="login_outer_wrapper">
                <button id="login"><div class="a"><i class="stripe"><i></i></i>Log in</div></button>
                <div id="login_wrapper">
                    <form method="post" action="{^login^}" id="form_login_global">
                        <div class="form_field no_description">
                            <label>{!auth:login_email!}</label>
                            <div class="input"><input type="text" name="form[login]"></div>
                        </div>
                        <div class="form_field no_description password">
                            <label>{!auth:password!}</label>
                            <div class="input"><input type="password" name="form[password]"></div>
                        </div>
                        <div class="remember">
                            <input type="checkbox" name="remember" id="remember_me_check" checked>
                            <label for="remember_me_check"><i class="fa fa-check" aria-hidden="true"></i>Remember</label>
                        </div>
                        <div class="submit_box">
                            <button class="btn btn_check">Log in</button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
        <div class="content clr">
            <div class="main_menu">
                <a href="">
                    <i class="ico a"><i class="fa fa-lightbulb-o" aria-hidden="true"></i></i>
                    <span>Idea</span>
                    <div>&nbsp;</div>
                </a>
                <a href="">
                    <i class="ico b"><i class="fa fa-user" aria-hidden="true"></i></i>
                    <span>FFa</span>
                </a>
                <a href="">
                    <i class="ico c"><i class="fa fa-briefcase" aria-hidden="true"></i></i>
                    <span>Buss</span>
                </a>
            </div>
            <div class="text_wrapper">

                <div>
                    <div class="register_wrapper">
                        <a id="main_register" class="btn register">Załóż konto</a>
                        <form method="post" action="{^login^}" id="form_register_home">
                            <div class="form_field no_description">
                                <label>{!auth:email!}</label>
                                <div class="input"><input type="text" name="form2[email]"></div>
                            </div>
                            <div class="form_field no_description password">
                                <label>{!auth:password!}</label>
                                <div class="input tooltip"><input type="password" name="form2[password]"><i class="fa fa-info-circle tooltip_open" aria-hidden="true" title="{!auth:password_format!}"></i></div>

                            </div>
                            <div class="form_field terms no_description">
                                <div class="input">
                                    <input type="checkbox" name="form2[terms]" id="terms_check">
                                    <label for="terms_check"><i class="fa fa-check" aria-hidden="true"></i>Agree</label>
                                </div>
                            </div>
                            <div class="form_field no_description">
                                <div class="input captcha_wrapper">
                                    <div class="g-recaptcha" data-sitekey="{%captcha_public_key%}"></div>
                                </div>
                            </div>
                            <div class="submit_box">
                                <button class="btn btn_check">{!auth:register_btn!}</button>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</header>
@}';

$if_counter = 0;

$parsed_view = preg_replace_callback( '/\{@([a-z0-9_]+)-((%?[a-z0-9_]+(:[a-z0-9_]+)*)+)\|(((?R)|.)*)@\}/Us',
        function( $match ) use( &$if_counter ){
            return '<-{'. ( $if_counter ++ ) .'}->';
        }, $string );


var_dump($parsed_view); // NULL

请访问 https://dev59.com/7JPea4cB1Zd3GeqP_B9b。 - Wiktor Stribiżew
http://php.net/manual/en/pcre.configuration.php - Deep
@WiktorStribiżew 我已经看过了,但我不确定是否使用:ini_set('pcre.jit', false);是正确的方法...这就像在想要隐藏错误时使用@一样。 - instead
@revo 不不... 这不是为了这个。这只是匿名函数内的简化代码。 - instead
那么我相信你可以做得更好:[检查这个。](https://regex101.com/r/aL6rN0/2) - revo
显示剩余2条评论
2个回答

9

什么是 PCRE JIT

即时编译是一种重量级的优化,可以大大加速模式匹配。不过,它会在执行匹配之前进行额外的处理。因此,在相同的模式需要被匹配多次的情况下,才能发挥最大的优势。

那么它基本上是如何工作的呢?

PCRE(和JIT)是一个递归、深度优先引擎,因此它需要一个堆栈,在检查其子节点之前将当前节点的本地数据推入其中... 当编译后的JIT代码运行时,它需要一个内存块用作堆栈。默认情况下,它在机器堆栈上使用32K。但是,某些较大或复杂的模式需要更多的空间。当没有足够的堆栈时,将出现错误PCRE_ERROR_JIT_STACKLIMIT

通过第一条引语,您将了解JIT是一个可选的功能,在PHP [v7.*] PCRE中默认开启。因此,您可以轻松关闭它:pcre.jit = 0(不建议这样做)

但是,当收到preg_*函数的错误代码#6时,这意味着可能JIT已经达到了堆栈大小限制。

由于捕获组消耗的内存比非捕获组多(就算按簇的量词类型使用更多内存):

  1. 捕获组OP_CBRApcre_jit_compile.c:#1138)- (实际内存超过此值):
case OP_CBRA:
case OP_SCBRA:
bracketlen = 1 + LINK_SIZE + IMM2_SIZE;
break;
  1. 非捕获组OP_BRA(pcre_jit_compile.c:#1134) - (实际内存比此更多):
case OP_BRA:
bracketlen = 1 + LINK_SIZE;
break;

因此,在你自己的正则表达式中将捕获组更改为非捕获组,可以使其产生正确的输出(我不知道确切地节省了多少内存)。
但是,似乎你需要捕获组并且它们是必要的。那么,出于性能考虑,你应该重新编写你的正则表达式。回溯几乎是正则表达式中要考虑的所有内容。
更新 #1
解决方案:
(?(DEFINE)
  (?<recurs>
    (?! {@|@} ) [^|] [^{@|\\]* ( \\.[^{@|\\]* )* | (?R)
  )
)
{@
(?<If> \w+)-
(?<Condition> (%?\w++ (:\w+)*)* )
(?<True> [|] [^{@|]*+ (?&recurs)* )
(?<False> [|] (?&recurs)* )?
\s*@}

演示链接

PHP 代码(注意反斜杠的转义):

preg_match_all('/(?(DEFINE)
  (?<recurs>
    (?! {@|@} ) [^|] [^{@|\\\\]* ( \\\\.[^{@|\\\\]* )* | (?R)
  )
)
{@
(?<If> \w+ )-
(?<Condition> (%?\w++ (:\w+)*)* )
(?<True> [|] [^{@|]*+ (?&recurs)* )
(?<False> [|] (?&recurs)* )?
\s*@}/x', $string, $matches);

这是您自己的正则表达式,它经过优化,以使回溯步骤最少。因此,您自己的正则表达式所匹配的任何内容也可以被此表达式匹配。

不包含嵌套if块的正则表达式:

{@
(?<If> \w+)-
(?<Condition> (%?\w++ (:\w+)*)* )
(?<True> [|] [^|\\]* (?: \\.[^|\\]* )* )
(?<False> [|] \X*)?
@}

实时演示

大多数量词都采用所有格书写方式(避免回溯),在它们后面添加+


我不确定为什么,但当我将您的正则表达式粘贴到我的PHP脚本中时,它返回NULL。我在开头和结尾添加了/,并且还放置了x标志。您在评论中提供的表达式有效... - instead
这个可以用。谢谢。我会检查一下是否是我需要的。 - instead
我还更新了匹配模式:sub%statement2的无限出现次数。请检查。 - revo
如果您不需要匹配嵌套的 if 块,代码会更短。 - revo
1
我添加了一个正则表达式,它不遵循嵌套的 if 块。请检查。 - revo
显示剩余2条评论

3
您看到的问题是您的模式效率低下。主要原因是:
  • 您使用这种子模式:(a+)+b,这是灾难性回溯的最佳方式
  • 您也使用这种子模式:(a|b)+,这可能是一个很好的设计,除了像pcre这样的回溯正则表达式引擎
  • 出于未知原因,您使用了U修饰符,使所有量词都变成非贪婪的,并生成了大量无用的测试

另外,有太多无用的捕获组消耗内存。如果您不需要捕获组,请不要编写它。如果您确实需要对元素进行分组,请使用非捕获组,但不要使用非捕获组来使模式“更易读”(还有其他方法可以做到这一点,如命名组、自由间距和注释)


如果我理解正确,您正在尝试构建一个正则表达式,用于preg_replace_callback来处理模板系统的控制语句。由于这些控制语句可以嵌套,并且正则表达式引擎无法匹配相同的子字符串多次,因此您必须在以下几种策略之间选择:
  1. 您可以编写递归模式来描述包含其他条件语句的条件语句。

  2. 您可以编写仅匹配最内层条件语句的模式。(换句话说,它禁止嵌套的条件语句。)

在这两种情况下,您需要多次解析字符串,直到没有内容可替换为止。(请注意,您也可以使用第一种策略的递归函数,但这会使事情更加复杂。) 让我们看看第二种方法:
$pattern = '~
{@ (?<cond> \w+ ) - (?<stat> \w+ (?: % \w+ )* ) (?: : (?<sub> \w+ ) )? \|

# a "THEN" part that doesn\'t have nested conditional statements
(?<then> [^{|@]*+ (?: { (?!@) [^{|@]* | @ (?!}) [^{|@]* )*+ )

# optional "ELSE" part (the content is similar to the "THEN" part)
(?: \| (?<else> \g<then> ) )? (*SKIP) @}~x';

$parsed_view = $string;
$count = 0;

do {
    $parsed_view = preg_replace_callback($pattern, function ($m) {
        // do what you need here. The different captures can be
        // easily accessed with their names: $m['cond'], $m['stat']...
        // as defined in the pattern.
        return $result;
    }, $parsed_view, -1, $count);
} while ($count);

模式演示

正如您所看到的,嵌套语句的问题可以通过使用do..while循环和preg_replace_callbackcount参数来解决,以查看是否替换了某些内容。

这段代码尚未经过测试,但我相信您可以完成它,并最终根据您的需求进行调整。


作为旁注,已经存在很多模板引擎(而且PHP本身就是一个模板引擎)。你可以使用它们并避免创建自己的语法。你也可以查看它们的代码。

@Kaii: 注意使用我建议的模式,您无法达到回溯限制,因为量词是占有的,或子模式被包含在原子组中(或充当原子组的组)。另一件事,由于模式以文字字符开头,快速算法用于选择要测试模式的位置。(*SKIP) 动词避免重试已经测试过的子字符串。无论字符串大小如何,搜索非常快。当达到回溯限制(或者模式花费太多时间)只有模式设计有问题,而不是工具本身。 - Casimir et Hippolyte
我刚想起我在StackOverflow上发布了问题和解决方案,详情请见http://stackoverflow.com/questions/20903722。 - Kaii
当我使用这个正则表达式测试 {@if-statement:sub%statement2| ... 时,它显示没有匹配。 - instead
@instead: 的确,但是你没有明确指出子部分可以在每个语句之后,目前"sub"部分仅允许在最后一个语句之后,您可以轻易地进行更正。将(?<stat> \w+ (?: % \w+ )* ) (?: : (?<sub> \w+ ) )?替换为(?<stat> \w+ (?: : \w+ )? (?: % \w+ (?: : \w+ )? )* ) - Casimir et Hippolyte
是的,我没有说明清楚,对此感到抱歉。现在它可以工作了。稍后我会检查一下它是否符合我的需求。 - instead
显示剩余3条评论

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