88 votes

Que sont les groupes d'équilibrage par expression régulière ?

Je viens de lire une question sur la façon de récupérer des données à l'intérieur de doubles accolades ( cette question ), et puis quelqu'un a parlé des groupes d'équilibrage. Je ne sais toujours pas très bien ce qu'ils sont et comment les utiliser.

J'ai lu à travers Définition du groupe d'équilibrage mais l'explication est difficile à suivre, et je ne comprends toujours pas les questions que j'ai mentionnées.

Quelqu'un pourrait-il simplement expliquer ce que sont les groupes d'équilibrage et en quoi ils sont utiles ?

168voto

Martin Büttner Points 26511

Pour autant que je sache, les groupes d'équilibrage sont uniques à la saveur regex de .NET.

Aside : Groupes répétés

Tout d'abord, vous devez savoir que .NET est (encore une fois, pour autant que je sache) la seule saveur de regex qui vous permet d'accéder à plusieurs captures d'un même groupe de capture (non pas dans les rétro-références mais après que la correspondance ait été effectuée).

Pour illustrer cela avec un exemple, considérons le modèle

(.)+

et la chaîne "abcd" .

dans toutes les autres saveurs de regex, en capturant le groupe 1 ne donnera qu'un seul résultat : d (note, le match complet sera bien sûr abcd comme prévu). Ceci est dû au fait que chaque nouvelle utilisation du groupe de capture écrase la capture précédente.

.NET, par contre, se souvient de tous. Et il le fait dans une pile. Après avoir fait correspondre la regex ci-dessus comme

Match m = new Regex(@"(.)+").Match("abcd");

vous constaterez que

m.Groups[1].Captures

Est un CaptureCollection dont les éléments correspondent aux quatre captures

0: "a"
1: "b"
2: "c"
3: "d"

où le numéro est l'index dans le CaptureCollection . En fait, chaque fois que le groupe est réutilisé, une nouvelle capture est poussée sur la pile.

La situation devient plus intéressante si l'on utilise des groupes de capture nommés. Étant donné que .NET autorise l'utilisation répétée du même nom, nous pourrions écrire une expression rationnelle du type

(?<word>\w+)\W+(?<word>\w+)

pour capturer deux mots dans le même groupe. Encore une fois, chaque fois qu'un groupe avec un certain nom est rencontré, une capture est poussée sur sa pile. Donc, en appliquant cette regex à l'entrée "foo bar" et d'inspecter

m.Groups["word"].Captures

nous trouvons deux captures

0: "foo"
1: "bar"

Cela nous permet même de pousser des choses sur une seule pile à partir de différentes parties de l'expression. Mais il ne s'agit là que d'une fonctionnalité de .NET permettant de suivre les captures multiples qui sont répertoriées dans ce tableau. CaptureCollection . Mais je l'ai dit, cette collection est un pile . Nous pouvons donc pop des choses à en tirer ?

Entrer : Groupes d'équilibrage

Il s'avère que nous le pouvons. Si nous utilisons un groupe comme (?<-word>...) puis la dernière capture est retirée de la pile. word si la sous-expression ... matches. Donc si nous changeons notre expression précédente en

(?<word>\w+)\W+(?<-word>\w+)

Ensuite, le deuxième groupe fera sauter la capture du premier groupe, et nous recevrons un vide CaptureCollection en fin de compte. Bien sûr, cet exemple est plutôt inutile.

Mais il y a un autre détail à la syntaxe moins : si la pile est déjà vide, le groupe échoue (quel que soit son sous-modèle). Nous pouvons tirer parti de ce comportement pour compter les niveaux d'imbrication - et c'est de là que vient le nom de groupe d'équilibrage (et où cela devient intéressant). Disons que nous voulons faire correspondre des chaînes de caractères qui sont correctement mises entre parenthèses. Nous poussons chaque parenthèse ouvrante sur la pile, et nous enlevons une capture pour chaque parenthèse fermante. Si nous rencontrons une parenthèse fermante de trop, la pile sera vide et le motif échouera :

^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*$

