9
votes

Est-il possible d'effacer un groupe de capture qui a déjà correspondu, le rendant non participant?

Dans PCRE2 ou dans tout autre moteur d'expression régulière prenant en charge les références inverses, est-il possible de changer un groupe de capture correspondant à une itération précédente d'une boucle en un groupe de capture non participant (également appelé groupe de capture non défini ou groupe non capturé ), ce qui fait que les conditions qui testent ce groupe correspondent à leur clause "false" plutôt qu'à leur clause "true"?

Pour Par exemple, prenez l'expression régulière PCRE suivante:

^(?:(?=a|(b)).(?(1)_))*$

Lorsqu'il est alimenté avec la chaîne zaazaa , il correspond à la chaîne entière, comme vous le souhaitez. Mais une fois nourri zaaaa , je voudrais qu'il corresponde à zaaa ; à la place, il correspond à zaaaa , la chaîne entière. (Ceci est juste à titre d'illustration. Bien sûr, cet exemple pourrait être géré par ^ (?: zaa | a) {2} mais ce n'est pas la question. L'utilisation pratique de l'effacement de groupe de capture aurait tendance à être dans des boucles qui font le plus souvent bien plus de 2 itérations.)

Une autre façon de faire cela, qui ne fonctionne pas non plus comme souhaité:

\b(?:a()|e()|i()|o()|u()|\w)++\1\2\3\4\5\b

Notez que les deux fonctionnent comme vous le souhaitez lorsque la boucle est "déroulée", car ils n'ont plus à effacer une capture qui a déjà été faite:

^(?:(z?)(?:(?!.*$\1)aa|(?=.*$\1)a)){2}

Donc au lieu de pouvant utiliser la forme la plus simple du conditionnel, il faut en utiliser une plus compliquée, qui ne fonctionne que dans cet exemple car la "vraie" correspondance de z n'est pas vide:

^(?:(z?)(?(?!.*$\1)aa|a)){2}

Ou simplement en utilisant un conditionnel émulé:

^(?:(z)?(?(1)aa|a))(?:(z)?(?(2)aa|a))
^(?:(?:z()|())(?:\1aa|\2a))(?:(?:z()|())(?:\3aa|\4a))

J'ai parcouru toute la documentation que je peux trouver, et il ne semble même pas y avoir de mention ou une description explicite de ce comportement (les captures effectuées dans une boucle persistent à travers les itérations de cette boucle même lorsqu'elles échoue à être recapturé).

C'est différent de ce à quoi je m'attendais intuitivement. La façon dont je le mettrais en œuvre est que l'évaluation d'un groupe de capture avec 0 répétition l'effacerait / le désarmerait (cela pourrait donc arriver à n'importe quel groupe de capture avec un * , ? , ou {0, N} quantifier), mais l'ignorer parce qu'il se trouve dans une alternative parallèle au sein du même groupe dans lequel il a obtenu une capture lors d'une itération précédente ne l'effacerait pas. Ainsi, cette expression régulière correspondrait toujours aux mots ssi ils contiennent au moins une de chaque voyelle : p >

^(?:(?:z()|())(?:\1aa|\2a)){2}

Mais sauter un groupe de capture car il se trouve à l'intérieur d'une alternative non évaluée d'un groupe qui est évalué avec des répétitions différentes de zéro qui est imbriqué dans le groupe dans lequel le groupe de capture a pris une valeur pendant une itération précédente ferait l'effacer / le désarmer, donc cette expression régulière serait capable de capturer ou d'effacer le groupe \ 1 à chaque itération de la boucle:

^(?:(z)?(?(1)aa|a)){2}

et correspondrait à des chaînes telles que aaab_ab_b_aaaab_ab_aab_b_b_aaa . Cependant, la façon dont les références sont en fait implémentées dans les moteurs existants, elle correspond à aaaaab_a_b_a_a_b_b_a_b_b_b_.

Je voudrais connaître la réponse à cette question non seulement parce qu'elle serait utile pour regexes, mais parce que j'ai écrit mon propre moteur regex , actuellement compatible ECMAScript avec certaines extensions facultatives (y compris lookahead (? *) , c'est-à-dire lookahead non atomique, qui pour autant que je sache, aucun autre moteur n'a), et je voudrais continuer à ajouter des fonctionnalités d'autres moteurs, y compris des références arrière en avant / imbriquées. Non seulement je veux que mon implémentation des backreferences avant soit compatible avec les implémentations existantes, mais s'il n'y a pas un moyen d'effacer les groupes de capture dans d'autres moteurs, je vais probablement créer un moyen de le faire dans mon moteur qui n'entre pas en conflit avec d'autres fonctionnalités de regex existantes.

