PHP PCRE 允许字符串中的嵌套模式(递归)

标签 php regex recursion pcre

我得到了一个类似 1(8()3(6()7())9()3())2(4())3()1(0()3()) 的字符串 代表一棵树。 如果我们再深入一层,就会出现一个括号。同一层的数字是邻居。

现在想要添加节点,例如,我想向每个路径添加一个 5,其中第一个路径有 1,第一个路径有 3 code> 在第二层,所以我想在每个 3( 后面放置一个 5() ,它位于 1(. 因此 5() 必须添加 3 次,结果应为 1(8()3(5()6()7())9()3(5() ))2(4())3()1(0()3(5()))

问题是,我没有让代码与 PCRE 递归一起工作。如果我匹配没有固定路径的树表示字符串,如 1(3( ) 它可以工作,但是当我生成具有这些固定模式的正则表达式时,它不起作用.

这是我的代码:

<?php
header('content-type: text/plain;utf-8');

$node = [1, 3, 5];
$path = '1(8()3(6()7())9()3())2(4())3()1(0()3())';

echo $path.'
';

$nes = '\((((?>[^()]+)|(?R))*)\)';
$nes = '('.$nes.')*';

echo preg_match('/'.$nes.'/x', $path) ? 'matches' : 'matches not';
echo '
';

// creates a regex with the fixed path structure, but allows nested elements in between
// in this example something like: /^anyNestedElementsHere 1( anyNestedElementsHere 3( anyNestedElementsHere ))/
$re = $nes;
for ($i = 0; $i < count($node)-1; $i++) {
    $re .= $node[$i].'\(';
    if ($i != count($node)-2)
        $re .= $nes;
}
$re = '/^('.$re.')/x';

echo str_replace($nes, '   '.$nes.'   ', $re).'
';
echo preg_match($re, $path) ? 'matches' : 'matches not';
echo '
';
// append 5()
echo preg_replace($re, '${1}'.$node[count($node)-1].'()', $path);
?>

这是输出,您可以在其中看到生成的正则表达式的样子:

1(8()3(6()7())9()3())2(4())3()1(0()3())
matches
/^(   (\((((?>[^()]+)|(?R))*)\))*   1\(   (\((((?>[^()]+)|(?R))*)\))*   3\()/x
matches not
1(8()3(6()7())9()3())2(4())3()1(0()3())

我希望你能理解我的问题,并希望你能告诉我,我的错误在哪里。

非常感谢!

最佳答案

解决方案

正则表达式

以下正则表达式以递归方式匹配嵌套括号,在第一层查找左 1(,在第二层查找左 3((作为直接子级)) .它还会尝试连续匹配,无论是在同一级别上还是向下相应级别查找另一个匹配项。

~
(?(?=\A)  # IF: First match attempt (if at start of string)   - -

  # we are on 1st level => find next "1("

  (?<balanced_brackets>
    # consumes balanced brackets recursively where there is no match
    [^()]*+
    \(  (?&balanced_brackets)*?  \)
  )*?

  # match "1(" => enter level 2
  1\(

|         # ELSE: Successive matches  - - - - - - - - - - - - - -

  \G    # Start at end of last match (level 3)

  # Go down to level 2 - match ")"
  (?&balanced_brackets)*?
  \)

  # or go back to level 1 - matching another ")"
  (?>
    (?&balanced_brackets)*?
    \)
    
    # and enter level 2 again
    (?&balanced_brackets)*?
    1\(
  )*?
)                                      # - - - - - - - - - - - -

# we are on level 2 => consume balanced brackets and match "3("
(?&balanced_brackets)*?
3\K\(  # also reset the start of the match
~x

更换

(5()

文字

Input:
1(8()3(6()7())9()3())2(4())3()1(0()3())

Output:
1(8()3(5()6()7())9()3(5()))2(4())3()1(0()3(5()))
       ^^^            ^^^                  ^^^
       [1]            [2]                  [3]

regex101 demo


它是如何工作的

我们首先使用conditional subpattern区分:

  • 第一次比赛尝试(从第 1 级开始)以及
  • 连续尝试(从第 3 级开始,以 \G assertion 为锚定)。
(?(?=\A)  # IF followed by start of string
    # This is the first attempt
|         # ELSE
    # This is another attempt
    \G    # and we'll anchor it to the end of last match
)

对于第一个匹配,我们将消耗所有不匹配 1( 的嵌套括号,以便将光标定位到第一级中的位置它可以找到成功的匹配。

  • 这是一种众所周知的匹配嵌套结构的递归模式。如果您不熟悉,请引用RecursionSubroutines .
(?<balanced_brackets>        # ANY NUMBER OF BALANCED BRACKETS
  [^()]*+                    # match any characters 
  \(                         # opening bracket
    (?&balanced_brackets)*?  #  with nested bracket (recursively)
  \)                         # closing bracket in the main level
)*?                          # Repeated any times (lazy)

注意这是一个named group我们将在模式中多次用作子例程调用来消耗不需要的平衡括号,如 (?&balanced_brackets)*?

下一个级别。现在,要进入第 2 级,我们需要匹配:

1\(

最后,我们将消耗所有平衡括号,直到找到第三层的开头:

(?&balanced_brackets)*?
3\(

就是这样。我们刚刚匹配了第一个匹配项,因此我们可以在该位置插入替换文本。

下一场比赛。对于连续的匹配尝试,我们可以:

  • 转到与结束 ) 匹配的第 2 级 以查找另一个出现的 3(
  • 进一步向下至级别 1 匹配 2 结束 ),并从那里开始使用与我们在第一场比赛中使用的相同策略进行匹配。

这是通过以下子模式实现的:

\G                             # anchored to the end of last match (level 3)
(?&balanced_brackets)*?        # consume any balanced brackets
\)                             # go down to level 2
                               #
(?>                            # And optionally
  (?&balanced_brackets)*?      #   consume level 2 brackets
  \)                           #   to go down to level 1
  (?&balanced_brackets)*?      #   consume level 1 brackets
  1\(                          #   and go up to level 2 again
)*?                            # As many times as it needs to (lazy)

总而言之,我们可以匹配第三层的开头:

(?&balanced_brackets)*?
3\(

我们还将 reset the start of match接近模式末尾时,带有 \K ,仅匹配最后一个左括号。因此,我们可以简单地替换为 (5(),避免使用反向引用。


PHP 代码

我们只需要调用 preg_replace() 与上面使用的值相同。

Ideone demo


为什么你的正则表达式失败了?

既然你问了,模式就锚定在字符串的开头。它只能匹配第一次出现的情况。

/^(   (\((((?>[^()]+)|(?R))*)\))*   1\(   (\((((?>[^()]+)|(?R))*)\))*   3\()/x

此外,它与第一次出现不匹配,因为构造 (?R) 递归了 整个 模式(尝试匹配 ^ 再次)。我们可以将 (?R) 更改为 (?2)

但主要原因是因为它在任何打开 \( 之前不消耗字符。例如:

Input:
1(8()3(6()7())9()3())2(4())3()1(0()3())
  ^
  #this "8" can't be consumed with the pattern

还有一个行为应该考虑:PCRE treats recursion as atomic 。因此,您必须确保该模式将消耗不需要的括号,如上例所示,但也要避免在各自的级别中匹配 1(3(

关于PHP PCRE 允许字符串中的嵌套模式(递归),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33698394/

相关文章:

php - 使用 PHP 转换 WooCommerce Webhook 有效负载

php - 我想在 sql 中选择客户和日期过滤器

php - 来自 value API 的 HTML 星级

php - 使用 preg_replace 将单词的每个首字母大写

javascript - 我如何修改这个 JavaScript 正则表达式来检查字符串是否是必须有最多 2 位小数的数字?

c++ - 如何制作一个递归函数来显示有多少个元音有输入

haskell - 理解 Haskell/GHCI 的递归行为

php - 将多行换行为一行

php - preg_replace 不接受 unicode

algorithm - 洪水填充递归算法