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


PHP PCRE allow nested patterns (recursion) in a string

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

现在想要添加节点,例如,我想为我们在第一层有1并在第二级有3的每个路径添加一个5,所以我想在1(内的每条3(之后放一个5()。所以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]

正则表达式101演示

<小时 />

工作原理

我们首先使用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'(

最后,我们将消耗任何平衡括号,直到找到第 3 级的开口:

(?&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'(

我们还将使用 '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 将递归视为原子。因此,您必须确保模式将像上面示例中那样使用不需要的括号,但也要避免在各自的级别中匹配1(3(

我将这个问题分解为两个较小的部分:

首先,使用以下正则表达式提取1节点:

(?(DEFINE)
  (?<tree>
    (?: 'd+ '( (?&tree) ') )*
  )
)
'b 1 '( (?&tree) ')

演示

为此使用preg_replace_callback。这将匹配1(8()3(6()7())9()3())1(0()3()).

接下来,只需将3(替换为3(5()即可完成。

PHP 中的示例:

$path = '1(8()3(6()7())9()3())2(4())3()1(0()3())';
$path = preg_replace_callback('#
    (?(DEFINE)
      (?<tree>
        (?: 'd+ '( (?&tree) ') )*
      )
    )
    'b 1 '( (?&tree) ')
#x', function($m) {
    return str_replace('3(', '3(5()', $m[0]);
}, $path);

其结果是 :1(8()3(5()6()7())9()3(5()))2(4())3()1(0()3(5()))