Pour être clair: une réponse indiquant que ce n'est pas possible dans les moteurs traditionnels sera acceptable, à condition qu'elle soit étayée par des recherches adéquates et / ou citant des sources. Une réponse indiquant que cela est possible serait beaucoup plus facile à énoncer, car elle ne nécessiterait qu'un seul exemple.

Quelques informations sur ce qu'est un groupe de capture non participant:
http://blog.stevenlevithan.com/archives/npcg-javascript - c'est le article qui m'a à l'origine présenté l'idée.
https://www.regular-expressions.info/backref2.html - la première section sur cette page donne une brève explication.
Dans les expressions régulières ECMAScript / Javascript, les références arrière aux NPCG correspondent toujours (ce qui fait une correspondance de longueur nulle). Dans presque toutes les autres variantes de regex, ils ne correspondent à rien.


6 commentaires

Je crois que \ K dira au moteur regex d'effacer tous les groupes de capture, mais je ne comprends pas ce que vous essayez de faire ici.


La seule erreur que vous faisiez dans le premier Regex de la question était de lui demander de capturer le premier groupe 2 fois, ce qui était aa. Je l'ai donc supprimé, laissez tout le groupe capturer puis laissez-le se répéter si vous le souhaitez ou au moins une fois.


@Deep Merci, mais vous avez mal compris ma question. L'exemple que j'ai donné n'était qu'un exemple de jouet. Je veux pouvoir effacer des groupes de capture tout en restant dans une boucle et en continuant à boucler. Je ne lui ai donné que des répétitions de {2} pour en faire un exemple très simple; en pratique, j'utiliserais principalement ceci sur des boucles illimitées comme (...) + et (...) * ... signifie tout ce qui irait dans la boucle.


Pouvez-vous mettre une chaîne d'exemple quelque part où nous pouvons jouer avec les données. Ce serait plus facile pour moi de comprendre.


@Deep J'essaierai, mais ce n'est pas une tâche d'exemple particulière qui compte dans ce cas, c'est la manière de procéder. Il n'y a aucune tâche qui nécessite cela, c'est juste que pouvoir effacer une capture pourrait rendre certaines tâches réalisables d'une manière plus élégante.


@Tim Biegeleisen \ K ne fait que changer l'endroit où le match final commence, et n'affecte en rien le contenu des groupes de capture. Je ne me soucie pas vraiment du match final dans cet exemple; ce n'est qu'un exemple pour différencier et démontrer / expliquer ce que je veux faire à l'intérieur de la boucle. Je souhaite effacer le groupe de capture pendant une boucle, tout en restant dans la boucle.


3 Réponses :


5
votes

J'ai trouvé ceci documenté dans la page de manuel de PCRE, sous "DIFFÉRENCES ENTRE PCRE2 ET PERL":

\A(?![\s\S]*^(?!(?:(.)(?=.*(\1(?(2)\2))))*+.?\2$))

J'ai du mal à penser à un problème pratique qui ne peut pas être mieux résolu avec une solution alternative, mais dans l'intérêt de la simplicité, voici:

Supposons que vous ayez un tâche simple bien adaptée à être résolue en utilisant des références directes; par exemple, vérifiez que la chaîne d'entrée est un palindrome. Cela ne peut pas être résolu en général avec la récursivité (en raison de la nature atomique des appels de sous-programmes), et donc nous expliquons ce qui suit:

\A(?:^(?:(.)(?=.*(\1(?(2)(?=\2\3\z)\2))([\s\S]*)))*+.?\2$(?:\n|\z))+\z 

Assez facile. Supposons maintenant qu'on nous demande de vérifier que chaque ligne de l'entrée est un palindrome. Essayons de résoudre ce problème en plaçant l'expression dans un groupe répété:

\A(?|^(?:(.)(?=.*(\1(?(2)\2))))*+.?\2$|\n()()|\z)+\z

Clairement cela ne fonctionne pas, puisque la valeur de \ 2 persiste de la première ligne à la suivante . Ceci est similaire au problème auquel vous êtes confronté, et voici donc un certain nombre de façons de le résoudre:

1. Insérez la sous-expression entière dans (?! (?!)) :

\A(?:(?!(?!^(?:(.)(?=.*(\1(?(2)\2))))*+.?\2$)).+(?:\n|\z))+\z

Très facile, il suffit de les pousser dedans et vous êtes essentiellement prêt à partir. Ce n'est pas une excellente solution si vous souhaitez que des valeurs capturées particulières persistent.

2. Groupe de réinitialisation de branche pour réinitialiser la valeur des groupes de capture:

\A(?:^(?:(.)(?=.*(\1(?(2)\2))))*+.?\2$(?:\n|\z))+\z

