簡體   English   中英

PHP PCRE允許在字符串中嵌套模式(遞歸)

[英]PHP PCRE allow nested patterns (recursion) in a string

我得到了像1(8()3(6()7())9()3())2(4())3()1(0()3())這樣的字符串,它表示一棵樹。 如果我們更深一層,將出現一個括號。 在同一級別上的數字是鄰居。

現在要添加節點,比如我想將增加5到,我們有一個每路1對第一和3在第二個層次,所以我想提出一個5()每經過3(這是內部a 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演示


這個怎么運作

我們首先使用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(嵌套括號,以便將光標移到第一級可以找到成功匹配項的位置。

  • 這是匹配嵌套構造的眾所周知的遞歸模式。 如果您不熟悉它,請參閱Recursion和子Subroutines
(?<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個close ) ,然后從那里匹配與第一個匹配相同的策略。

這可以通過以下子模式實現:

\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演示


為什么您的正則表達式失敗?

如您所問,該模式已錨定到字符串的開頭。 它只能匹配第一個匹配項。

/^(   (\((((?>[^()]+)|(?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將遞歸視為atomic 因此,您必須確保模式會像上面的示例一樣使用不需要的括號,但也要避免在各自的級別匹配1(3(

我將這個問題分解為兩個較小的部分:

首先,使用以下正則表達式提取1節點:

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

演示

為此使用preg_replace_callback 這將匹配1(8()3(6()7())9()3())1(0()3())

接下來,只需要用3(5()替換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()))

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM