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.