Nous avons donc trois alternatives dans une répétition. La première alternative consomme tout ce qui n'est pas une parenthèse. La deuxième alternative correspond à ( tout en les poussant sur la pile. La troisième alternative correspond à ) tout en retirant des éléments de la pile (si possible !).

<strong>Note : </strong>Juste pour clarifier, nous vérifions seulement qu'il n'y a pas de parenthèses non appariées ! Cela signifie qu'une chaîne de caractères ne contenant pas de parenthèses du tout <em>sera </em>parce qu'elles sont toujours syntaxiquement valides (dans certaines syntaxes où vous avez besoin que vos parenthèses correspondent). Si vous souhaitez garantir la présence d'au moins un jeu de parenthèses, ajoutez simplement un lookahead <code>(?=.*[(])</code> juste après le <code>^</code> .

Ce modèle n'est cependant pas parfait (ou entièrement correct).

Finale : Motifs conditionnels

Il y a un autre problème : cela ne garantit pas que la pile soit vide à la fin de la chaîne (d'où l'utilisation de la fonction (foo(bar) serait valable). .NET (et de nombreuses autres variantes) dispose d'une construction supplémentaire qui nous aide ici : les modèles conditionnels. La syntaxe générale est la suivante

(?(condition)truePattern|falsePattern)

où le falsePattern est facultative - si elle est omise, la fausse-case correspondra toujours. La condition peut être soit un motif, soit le nom d'un groupe de capture. Je me concentre ici sur ce dernier cas. S'il s'agit du nom d'un groupe de capture, alors truePattern est utilisé si et seulement si la pile de capture pour ce groupe particulier n'est pas vide. C'est-à-dire qu'un motif conditionnel comme (?(name)yes|no) lit "si name a trouvé une correspondance et capturé quelque chose (qui est toujours sur la pile), utilisez le motif yes sinon, utilisez le modèle no ".

Ainsi, à la fin de notre modèle ci-dessus, nous pourrions ajouter quelque chose comme (?(Open)failPattern) ce qui entraîne l'échec de l'ensemble du motif, si l'option Open -stack n'est pas vide. La chose la plus simple pour que le motif échoue inconditionnellement est la suivante (?!) (un lookahead négatif vide). Nous avons donc notre modèle final :

^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*(?(Open)(?!))$

Notez que cette syntaxe conditionnelle n'a rien à voir en soi avec l'équilibrage des groupes, mais qu'elle est nécessaire pour exploiter leur pleine puissance.

A partir d'ici, le ciel est la limite. De nombreuses utilisations très sophistiquées sont possibles et il y a quelques problèmes lorsqu'elles sont utilisées en combinaison avec d'autres fonctionnalités de .NET-Regex telles que les lookbehinds de longueur variable ( ce que j'ai dû apprendre à mes dépens ). Cependant, la question principale est toujours la suivante : votre code est-il toujours maintenable lorsque vous utilisez ces fonctionnalités ? Vous devez le documenter très bien, et vous assurer que tous ceux qui travaillent dessus sont également au courant de ces fonctionnalités. Sinon, vous feriez mieux de parcourir la chaîne manuellement, caractère par caractère, et de compter les niveaux d'imbrication dans un nombre entier.

Addendum : C'est quoi le (?<A-B>...) la syntaxe ?

Les crédits pour cette partie vont à Kobi (voir sa réponse ci-dessous pour plus de détails).

Maintenant, avec tout ce qui précède, nous pouvons valider qu'une chaîne est correctement mise entre parenthèses. Mais ce serait beaucoup plus utile si nous pouvions obtenir des captures (imbriquées) de tout le contenu de ces parenthèses. Bien sûr, nous pourrions nous souvenir des parenthèses ouvrantes et fermantes dans une pile de capture séparée qui n'est pas vidée, et ensuite faire une extraction de sous-chaîne basée sur leurs positions dans une étape séparée.

