web-dev-qa-db-fra.com

Comment faire correspondre les crochets imbriqués en utilisant regex?

Comme le titre l'indique, voici un exemple d'entrée:

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

Bien entendu, les chaînes correspondantes seront traitées par récursivité.

Je veux que la première récursion corresponde:

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

Et il est inutile de dire que les processus après sont ...

16
gordomium

De nombreuses implémentations regex ne vous permettront pas de faire correspondre une quantité arbitraire d'imbrication. Cependant, Perl, PHP et .NET prennent en charge les modèles récursifs.

Une démo en 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");
}

qui va imprimer:

----------.__ (extérieur 
 (centre 
 (intérieur) 
 (intérieur) 
 centre) 
 ouer) 
----- ----- 
 (extérieur 
 (intérieur) 
 ouer) 
 ---------- 
 (extérieur 
 ouer)

Ou l'équivalent PHP:

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

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

print_r($matches);

qui produit:

Tableau 
 (
 [0] => Tableau 
 (
 [0] => (extérieur 
 (Centre 
 (Intérieur) 
 (Intérieur) 
 Centre.) 
 ouer) 
 [1] => (extérieur 
 (intérieur) 
 ou.) 
 [2] => (extérieur 
 ou.) 
) 
 ....

Une explication:

 (# début du groupe 1 
\(# correspond à un littéral '(' 
 (# groupe 2 
 [^ ()] # tout caractère autre que '(' et ')' 
 | # OU 
 (? R) # correspond récursivement au motif entier 
) * # Groupe d'extrémité 2 et répétez zéro ou plusieurs fois 
 \) # Correspond à un littéral ')'. 1

MODIFIER

Remarquez le commentaire de Goozak:

Un meilleur modèle pourrait être \(((?>[^()]+)|(?R))*\) (from PHP: Patterns récursifs ). Pour mes données, le modèle de Bart plantait PHP lorsqu'il rencontrait une (chaîne longue) sans imbrication. Ce modèle a traversé toutes mes données sans problème.

29
Bart Kiers

N'utilisez pas de regex.

Au lieu de cela, une simple fonction récursive suffira:

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
21
nneonneo

J'ai trouvé cette expression rationnelle simple qui extrait tous les groupes équilibrés imbriqués à l'aide de la récursivité, bien que la solution résultante ne soit pas aussi simple que vous le souhaiteriez:

Modèle de regex: (1(?:\1??[^1]*?2))+

Exemple d'entrée: 1ab1cd1ef2221ab1cd1ef222

Pour plus de simplicité, je mets 1 pour ouvert et 2 pour crochet fermé. Les caractères alphabétiques représentent des données internes ... Je vais réécrire les entrées pour qu'elles soient faciles à expliquer.

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

            |_1|
       |______2__|
|_____________3_____|

Lors de la première itération, l'expression rationnelle correspond au sous-groupe le plus interne, 1ef2, du premier groupe apparenté, 1ab1cd1ef222. Si nous nous en souvenons et que c'est sa position, et retirons ce groupe, il resterait 1ab1cd22. Si nous continuons avec regex, il renverrait 1cd2 et enfin 1ab2. Ensuite, il continuera à analyser le deuxième groupe frère de la même manière.

Comme nous le voyons dans cet exemple, regex extraira correctement les sous-chaînes telles qu'elles apparaissent dans la hiérarchie définie entre crochets. La position de la sous-chaîne en particulier dans la hiérarchie sera déterminée lors de la deuxième itération. Si sa position dans la chaîne se situe entre la sous-chaîne de la deuxième itération, il s’agit d’un nœud enfant, sinon d’un nœud frère.

De notre exemple:

  1. 1ab1cd1ef222 1ab1cd1ef222, correspondance d'itération 1ef2, avec index 6,

  2. 1ab1cd22 1ab1cd1ef222, correspondance d'itération 1cd2, avec index 3, se terminant par 6. Étant donné que 3 <6 <= 6, la première sous-chaîne est l'enfant de la deuxième sous-chaîne.

  3. 1ab2 1ab1cd1ef222, correspondance d'itération 1ab2, avec index 0, se terminant par 3. Étant donné que 0 <3 <= 3, la première sous-chaîne est l'enfant de la deuxième sous-chaîne.

  4. 1ab1cd1ef222, correspondance d'itération 1ef2, avec index 6, Parce que ce n'est pas 3 <0 <= 6, c'est une branche d'un autre frère, etc ...

Nous devons itérer et supprimer tous les frères et sœurs avant de pouvoir passer au parent. Ainsi, nous devons nous souvenir de tous ces frères et sœurs dans l'ordre dans lequel ils apparaissent dans l'itération.

8
Elvedin Hamzagic

Code Delphi Pascal basé sur l'affichage ci-dessus de nneonneo :

Vous avez besoin d'un formulaire avec un bouton, nommé btnRun. Dans le code source, remplacez "arnolduss" par votre nom dans le dossier DownLoads. Notez le niveau de pile dans la sortie créée par ParseList. Il est évident que les crochets du même type doivent s’ouvrir et se fermer au même niveau de pile. Vous pourrez maintenant extraire les soi-disant groupes par niveau de pile.

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.
0
Nols Smit