【问题标题】:How to write a recursive regex that matches nested parentheses?如何编写匹配嵌套括号的递归正则表达式?
【发布时间】:2013-12-13 14:49:45
【问题描述】:

我正在尝试编写一个匹配嵌套括号的正则表达式,例如:

"(((text(text))))(text()()text)(casual(characters(#$%^^&&#^%#@!&**&#^*!@#^**_)))"

应该匹配这样的字符串,因为所有嵌套的括号都被关闭,而不是:

"(((text)))(text)(casualChars*#(!&#*(!))"

不应该,或者更好的是,至少应该匹配第一个“(((text)))(text)”部分。

其实我的正则表达式是:

 $regex = '/( (  (\() ([^[]*?)  (?R)?  (\))  ){0,}) /x';

但它不能像我预期的那样正常工作。如何解决?我哪里错了?谢谢!

【问题讨论】:

  • 我为需要递归执行此操作的 SQL 编写了一个解析器。使用正则表达式实现递归函数比尝试仅使用正则表达式递归执行此操作要容易得多。
  • 你找错了树,纯粹的正则表达式解决方案可能过于复杂且难以维护。你最好递归解析字符串。
  • 不要...好吧,理论上是可以做到的,但是当你设法做到的时候,它可能看起来像空话。哦,看我们在正则表达式中发现了一个错误!恩...你怎么解决这个问题?哦,我们还需要添加对刹车的支持!恩...你怎么添加它?我告诉你,你最好使用更易读的解析器。你问这个的事实表明你可能无论如何都无法维护它。
  • 感谢您的建议,但我还是想这样做,您能帮帮我吗?为什么我的正则表达式没有按预期工作?
  • @user3019105 你想对匹配做什么? (你只是验证,你想替换括号上的内容,还是只对每个内容运行回调)另外,你只想要最深的括号还是全部想要?

标签: php regex


【解决方案1】:

这种模式有效:

$pattern = '~ \( (?: [^()]+ | (?R) )*+ \) ~x';

括号内的内容是简单的描述:

"所有不是括号或递归(= 其他括号)" x 0 次或更多次

如果要捕获括号内的所有子字符串,则必须将此模式放入前瞻中以获得所有重叠结果:

$pattern = '~(?= ( \( (?: [^()]+ | (?1) )*+ \) ) )~x';
preg_match_all($pattern, $subject, $matches);
print_r($matches[1]);

请注意,我添加了一个捕获组,并将(?R) 替换为(?1)

(?R) -> refers to the whole pattern (You can write (?0) too)
(?1) -> refers to the first capturing group

这个前瞻技巧是什么?

前瞻(或后瞻)内的子模式不匹配任何内容,它只是一个断言(测试)。因此,它允许多次检查相同的子字符串。

如果您显示整个模式结果 (print_r($matches[0]);),您将看到所有结果都是空字符串。获取前瞻中子模式找到的子字符串的唯一方法是将子模式包含在捕获组中。

注意:递归子模式可以这样改进:

\( [^()]*+ (?: (?R) [^()]* )*+ \)

【讨论】:

  • 我试过了但是不行,而且我也没有捕捉到子模式...还有其他方法吗?
  • 感谢您的解释。需要玩弄它才能理解。
  • 抱歉我的无知,,"(?1)" 和 "\1" 一样吗?是反向引用吗?还有一个问题:[^()] 类后面的双“++”是什么意思?
  • @user3019105:不,不一样。 \1 指的是捕获组 1 匹配的内容。(?1) 指的是捕获组 1 内的子模式。您只需重复子模式。
  • @user3019105:当您将(?1) 放入第一个捕获组本身时,您将获得一个递归子模式。
【解决方案2】:

当我找到这个答案时,我无法弄清楚如何修改模式以使用我自己的分隔符 {}。所以我的方法是让它更通用。

这是一个使用您自己的变量左右分隔符生成正则表达式模式的脚本。