Avec cette technique, vous pouvez réinitialiser la valeur des groupes de capture à partir du premier (\ 1 dans ce case) jusqu'à un certain (\ 2 ici). Si vous devez conserver la valeur de \ 1 mais effacer \ 2, cette technique ne fonctionnera pas.

3. Introduisez un groupe qui capture le reste de la chaîne à partir d'une certaine position pour vous aider à identifier plus tard où vous êtes:

/^(?:(.)(?=.*(\1(?(2)\2))))*+.?\2$/

Le reste de la collection de lignes est enregistré dans \ 3, ce qui vous permet de vérifier de manière fiable si vous êtes passé à la ligne suivante (lorsque (? = \ 2 \ 3 \ z) n'est plus vrai).

C'est l'une de mes techniques préférées car elle peut être utilisée pour résoudre des tâches qui paraissent impossibles, comme l'ol ' correspondance des crochets imbriqués en utilisant des références directes . Avec lui, vous pouvez conserver toutes les autres informations de capture dont vous avez besoin. Le seul inconvénient est qu'il est horriblement inefficace, en particulier pour les longs sujets.

4. Cela ne répond pas vraiment à la question, mais cela résout le problème:

   12.  There are some differences that are concerned with the settings of
   captured strings when part of  a  pattern  is  repeated.  For  example,
   matching  "aba"  against  the  pattern  /^(a(b)?)+$/  in Perl leaves $2
   unset, but in PCRE2 it is set to "b".

C'est la solution alternative dont je parlais. En gros, «réécrivez le modèle» :) Parfois c'est possible, parfois non.


2 commentaires

En remarque, la partie différence n'est pas spécifique à PCRE2.


+1, Très belle réponse (pas techniquement une réponse mais toujours utile), avec un excellent exemple de problème qui bénéficierait de l'effacement du groupe de capture, et au moins une méthode de contournement du problème auquel je n'avais pas pensé. Et chose intéressante à savoir sur Perl; / ^ (? :( z)? (? (1) aa | a)) {2} / fonctionne réellement comme je le souhaite (différemment de PCRE). Cependant, la version alternative, / ^ (? :(? :( z) |) (? (1) aa | a)) {2} / , fonctionne de la même manière en Perl et PCRE (qui ce n'est pas la façon dont je veux).



5
votes

Avec PCRE (et tout ce que je sais), il n'est pas possible de désarmer un groupe de capture, mais en utilisant des appels de sous-programmes puisque leur nature ne se souvient pas des valeurs de la récursivité précédente, vous pouvez accomplir la même tâche:

(?(DEFINE)((z)?(?(2)aa|a)))^(?1){2}

Voir la démo en direct ici

Si vous allez implémenter un comportement dans votre propre saveur regex pour désactiver un groupe de capture, je suggère fortement de ne pas le laisser se produire automatiquement. Fournissez simplement quelques indicateurs.


1 commentaires

C'est en effet une méthode viable dans certains cas, mais l'inconvénient est que vous ne pouvez pas non plus renvoyer de capture à partir d'un appel de sous-programme dans PCRE. Le choix est soit de capturer l'intégralité de ce à quoi correspond le sous-programme, soit de ne pas le capturer.



4
votes

Cela est partiellement possible dans la version .NET de l'expression régulière.

La première chose à noter est que .NET enregistre toutes les captures pour un groupe de capture donné, pas seulement la dernière. Par exemple, ^ (? = (.) *) enregistre chaque caractère de la première ligne comme une capture distincte dans le groupe.

Pour supprimer réellement les captures, .NET regex a une construction connue sous le nom de groupes d'équilibrage . Le format complet de cette construction est (? subexpression) .

  • Tout d'abord, name2 doit avoir été préalablement capturé.
  • La sous-expression doit alors correspondre.
  • Si name1 est présent, la sous-chaîne entre la fin de la capture de name2 et le début de la correspondance de sous-expression est capturée dans name1 .
  • La dernière capture de nom2 est alors supprimée. (Cela signifie que l'ancienne valeur pourrait être référencée en arrière dans la sous-expression.)
  • La correspondance est avancée jusqu'à la fin de la sous-expression.

Si vous savez que name2 a été capturé une seule fois, il peut être facilement supprimé en utilisant (? <-name2>) ; si vous ne savez pas si vous avez capturé name2 , vous pouvez utiliser (?> (? <-name2>)?) ou un conditionnel. Le problème se pose si vous pouvez avoir capturé name2 plus d'une fois depuis, cela dépend de la possibilité d'organiser suffisamment de répétitions de la suppression de name2 . ( (? <-name2>) * ne fonctionne pas car * équivaut à ? pour les correspondances de longueur nulle.)


0 commentaires