如何使用正则表达式匹配嵌套括号?

时间:2013-02-19 07:34:21

标签: regex nested

正如标题所说,这是一个示例输入:

 (outer
   (center
     (inner)
     (inner)
   center)
 ouer)
 (outer
   (inner)
 ouer)
 (outer
 ouer)

当然,匹配的字符串将通过递归处理。

我希望第一次递归匹配:

 [
 (outer
   (center
     (inner)
     (inner)
   center)
 ouer),
 (outer
   (inner)
 ouer),
 (outer
 ouer)]

后续流程不用说......

4 个答案:

答案 0 :(得分:29)

许多正则表达式实现不允许您匹配任意数量的嵌套。但是,Perl,PHP和.NET支持递归模式。

Perl中的演示:

#!/usr/bin/perl -w

my $text = '(outer
   (center
     (inner)
     (inner)
   center)
 ouer)
 (outer
   (inner)
 ouer)
 (outer
 ouer)';

while($text =~ /(\(([^()]|(?R))*\))/g) {
  print("----------\n$1\n");
}

将打印:

----------
(outer
   (center
     (inner)
     (inner)
   center)
 ouer)
----------
(outer
   (inner)
 ouer)
----------
(outer
 ouer)

或者,PHP等价物:

$text = '(outer
   (center
     (inner)
     (inner)
   center)
 ouer)
 (outer
   (inner)
 ouer)
 (outer
 ouer)';

preg_match_all('/(\(([^()]|(?R))*\))/', $text, $matches);

print_r($matches);

产生:

Array
(
    [0] => Array
        (
            [0] => (outer
   (center
     (inner)
     (inner)
   center)
 ouer)
            [1] => (outer
   (inner)
 ouer)
            [2] => (outer
 ouer)
        )

...

解释:

(          # start group 1
  \(       #   match a literal '('
  (        #   group 2
    [^()]  #     any char other than '(' and ')'
    |      #     OR
    (?R)   #     recursively match the entir pattern
  )*       #   end group 2 and repeat zero or more times
  \)       #   match a literal ')'
)          # end group 1

修改

注意@ Goozak的评论:

  

更好的模式可能是\(((?>[^()]+)|(?R))*\)(来自PHP:Recursive patterns)。对于我的数据,Bart的模式在遇到没有嵌套的(长字符串)时崩溃了PHP。这个模式没有问题地遍历了我的所有数据。

答案 1 :(得分:21)

不要使用正则表达式。

相反,一个简单的递归函数就足够了:

def recursive_bracket_parser(s, i):
    while i < len(s):
        if s[i] == '(':
            i = recursive_bracket_parser(s, i+1)
        elif s[i] == ')':
            return i+1
        else:
            # process whatever is at s[i]
            i += 1
    return i

答案 2 :(得分:8)

我发现这个简单的正则表达式使用递归提取所有嵌套的平衡组,尽管得到的解决方案并不像你期望的那样简单:

正则表达式模式:(1(?:\1??[^1]*?2))+

示例输入:1ab1cd1ef2221ab1cd1ef222

为简单起见,我将1设置为open,2设置为封闭括号。 Alpha字符代表一些内部数据。 我会重写输入,以便于解释。

1  ab  1 cd 1ef2 2  2     1  ab  1 cd 1ef2 2  2

            |_1|
       |______2__|
|_____________3_____|

在第一次迭代中,regex将匹配第一个兄弟组1ef2中最内层的子组1ab1cd1ef222。如果我们记住它及其位置,并删除此组,则会保留1ab1cd22。如果我们继续使用正则表达式,它将返回1cd2,最后返回1ab2。然后,它将继续以相同的方式解析第二个兄弟组。

正如我们从这个例子中看到的,正则表达式将正确提取子括号,因为它们出现在由括号定义的层次结构中。层次结构中特别子串的位置将在第二次迭代中确定,如果它在string中的位置在第二次迭代的子串之间,那么它是子节点,否则它是一个兄弟节点。

从我们的例子中可以看出:

  1. 1ab1cd1ef222 1ab1cd1ef222,迭代匹配1ef2,索引为6

  2. 1ab1cd22 1ab1cd1ef222,迭代匹配1cd2,索引3,以6结尾。 因为3&lt; 6&lt; = 6,第一个子字符串是第二个子字符串的子字符。

  3. 1ab2 1ab1cd1ef222,迭代匹配1ab2,索引0,以3结尾。 因为0&lt; 3&lt; = 3,第一个子字符串是第二个子字符串的子字符。

  4. 1ab1cd1ef222,迭代匹配1ef2,索引为6, 因为它不是3&lt; 0&lt; = 6,它来自另一个兄弟姐妹的分支......

  5. 我们必须迭代并删除所有兄弟姐妹才能转移到父母。因此,我们必须按照它们在迭代中出现的顺序记住所有兄弟姐妹。

答案 3 :(得分:0)

基于nneonneo上方发布的Delphi Pascal代码:

您需要一个带有按钮的表单,名为btnRun。在源代码中,替换&#34; arnolduss&#34;在DownLoads文件夹中显示您的姓名。请注意ParseList创建的输出中的堆栈级别。显然,相同类型的括号必须在同一堆栈级别上打开和关闭。您现在可以在每个堆栈级别提取所谓的组。

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

Type TCharPos = record
  Level: integer;
  Pos: integer;
  Char: string;
end;

type
  TForm1 = class(TForm)
    btnRun: TButton;
    procedure btnRunClick(Sender: TObject);
  private
    { Private declarations }
    procedure FormulaFunctionParser(var CharPos: array of TCharPos;
       const formula, LBracket, RBracket: string; var itr, iChar,
       iLevel: integer; var ParseList: TStringList);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.btnRunClick(Sender: TObject);
var
  formula: string;
  CharPos: array of TCharPos;
  itr, iChar, iLevel: integer;
  ParseList: TStringList;
begin
  screen.Cursor := crHourGlass;
  itr := 0;
  iChar := 1;
  iLevel := 0;
  ParseList := TStringList.Create;
  try
    formula := Trim('add(mul(a,add(b,c)),d) + e - sub(f,g)');
    ParseList.Add(formula);
    ParseList.Add('1234567890123456789012345678901234567890');

    SetLength(CharPos, Length(formula));
    FormulaFunctionParser(CharPos[0], formula, '(', ')', itr, iChar, iLevel, ParseList);
  finally
    ParseList.SaveToFile('C:\Users\arnolduss\Downloads\ParseList.txt');
    FreeAndNil(ParseList);
    screen.Cursor := crDefault;
  end;
end;

//Based on code from nneonneo in: https://stackoverflow.com/questions/14952113/how-can-i-match-nested-brackets-using-regex
procedure TForm1.FormulaFunctionParser(var CharPos: array of TCharPos;
       const formula, LBracket, RBracket: string; var itr, iChar,
       iLevel: integer; var ParseList: TStringList);
  procedure UpdateCharPos;
  begin
    CharPos[itr-1].Level := iLevel;
    CharPos[itr-1].Pos := itr;
    CharPos[itr-1].Char := formula[iChar];

    ParseList.Add(IntToStr(iLevel) + '  ' + intToStr(iChar) + ' = ' + formula[iChar]);
  end;
begin
  while iChar <= length(formula) do
  begin
    inc(itr);
    if formula[iChar] = LBracket then
    begin
      inc(iLevel);
      UpdateCharPos;
      inc(iChar);
      FormulaFunctionParser(CharPos, formula, LBracket, RBracket, itr, iChar, iLevel, ParseList);
    end
    else
    if formula[iChar] = RBracket then
    begin
      UpdateCharPos;
      dec(iLevel);
      inc(iChar);
    end
    else
    begin
      UpdateCharPos;
      inc(iChar);
    end;
  end;
end;

end.