Как я могу сопоставить вложенные скобки с помощью regex?

Как гласит название, вот пример ввода:

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

Конечно, согласованные строки будут обработаны рекурсией.

Я хочу, чтобы первая рекурсия соответствовала:

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

И последующие процессы бесполезно сказать...

Ответы

Ответ 1

Многие реализации регулярных выражений не позволят вам сопоставить произвольное количество вложенности. Тем не менее, 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: рекурсивные шаблоны). По моим данным, шаблон Bart сбивал PHP, когда он встречал (длинную строку) без вложенности. Этот шаблон прошел через все мои данные без проблем.

Ответ 2

Не используйте регулярное выражение.

Вместо этого достаточно простой рекурсивной функции:

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

Ответ 3

Я нашел это простое регулярное выражение, которое извлекает все вложенные сбалансированные группы, используя рекурсию, хотя результирующее решение не так просто, как вы можете ожидать:

Шаблон регулярного выражения: (1(?:\1??[^1]*?2))+

Пример ввода: 1ab1cd1ef2221ab1cd1ef222

Для простоты я положил 1 для open и 2 для закрытой скобки. Альфа-символы представляют собой некоторые внутренние данные. Я переписал ввод, чтобы его было легко объяснить.

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

            |_1|
       |______2__|
|_____________3_____|

В первом итерационном регулярном выражении будет соответствовать самая внутренняя подгруппа 1ef2 в первой группе sibling 1ab1cd1ef222. Если мы запомним его и его положение и удалим эту группу, останется 1ab1cd22. Если мы продолжим с регулярным выражением, оно вернет 1cd2 и, наконец, 1ab2. Затем он будет продолжать разбирать вторую группу братьев по-разному.

Как видно из этого примера, регулярное выражение будет правильно извлекать подстроки, как они отображаются в иерархии, определенной скобками. Позиция конкретной подстроки в иерархии будет определяться во время второй итерации, если она находится в строке между подстрокой из второй итерации, то она является дочерней node, иначе она является братом node.

В нашем примере:

  • 1ab1cd1ef222 1ab1cd1ef222, итерация соответствует 1ef2, с индексом 6,

  • 1ab1cd22 1ab1cd1ef222, итерация соответствует 1cd2, с индексом 3, заканчивающимся на 6. Поскольку 3 < 6 <= 6, первая подстрока является дочерней для второй подстроки.

  • 1ab2 1ab1cd1ef222, итерация соответствует 1ab2, с индексом 0, заканчивающимся на 3. Поскольку 0 < 3 <= 3, первая подстрока является дочерней для второй подстроки.

  • 1ab1cd1ef222, итерация соответствует 1ef2, с индексом 6, Поскольку это не 3 < 0 <= 6, это ветвь от другого брата и т.д.

Мы должны повторять и удалять всех братьев и сестер, прежде чем мы сможем перейти к родительскому. Таким образом, мы должны помнить всех братьев и сестер в том порядке, в котором они появляются на итерации.

Ответ 4

Код Delphi Pascal, основанный на публикации выше из nneonneo:

Вам нужна форма с кнопкой на ней, называемая btnRun. В исходном коде замените "arnolduss" своим именем в папке 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/info/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.