web-dev-qa-db-fra.com

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

Je venais de lire une question sur la façon d'obtenir des données entre accolades doubles ( cette question ), puis quelqu'un a créé des groupes d'équilibrage. Je ne sais toujours pas exactement ce que c'est et comment les utiliser.

J'ai lu Balancing Group Definition , mais l'explication est difficile à suivre, et je suis encore assez confus sur les questions que j'ai mentionnées.

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

85
It'sNotALie.

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

Mis à part: 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 regex qui vous permet d'accéder à plusieurs captures d'un même groupe de capture (pas dans les références arrières mais après la fin du match).

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

(.)+

et la chaîne "abcd".

dans toutes les autres saveurs regex, capture du groupe 1 donnera simplement un résultat: d (notez que la correspondance complète sera bien sûr abcd comme prévu). En effet, chaque nouvelle utilisation du groupe de capture écrase la capture précédente.

.NET d'autre part se souvient de tous. Et il le fait dans une pile. Après avoir fait correspondre l'expression régulière 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 nombre est l'index dans le CaptureCollection. Donc, fondamentalement, chaque fois que le groupe est utilisé à nouveau, une nouvelle capture est insérée dans la pile.

Cela devient plus intéressant si nous utilisons des groupes de capture nommés. Parce que .NET permet l'utilisation répétée du même nom, nous pourrions écrire une expression régulière comme

(?<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, appliquer cette expression régulière à l'entrée "foo bar" et inspection

m.Groups["Word"].Captures

on trouve 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 encore, c'est juste la fonctionnalité de .NET de pouvoir suivre plusieurs captures qui sont répertoriées dans ce CaptureCollection. Mais je l'ai dit, cette collection est une pile . Alors, pouvons-nous pop les choses?

Entrer: Groupes d'équilibrage

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

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

Ensuite, le deuxième groupe affichera la capture du premier groupe et nous recevrons un CaptureCollection vide à la fin. Bien sûr, cet exemple est assez inutile.

Mais il y a un détail de plus à 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 groupe d'équilibrage des noms (et où il devient intéressant). Disons que nous voulons faire correspondre les chaînes qui sont correctement entre parenthèses. Nous poussons chaque parenthèse ouvrante sur la pile et faisons une capture pour chaque parenthèse fermante. Si nous rencontrons une parenthèse fermante de trop, elle essaiera de faire apparaître une pile vide et entraînera l'échec du modèle:

^(?:[^()]|(?<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 à (s tout en les poussant sur la pile. La troisième alternative correspond à )s lors de l'extraction d'éléments de la pile (si possible!).

Remarque: Juste pour clarifier, nous vérifions seulement qu'il n'y a pas de parenthèses inégalées! Cela signifie que la chaîne ne contenant aucune parenthèse du tout sera correspond, car elles sont toujours syntaxiquement valides (dans une syntaxe où vous avez besoin que vos parenthèses correspondent). Si vous voulez garantir au moins un jeu de parenthèses, ajoutez simplement un lookahead (?=.*[(]) juste après le ^.

Ce modèle n'est cependant pas parfait (ou tout à fait correct).

Finale: modèles conditionnels

Il y a encore un hic: cela ne garantit pas que la pile est vide à la fin de la chaîne (d'où (foo(bar) serait valide). .NET (et de nombreuses autres versions) ont une autre construction qui nous aide ici: les modèles conditionnels. La syntaxe générale est

(?(condition)truePattern|falsePattern)

falsePattern est facultatif - s'il est omis, le faux-cas correspondra toujours. La condition peut être soit un modèle, soit le nom d'un groupe de capture. Je vais me concentrer sur ce dernier cas ici. Si c'est le 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. Autrement dit, un modèle conditionnel comme (?(name)yes|no) lit "si name a correspondu et capturé quelque chose (qui est toujours sur la pile), utilisez le modèle yes sinon utilisez le modèle no".

Donc, à la fin de notre schéma ci-dessus, nous pourrions ajouter quelque chose comme (?(Open)failPattern) ce qui provoque l'échec de l'ensemble du modèle, si la pile Open- n'est pas vide. La chose la plus simple pour faire échouer inconditionnellement le modèle est (?!) (un regard négatif négatif). Nous avons donc notre modèle final:

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

Notez que cette syntaxe conditionnelle n'a rien à voir avec les groupes d'équilibrage, mais il est nécessaire d'exploiter leur pleine puissance.

De là, le ciel est la limite. De nombreuses utilisations très sophistiquées sont possibles et il y a quelques pièges lorsqu'ils sont utilisés en combinaison avec d'autres fonctionnalités .NET-Regex comme les lookbehinds de longueur variable ( que j'ai dû apprendre moi-même à la dure ). La question principale est cependant toujours: votre code est-il toujours maintenable lors de l'utilisation de ces fonctionnalités? Vous devez le documenter très bien et assurez-vous que tous ceux qui y travaillent connaissent également ces fonctionnalités. Sinon, vous pourriez être mieux, simplement en parcourant la chaîne manuellement caractère par caractère et en comptant les niveaux d'imbrication dans un entier.

Addendum: Qu'est-ce que le (?<A-B>...) 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 entre parenthèses. Mais ce serait beaucoup plus utile, si nous pouvions réellement obtenir des captures (imbriquées) pour tous les contenus de ces parenthèses. Bien sûr, nous pourrions nous souvenir d'ouvrir et de fermer des parenthèses dans une pile de capture distincte qui n'est pas vidée, puis d'effectuer une extraction de sous-chaîne en fonction de leur position dans une étape distincte.

Mais .NET fournit une fonctionnalité supplémentaire ici: si nous utilisons (?<A-B>subPattern), non seulement une capture est extraite de la pile B, mais aussi tout ce qui se trouve entre cette capture éclatée de B et ce groupe actuel est poussé sur la pile A. Donc, si nous utilisons un groupe comme celui-ci pour les parenthèses fermantes, tout en extrayant 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 Live-Demo dans sa réponse

Donc, en prenant toutes ces choses ensemble, nous pouvons:

  • Rappelez-vous arbitrairement de nombreuses captures
  • Valider les structures imbriquées
  • Capturez chaque niveau d'imbrication

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

Quelques ressources que j'ai trouvées utiles lorsque j'ai entendu parler d'eux:

166
Martin Ender

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

Quel est le problème avec le (?<A-B>) syntaxe?

(?<A-B>x) est subtilement différent de (?<-A>(?<B>x)). Ils se traduisent par le même flux de contrôle*, mais ils capturent différemment.
Par exemple, examinons un modèle d'appareil orthopédique équilibré:

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

À la fin du match, nous avons une chaîne équilibrée, mais c'est tout ce que nous avons - nous ne savons pas les accolades sont parce que le B la pile est vide. Le travail acharné que le moteur a fait pour nous a disparu.
( exemple sur Regex Storm )

(?<A-B>x) est la solution à ce problème. Comment? Il ne capture pas x dans $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 capturerait dans $Content les cordes entre les accolades (et leurs positions), pour chaque paire en cours de route.
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 }}}}.
( exemple - cliquez sur l'onglet table et regardez ${Content}, capture )

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

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

38
Kobi