32 votes

Réduire et capturer un motif répétitif dans une seule expression Regex

Je me retrouve constamment dans des situations où je dois capturer un certain nombre d'éléments d'une chaîne de caractères et, après d'innombrables essais, je n'ai pas trouvé de moyen de simplifier le processus.

Disons que le texte est :

début:test-test-lorem-ipsum-sir-doloret-etc-etc-quelque chose:fin

Cet exemple contient 8 articles, mais on peut dire qu'il pourrait avoir entre 3 et 10 articles.

Idéalement, j'aimerais quelque chose comme ça :
start:(?:(\w+)-?){3,10}:end beau et propre MAIS il ne capture que le dernier match. voir ici

J'utilise généralement quelque chose comme ça dans des situations simples :

start:(\w+)-(\w+)-(\w+)-?(\w+)?-?(\w+)?-?(\w+)?-?(\w+)?-?(\w+)?-?(\w+)?-?(\w+)?:end

3 groupes obligatoires et 7 autres facultatifs en raison de la limite maximale de 10, mais cela n'a pas l'air "joli" et ce serait une douleur à écrire et à suivre si la limite maximale était de 100 et si les matches étaient plus complexes. Démo

Et le meilleur que j'ai pu faire jusqu'à présent :

start:(\w+)-((?1))-((?1))-?((?1))?-?((?1))?-?((?1))?-?((?1))?-?((?1))?:end

plus courts, surtout si les matches sont complexes, mais toujours longs. Démo

Quelqu'un a réussi à le faire fonctionner comme une solution à 1 regex seulement. sans programmation ?

Je suis surtout intéressé par la façon dont cela peut être fait dans PCRE, mais d'autres saveurs seraient également acceptables.

Mise à jour :

L'objectif est de valider une correspondance et de capturer les jetons individuels à l'intérieur. match 0 par RegEx uniquement, sans aucune limitation de système d'exploitation, de logiciel ou de langage de programmation.

Mise à jour 2 (prime) :

Avec l'aide de @nhahtdh, je suis arrivé au RegExp ci-dessous en utilisant \G :

(?:start:(?=(?:[\w]+(?:-|(?=:end))){3,10}:end)|(?!^)\G-)([\w]+)

Démo encore plus courte, mais peut être décrite sans répéter le code

Je suis également intéressé par la version de l'ECMA et comme elle ne supporte pas \G Je me demande s'il y a un autre moyen, notamment sans utiliser /g modificateur.

36voto

nhahtdh Points 28167

Lisez ceci d'abord !

Ce billet vise à montrer les possibilités plutôt qu'à approuver l'approche "tout regex" du problème. L'auteur a écrit 3-4 variations, chacune ayant un bug subtil qui est délicat à détecter, avant d'arriver à la solution actuelle.

Pour votre exemple spécifique, il existe d'autres solutions plus faciles à maintenir, comme la mise en correspondance et la division de la correspondance le long des délimiteurs.

Ce billet traite de votre exemple spécifique. Je doute vraiment qu'une généralisation complète soit possible, mais l'idée sous-jacente est réutilisable pour des cas similaires.

Résumé

  • .NET prend en charge la capture de motifs répétitifs avec CaptureCollection classe.
  • Pour les langues qui supportent \G et look-behind, nous pouvons être en mesure de construire une regex qui fonctionne avec la fonction de correspondance globale. Il n'est pas facile d'écrire une regex complètement correcte et il est facile d'écrire une regex subtilement boguée.
  • Pour les langues sans \G et la prise en charge du look-behind : il est possible d'émuler \G avec ^ en hachant la chaîne d'entrée après une seule correspondance. (Non couvert dans cette réponse).

Solution

Cette solution suppose que le moteur de regex supporte \G limite de correspondance, anticipation (?=pattern) et le look-behind (?<=pattern) . Les saveurs regex de Java, Perl, PCRE, .NET, Ruby prennent en charge toutes les fonctionnalités avancées ci-dessus.

Cependant, vous pouvez utiliser votre regex dans .NET. Puisque .NET prend en charge la capture de toutes les instances de qui correspond à un groupe de capture qui est répété via CaptureCollection classe.

Pour votre cas, cela peut être fait en une seule regex, avec l'utilisation de \G la limite de la correspondance, et le look-ahead pour limiter le nombre de répétitions :

(?:start:(?=\w+(?:-\w+){2,9}:end)|(?<=-)\G)(\w+)(?:-|:end)

DEMO . La construction est \w+- répétées, puis \w+:end .