$delimiter_wrap  = '~';
$delimiter_left  = '{';/* put YOUR left delimiter here.  */
$delimiter_right = '}';/* put YOUR right delimiter here. */

$delimiter_left  = preg_quote( $delimiter_left,  $delimiter_wrap );
$delimiter_right = preg_quote( $delimiter_right, $delimiter_wrap );
$pattern         = $delimiter_wrap . $delimiter_left
                 . '((?:[^' . $delimiter_left . $delimiter_right . ']++|(?R))*)'
                 . $delimiter_right . $delimiter_wrap;

/* Now you can use the generated pattern. */
preg_match_all( $pattern, $subject, $matches );

【讨论】:

  • 很好,只要有一个$delimiter_right 关闭一个打开的$delimiter_left,你就可以匹配整个字符串
【解决方案3】:

以下代码使用 my Parser class(它在 CC-BY 3.0 下),它适用于 UTF-8(感谢 my UTF8 class)。

它的工作方式是使用递归函数来迭代字符串。每次找到( 时,它都会调用自己。当它到达字符串的末尾而没有找到对应的)时,它也会检测到不匹配的对。

此外,此代码采用 $callback 参数,您可以使用它来处理它找到的每个部分。回调接收两个参数:1) 字符串和 2) 级别(0 = 最深)。无论回调返回什么,都将替换为字符串的内容(这种变化在更高级别的回调中可见)。

注意:代码不包括类型检查。

非递归部分:

function ParseParenthesis(/*string*/ $string, /*function*/ $callback)
{
    //Create a new parser object
    $parser = new Parser($string);
    //Call the recursive part
    $result = ParseParenthesisFragment($parser, $callback);
    if ($result['close'])
    {
        return $result['contents'];
    }
    else
    {
        //UNEXPECTED END OF STRING
        // throw new Exception('UNEXPECTED END OF STRING');
        return false;
    }
}

递归部分:

function ParseParenthesisFragment(/*parser*/ $parser, /*function*/ $callback)
{
    $contents = '';
    $level = 0;
    while(true)
    {
        $parenthesis = array('(', ')');
        // Jump to the first/next "(" or ")"
        $new = $parser->ConsumeUntil($parenthesis);
        $parser->Flush(); //<- Flush is just an optimization
        // Append what we got so far
        $contents .= $new;
        // Read the "(" or ")"
        $element = $parser->Consume($parenthesis);
        if ($element === '(') //If we found "("
        {
            //OPEN
            $result = ParseParenthesisFragment($parser, $callback);
            if ($result['close'])
            {
                // It was closed, all ok
                // Update the level of this iteration
                $newLevel = $result['level'] + 1;
                if ($newLevel > $level)
                {
                    $level = $newLevel;
                }
                // Call the callback
                $new = call_user_func
                (
                    $callback,
                    $result['contents'],
                    $level
                );
                // Append what we got
                $contents .= $new;
            }
            else
            {
                //UNEXPECTED END OF STRING
                // Don't call the callback for missmatched parenthesis
                // just append and return
                return array
                (
                    'close' => false,
                    'contents' => $contents.$result['contents']
                );
            }
        }
        else if ($element == ')') //If we found a ")"
        {
            //CLOSE
            return array
            (
                'close' => true,
                'contents' => $contents,
                'level' => $level
            );
        }
        else if ($result['status'] === null)
        {
            //END OF STRING
            return array
            (
                'close' => false,
                'contents' => $contents
            );
        }
    }
}

【讨论】:

  • 像这样的发布功能落后于光年
  • @MartinZvarík 感谢您让我注意到这个答案,我已经修复了链接。这意味着比正则表达式更容易维护。
  • 我也很喜欢你提到它是在知识共享许可下的:DD ...兄弟...只需删除整个内容,我们就会忘记它曾经在这里发布过... 5 年后你一定更了解
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多