Mais .NET offre une autre fonctionnalité pratique : si nous utilisons la fonction (?<A-B>subPattern) non seulement une capture est extraite de la pile B mais aussi tout ce qui se trouve entre les deux et qui a été capturé de B et ce groupe actuel est poussé sur la pile A . Ainsi, si nous utilisons un groupe comme celui-ci pour les parenthèses fermantes, tout en supprimant les niveaux d'imbrication de notre pile, nous pouvons également pousser le contenu de la paire sur une autre pile :

^(?:[^()]|(?<Open>[(])|(?<Content-Open>[)]))*(?(Open)(?!))$

Kobi a fourni ceci <a href="http://regexstorm.net/tester?p=%28?%3a%5B%5E%7B%7D%5D%7C%28?%3COpen%3E%7B%29%7C%28?%3CContent-Open%3E%7D%29%29%2b%28?%28Open%29%28?!%29%29&amp;i=0%20%7B1%202%20%7B3%7D%20%7B4%205%20%7B6%7D%7D%207%7D%208">Démonstration en direct </a>dans sa réponse

Donc en prenant toutes ces choses ensemble, nous pouvons :

  • Se souvenir d'un nombre arbitraire de captures
  • Valider les structures imbriquées
  • Capturez chaque niveau d'emboîtement

Le tout en une seule expression régulière. Si ce n'est pas excitant... ;)

Quelques ressources que j'ai trouvées utiles lorsque j'ai appris leur existence :

38voto

Kobi Points 65357

Juste un petit ajout à l'excellente réponse de M. Buettner :

C'est quoi le problème avec le (?<A-B>) la syntaxe ?

(?<A-B>x) est subtilement différent de (?<-A>(?<B>x)) . Ils aboutissent au même flux de contrôle * mais ils capture différemment.
Par exemple, examinons un modèle d'accolade équilibrée :

(?:[^{}]|(?<B>{)|(?<-B>}))+(?(B)(?!))

À la fin du match, nous avons une corde équilibrée, mais c'est tout ce que nous avons - nous ne savons pas les bretelles sont parce que le B La pile est vide. Le dur travail que le moteur a fait pour nous est parti.
( <a href="http://regexstorm.net/tester?p=%28?%3a%5B%5E%7B%7D%5D%7C%28?%3COpen%3E%7B%29%7C%28?%3C-Open%3E%7D%29%29%2b%28?%28Open%29%28?!%29%29&amp;i=0%20%7B1%202%20%7B3%7D%20%7B4%205%20%7B6%7D%7D%207%7D%208">exemple sur Regex Storm </a>)

(?<A-B>x) est la solution à ce problème. Comment ? C'est n'a pas capture x en $A : il capture le contenu entre la capture précédente de B et la position actuelle.

Utilisons-le dans notre modèle :

(?:[^{}]|(?<Open>{)|(?<Content-Open>}))+(?(Open)(?!))

Cela permettrait de saisir dans $Content les cordes entre les accolades (et leurs positions), pour chaque paire du parcours.
Pour la chaîne {1 2 {3} {4 5 {6}} 7} il y aurait quatre captures : 3 , 6 , 4 5 {6} et 1 2 {3} {4 5 {6}} 7 - bien mieux que rien ou } } } } .
( <a href="http://regexstorm.net/tester?p=%28?%3a%5B%5E%7B%7D%5D%7C%28?%3COpen%3E%7B%29%7C%28?%3CContent-Open%3E%7D%29%29%2b%28?%28Open%29%28?!%29%29&amp;i=0%20%7B1%202%20%7B3%7D%20%7B4%205%20%7B6%7D%7D%207%7D%208">exemple - cliquez sur le <code>table</code> et regardez <code>${Content}</code> , les captures </a>)

En fait, il peut être utilisé sans aucun équilibrage : (?<A>).(.(?<Content-A>).) capture les deux premiers personnages, même s'ils sont séparés par des groupes.
(un lookahead est plus couramment utilisé ici, mais il n'est pas toujours adapté : il peut dupliquer votre logique).

(?<A-B>) est une caractéristique forte - elle vous donne exact le contrôle de vos captures. Gardez cela à l'esprit lorsque vous essayez de tirer le meilleur parti de votre modèle.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X