(?:start:(?=\w+(?:-\w+){2,9}:end)|(?!^)\G-)(\w+)

DEMO . La construction est \w+ pour le premier élément, puis -\w+ répétées. (Merci à ka ᵠ pour la suggestion). Cette construction est plus simple pour raisonner sur sa justesse, car il y a moins d'alternances.

\G match boundary est particulièrement utile lorsque vous avez besoin de faire de la tokenisation, où vous devez vous assurer que le moteur ne saute pas en avant et ne correspond pas à des choses qui auraient dû être invalides.

Explication

Décomposons la regex :

(?:
  start:(?=\w+(?:-\w+){2,9}:end)
    |
  (?<=-)\G
)
(\w+)
(?:-|:end)

La partie la plus facile à reconnaître est (\w+) dans l'avant-dernière ligne, qui est le mot que vous voulez capturer.

La dernière ligne est également assez facile à reconnaître : le mot à mettre en correspondance peut être suivi de - o :end .

Je permets à la regex de commencer librement la correspondance n'importe où dans la chaîne . En d'autres termes, start:...:end peut apparaître n'importe où dans la chaîne, et un nombre quelconque de fois ; la regex fera simplement correspondre tous les mots. Vous n'avez qu'à traiter le tableau retourné pour séparer l'origine des mots correspondants.

Quant à l'explication, le début de la regex vérifie la présence de la chaîne de caractères start: et le look-ahead suivant vérifie que le nombre de mots est dans la limite spécifiée et il se termine par :end . Soit ça, soit nous vérifions que le caractère qui précède la correspondance précédente est un - et reprendre le match précédent.

Pour l'autre construction :

(?:
  start:(?=\w+(?:-\w+){2,9}:end)
    |
  (?!^)\G-
)
(\w+)

Tout est presque identique, sauf que nous correspondons start:\w+ d'abord avant de faire correspondre la répétition de la forme -\w+ . Contrairement à la première construction, où nous faisons correspondre start:\w+- d'abord, et les instances répétées de \w+- (ou \w+:end pour la dernière répétition).

Il est assez délicat de faire fonctionner cette regex pour la correspondance au milieu de la chaîne :

  • Nous devons vérifier le nombre de mots entre start: y :end (dans le cadre de l'exigence de la regex originale).

  • \G correspond aussi au début de la chaîne ! (?!^) est nécessaire pour éviter ce comportement. Si l'on ne prend pas garde à cela, l'expression rationnelle peut produire une correspondance alors qu'il n'y en a pas. start: .

    Pour la première construction, le look-behind (?<=-) empêchent déjà ce cas ( (?!^) est impliquée par (?<=-) ).

  • Pour la première construction (?:start:(?=\w+(?:-\w+){2,9}:end)|(?<=-)\G)(\w+)(?:-|:end) nous devons nous assurer que nous ne correspondons pas à quelque chose de drôle après :end . Le look-behind est là pour ça : il empêche tout garbage après l'exécution de :end de la correspondance.

    La deuxième construction ne rencontre pas ce problème, puisque nous resterons bloqués à : (de :end ) après avoir fait correspondre tous les jetons entre eux.

Version de validation

Si vous voulez valider que la chaîne d'entrée respecte le format (pas d'éléments supplémentaires devant et derrière), et extraire les données, vous pouvez ajouter des ancres comme telles :

(?:^start:(?=\w+(?:-\w+){2,9}:end$)|(?!^)\G-)(\w+)
(?:^start:(?=\w+(?:-\w+){2,9}:end$)|(?!^)\G)(\w+)(?:-|:end)

(Look-behind n'est pas non plus nécessaire, mais nous avons toujours besoin de (?!^) pour éviter \G de correspondre au début de la chaîne).

Construction

Pour tous les problèmes où vous voulez capturer toutes les instances d'une répétition, je ne pense pas qu'il existe un moyen général de modifier la regex. Un exemple de cas "difficile" (ou impossible ?) à convertir est lorsqu'une répétition doit revenir en arrière d'une ou plusieurs boucles pour remplir certaines conditions de correspondance.

Lorsque la regex d'origine décrit la totalité de la chaîne d'entrée (type validation), il est généralement plus facile de la convertir qu'une regex qui essaie de correspondre à partir du milieu de la chaîne (type correspondance). Cependant, vous pouvez toujours faire une correspondance avec la regex originale, et nous reconvertissons le problème de type correspondance en problème de type validation.

Nous construisons une telle regex en passant par les étapes suivantes :

  • Écrivez une regex qui couvre la partie avant la répétition (par ex. start: ). Appelons cela préfixe regex .
  • Correspondance et capture de la première instance. (ex. (\w+) )
    (A ce stade, la première instance et le premier délimiteur auraient dû correspondre).
  • Ajouter le \G en alternance. En général, il faut aussi l'empêcher de correspondre au début de la chaîne.
  • Ajoutez le délimiteur (le cas échéant). (par exemple - )
    (Après cette étape, le reste des jetons devrait également avoir été apparié, sauf peut-être le dernier).
  • Ajoutez la partie qui couvre la partie après la répétition (si nécessaire) (ex. :end ). Appelons la partie après la répétition regex de suffixe (que nous l'ajoutions ou non à la construction n'a pas d'importance).
  • Maintenant, la partie difficile. Vous devez vérifier ça :
    • Il n'y a pas d'autre moyen de commencer un match, en dehors de la fonction préfixe regex . Prenez note de la \G branche.
    • Il n'y a aucun moyen de commencer un match après le site regex de suffixe a été apparié. Prenez note de la façon dont \G la branche commence un match.
    • Pour la première construction, si vous mélangez le suffixe regex (par ex. :end ) avec un délimiteur (par ex. - ) dans une alternance, assurez-vous que vous ne finissez pas par autoriser le suffixe regex comme délimiteur.

6voto

Jack Points 88446

Bien qu'il soit théoriquement possible d'écrire une seule expression, il est beaucoup plus pratique de faire correspondre d'abord les limites extérieures, puis d'effectuer un fractionnement sur la partie intérieure.

En ECMAScript, je l'écrirais comme ceci :

'start:test-test-lorem-ipsum-sir-doloret-etc-etc-something:end'
    .match(/^start:([\w-]+):end$/)[1] // match the inner part
    .split('-') // split inner part (this could be a split regex as well)

En PHP :

$txt = 'start:test-test-lorem-ipsum-sir-doloret-etc-etc-something:end';
if (preg_match('/^start:([\w-]+):end$/', $txt, $matches)) {
    print_r(explode('-', $matches[1]));
}

1voto

minopret Points 2735

Bien sûr, vous pouvez utiliser le regex dans cette chaîne entre guillemets.

"(?<a>\\w+)-(?<b>\\w+)-(?:(?<c>\\w+)" \
"(?:-(?<d>\\w+)(?:-(?<e>\\w+)(?:-(?<f>\\w+)" \
"(?:-(?<g>\\w+)(?:-(?<h>\\w+)(?:-(?<i>\\w+)" \
"(?:-(?<j>\\w+))?" \
")?)?)?" \
")?)?)?" \
")"

Est-ce une bonne idée ? Non, je ne le pense pas.

0voto

spiralx Points 534

Je ne suis pas sûr que vous puissiez le faire de cette façon, mais vous pouvez utiliser le drapeau global pour trouver tous les mots entre les deux points :

http://regex101.com/r/gK0lX1

Vous devrez cependant valider vous-même le nombre de groupes. Sans le drapeau global, vous n'obtiendrez qu'une seule correspondance, pas toutes les correspondances - changez {3,10} a {1,5} et vous obtenez le résultat "monsieur" à la place.

import re

s = "start:test-test-lorem-ipsum-sir-doloret-etc-etc-something:end"
print re.findall(r"(\b\w+?\b)(?:-|:end)", s)

produit

['test', 'test', 'lorem', 'ipsum', 'sir', 'doloret', 'etc', 'etc', 'something']

0voto

mrhobo Points 3257

Quand vous combinez :

  1. Votre observation : tout type de répétition d'un groupe de capture unique entraînera un écrasement de la dernière capture, ne renvoyant ainsi que la dernière capture du groupe de capture.
  2. Le savoir : Tout type de capture basé sur les parties, au lieu de l'ensemble, rend impossible la fixation d'une limite au nombre de répétitions du moteur regex. La limite devrait être fixée par les métadonnées (et non par l'expression rationnelle).
  3. La réponse ne doit pas comporter de programmation (boucles), ni de copier-coller de groupes de capture comme vous l'avez fait dans votre question.

On peut en déduire que c'est impossible.

Mise à jour : Il y a certains moteurs de regex pour lesquels p. 1 n'est pas nécessairement vrai. Dans ce cas, la regex que vous avez indiquée start:(?:(\w+)-?){3,10}:end fera l'affaire ( source ).

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