Ceci est la deuxième partie d'une série d'articles éducatifs sur les expressions rationnelles. Il montre comment les anticipations et les références imbriquées peuvent être utilisées pour faire correspondre la langue non régulière anbn. Les références imbriquées sont d'abord introduites dans: Comment cette expression régulière trouve-t-elle des nombres triangulaires?
L'un des archétypes non - langues régulières est:
L = { a
nb
n: n > 0 }
Il s'agit du langage de toutes les chaînes non vides consistant en un certain nombre de a
suivis d'un nombre égal de b
. Des exemples de chaînes dans ce langage sont ab
, aabb
, aaabbb
.
Cette langue peut être non régulière par le lemme de pompage . Il s'agit en fait d'un archétype langage sans contexte , qui peut être généré par la grammaire sans contexteS → aSb | ab
.
Néanmoins, les implémentations de regex modernes reconnaissent clairement plus que les langages normaux. Autrement dit, ils ne sont pas "réguliers" selon la définition formelle de la théorie du langage. PCRE et Perl prennent en charge l'expression régulière récursive et .NET prend en charge la définition des groupes d'équilibrage. Encore moins de fonctionnalités "fantaisie", par ex. correspondance de référence arrière, signifie que l'expression régulière n'est pas régulière.
Mais quelle est la puissance de ces fonctionnalités "de base"? Pouvons-nous reconnaître L
avec Java regex, par exemple? Pouvons-nous peut-être combiner des contournements et des références imbriquées et avoir un modèle qui fonctionne avec par exemple String.matches
pour faire correspondre des chaînes comme ab
, aabb
, aaabbb
, etc.?
Java.util.regex.Pattern
La réponse est, inutile de dire, OUI ! Vous pouvez très certainement écrire un Java motif regex pour correspondre à anbn. Il utilise une anticipation positive pour l'assertion et une référence imbriquée pour le "comptage".
Plutôt que de donner immédiatement le modèle, cette réponse guidera les lecteurs à travers le processus de le dériver. Divers conseils sont donnés au fur et à mesure que la solution se construit lentement. Dans cet aspect, j'espère que cette réponse contiendra bien plus qu'un simple motif regex soigné. Avec un peu de chance, les lecteurs apprendront également à "penser en expression rationnelle" et à assembler harmonieusement diverses constructions, afin de pouvoir dériver plus de modèles par eux-mêmes à l'avenir.
Le langage utilisé pour développer la solution sera PHP pour sa concision. Le test final une fois le modèle finalisé sera fait en Java.
Commençons par un problème plus simple: nous voulons faire correspondre a+
Au début d'une chaîne, mais seulement si elle est suivie immédiatement par b+
. Nous pouvons utiliser ^
Pour ancre notre correspondance, et puisque nous voulons seulement faire correspondre le a+
Sans le b+
, Nous pouvons utiliser lookahead assertion (?=…)
.
Voici notre modèle avec un simple harnais de test:
function testAll($r, $tests) {
foreach ($tests as $test) {
$isMatch = preg_match($r, $test, $groups);
$groupsJoined = join('|', $groups);
print("$test $isMatch $groupsJoined\n");
}
}
$tests = array('aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb');
$r1 = '/^a+(?=b+)/';
# └────┘
# lookahead
testAll($r1, $tests);
La sortie est ( comme vu sur ideone.com ):
aaa 0
aaab 1 aaa
aaaxb 0
xaaab 0
b 0
abbb 1 a
C'est exactement la sortie que nous voulons: nous faisons correspondre a+
, Seulement si c'est au début de la chaîne, et seulement s'il est immédiatement suivi de b+
.
Leçon: vous pouvez utiliser des modèles dans les contournements pour faire des assertions.
Maintenant, disons que même si nous ne voulons pas que le b+
Fasse partie du match, nous voulons quand même capturer dans le groupe 1. De plus, comme nous prévoyons d'avoir un modèle plus compliqué, utilisons le modificateur x
pour espace libre afin que nous puissions rendre notre expression rationnelle plus lisible.
En nous appuyant sur notre précédent PHP snippet, nous avons maintenant le modèle suivant:
$r2 = '/ ^ a+ (?= (b+) ) /x';
# │ └──┘ │
# │ 1 │
# └────────┘
# lookahead
testAll($r2, $tests);
La sortie est maintenant ( comme vu sur ideone.com ):
aaa 0
aaab 1 aaa|b
aaaxb 0
xaaab 0
b 0
abbb 1 a|bbb
Notez que par ex. aaa|b
Est le résultat de join
- ce que chaque groupe a capturé avec '|'
. Dans ce cas, le groupe 0 (c'est-à-dire ce à quoi correspondait le modèle) a capturé aaa
et le groupe 1 a capturé b
.
Leçon: Vous pouvez capturer à l'intérieur d'un aperçu. Vous pouvez utiliser l'espacement libre pour améliorer la lisibilité.
Avant de pouvoir introduire notre mécanisme de comptage, nous devons apporter une modification à notre modèle. Actuellement, l'anticipation est en dehors de la "boucle" de répétition +
. C'est bien pour l'instant parce que nous voulions juste affirmer qu'il y a un b+
Suivant notre a+
, Mais ce que nous vraiment voulons faire finalement c'est affirmer que pour chaque a
que nous faisons correspondre à l'intérieur de la "boucle", il y a un b
correspondant pour l'accompagner.
Ne nous inquiétons pas du mécanisme de comptage pour l'instant et faisons simplement le refactoring comme suit:
a+
En (?: a )+
(Notez que (?:…)
Est un groupe non capturant)a*
Avant de pouvoir "voir" le b+
, Donc modifiez le modèle en conséquenceNous avons donc maintenant les éléments suivants:
$r3 = '/ ^ (?: a (?= a* (b+) ) )+ /x';
# │ │ └──┘ │ │
# │ │ 1 │ │
# │ └───────────┘ │
# │ lookahead │
# └───────────────────┘
# non-capturing group
La sortie est la même qu'avant ( comme vu sur ideone.com ), donc il n'y a aucun changement à cet égard. L'important est que maintenant nous faisons l'assertion à chaque itération de la "boucle" +
. Avec notre modèle actuel, ce n'est pas nécessaire, mais nous allons ensuite faire en sorte que le groupe 1 "compte" pour nous en utilisant l'auto-référence.
Leçon: Vous pouvez capturer à l'intérieur d'un groupe non capturant. Les contournements peuvent être répétés.
Voici ce que nous allons faire: nous réécrirons le groupe 1 de sorte que:
+
, Lorsque le premier a
est mis en correspondance, il doit capturer b
a
correspond, il doit capturer bb
bbb
b
pour capturer dans le groupe 1, alors l'assertion échoue simplementLe groupe 1, qui est maintenant (b+)
, Devra donc être réécrit en quelque chose comme (\1 b)
. Autrement dit, nous essayons "d'ajouter" un b
à ce groupe 1 capturé dans l'itération précédente.
Il y a un léger problème ici en ce que ce modèle manque le "cas de base", c'est-à-dire le cas où il peut correspondre sans l'auto-référence. Un cas de base est requis car le groupe 1 démarre "non initialisé"; il n'a encore rien capturé (pas même une chaîne vide), donc une tentative d'auto-référence échouera toujours.
Il y a plusieurs façons de contourner cela, mais pour l'instant, faisons simplement la correspondance d'auto-référence facultatif , c'est-à-dire \1?
. Cela peut ou peut ne pas fonctionner parfaitement, mais voyons ce que cela fait, et s'il y a un problème, nous traverserons ce pont lorsque nous y arriverons. De plus, nous ajouterons d'autres cas de test pendant que nous y serons.
$tests = array(
'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb'
);
$r4 = '/ ^ (?: a (?= a* (\1? b) ) )+ /x';
# │ │ └─────┘ | │
# │ │ 1 | │
# │ └──────────────┘ │
# │ lookahead │
# └──────────────────────┘
# non-capturing group
La sortie est maintenant ( comme vu sur ideone.com ):
aaa 0
aaab 1 aaa|b # (*gasp!*)
aaaxb 0
xaaab 0
b 0
abbb 1 a|b # yes!
aabb 1 aa|bb # YES!!
aaabbbbb 1 aaa|bbb # YESS!!!
aaaaabbb 1 aaaaa|bb # NOOOOOoooooo....
A-ha! Il semble que nous soyons vraiment proches de la solution maintenant! Nous avons réussi à faire "compter" le groupe 1 en utilisant l'auto-référence! Mais attendez ... quelque chose ne va pas avec le deuxième et le dernier cas de test !! Il n'y a pas assez de b
s, et en quelque sorte ça a mal compté! Nous examinerons pourquoi cela s'est produit à l'étape suivante.
Leçon: Une façon "d'initialiser" un groupe d'auto-référencement est de rendre la correspondance d'auto-référence facultative.
Le problème est que puisque nous avons rendu la correspondance d'auto-référence facultative, le "compteur" peut "réinitialiser" à 0 lorsqu'il n'y a pas assez de b
. Examinons de près ce qui se passe à chaque itération de notre modèle avec aaaaabbb
en entrée.
a a a a a b b b
↑
# Initial state: Group 1 is "uninitialized".
_
a a a a a b b b
↑
# 1st iteration: Group 1 couldn't match \1 since it was "uninitialized",
# so it matched and captured just b
___
a a a a a b b b
↑
# 2nd iteration: Group 1 matched \1b and captured bb
_____
a a a a a b b b
↑
# 3rd iteration: Group 1 matched \1b and captured bbb
_
a a a a a b b b
↑
# 4th iteration: Group 1 could still match \1, but not \1b,
# (!!!) so it matched and captured just b
___
a a a a a b b b
↑
# 5th iteration: Group 1 matched \1b and captured bb
#
# No more a, + "loop" terminates
A-ha! Lors de notre 4ème itération, nous pouvions toujours faire correspondre \1
, Mais nous ne pouvions pas faire correspondre \1b
! Puisque nous permettons à la correspondance d'auto-référence d'être facultative avec \1?
, Le moteur revient en arrière et prend l'option "non merci", ce qui nous permet alors de faire correspondre et de capturer juste b
!
Notez cependant que, sauf lors de la toute première itération, vous pouvez toujours faire correspondre uniquement l'auto-référence \1
. Ceci est évident, bien sûr, car c'est ce que nous venons de capturer lors de notre précédente itération, et dans notre configuration, nous pouvons toujours le faire correspondre à nouveau (par exemple, si nous avons capturé bbb
la dernière fois, nous avons la garantie qu'il y aura toujours être bbb
, mais il peut y avoir ou non bbbb
cette fois).
Leçon: Méfiez-vous des retours en arrière. Le moteur d'expression régulière fera autant de retours en arrière que vous le permettez jusqu'à ce que le motif donné corresponde. Cela peut affecter les performances (c'est-à-dire retour en arrière catastrophique ) et/ou l'exactitude.
La "correction" devrait maintenant être évidente: combinez la répétition facultative avec le quantificateur possessif . Autrement dit, au lieu de simplement ?
, Utilisez plutôt ?+
(Rappelez-vous qu'une répétition qui est quantifiée comme possessive ne revient pas en arrière, même si une telle "coopération" peut entraîner une correspondance avec le modèle global ).
En termes très informels, voici ce que ?+
, ?
Et ??
Disent:
?+
- (facultatif) "Il n'a pas besoin d'être là",
- (possessif) "mais s'il est là, vous devez le prendre et ne pas le lâcher!"
?
- (facultatif) "Il n'a pas besoin d'être là",
- (gourmand) "mais si c'est le cas, vous pouvez le prendre pour l'instant",
- (retour en arrière) "mais on vous demandera peut-être de laisser tomber plus tard!"
??
- (facultatif) "Il n'a pas besoin d'être là",
- (réticent) "et même si c'est le cas, vous n'êtes pas obligé de le prendre tout de suite",
- (retour en arrière) "mais on vous demandera peut-être de le reprendre plus tard!"
Dans notre configuration, \1
Ne sera pas là la toute première fois, mais il sera toujours là à tout moment après cela, et nous toujours = voulez le faire correspondre alors. Ainsi, \1?+
Accomplirait exactement ce que nous voulons.
$r5 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ /x';
# │ │ └──────┘ │ │
# │ │ 1 │ │
# │ └───────────────┘ │
# │ lookahead │
# └───────────────────────┘
# non-capturing group
Maintenant, la sortie est ( comme vu sur ideone.com ):
aaa 0
aaab 1 a|b # Yay! Fixed!
aaaxb 0
xaaab 0
b 0
abbb 1 a|b
aabb 1 aa|bb
aaabbbbb 1 aaa|bbb
aaaaabbb 1 aaa|bbb # Hurrahh!!!
Voilà !!! Problème résolu!!! Nous comptons maintenant correctement, exactement comme nous le voulons!
Leçon: Apprenez la différence entre les répétitions gourmandes, réticentes et possessives. La possession facultative peut être une combinaison puissante.
Donc, ce que nous avons en ce moment est un modèle qui correspond à a
à plusieurs reprises, et pour chaque a
qui a été trouvé, il y a un b
correspondant capturé dans le groupe 1. Le +
Se termine lorsqu'il n'y a plus de a
, ou si l'assertion a échoué car il n'y a pas de b
correspondant pour un a
.
Pour terminer le travail, nous devons simplement ajouter à notre modèle \1 $
. Ceci est maintenant une référence arrière à ce que le groupe 1 correspondait, suivi de la fin de l'ancre de ligne. L'ancre garantit qu'il n'y a pas de b
supplémentaires dans la chaîne; en d'autres termes, que nous avons en fait anbn.
Voici le modèle finalisé, avec des cas de test supplémentaires, dont un de 10 000 caractères:
$tests = array(
'aaa', 'aaab', 'aaaxb', 'xaaab', 'b', 'abbb', 'aabb', 'aaabbbbb', 'aaaaabbb',
'', 'ab', 'abb', 'aab', 'aaaabb', 'aaabbb', 'bbbaaa', 'ababab', 'abc',
str_repeat('a', 5000).str_repeat('b', 5000)
);
$r6 = '/ ^ (?: a (?= a* (\1?+ b) ) )+ \1 $ /x';
# │ │ └──────┘ │ │
# │ │ 1 │ │
# │ └───────────────┘ │
# │ lookahead │
# └───────────────────────┘
# non-capturing group
Il trouve 4 correspondances: ab
, aabb
, aaabbb
et le a5000b5000. Il faut seulement 0,06 s pour fonctionner sur ideone.com .
Donc, le modèle fonctionne en PHP, mais le but ultime est d'écrire un modèle qui fonctionne en Java.
public static void main(String[] args) {
String aNbN = "(?x) (?: a (?= a* (\\1?+ b)) )+ \\1";
String[] tests = {
"", // false
"ab", // true
"abb", // false
"aab", // false
"aabb", // true
"abab", // false
"abc", // false
repeat('a', 5000) + repeat('b', 4999), // false
repeat('a', 5000) + repeat('b', 5000), // true
repeat('a', 5000) + repeat('b', 5001), // false
};
for (String test : tests) {
System.out.printf("[%s]%n %s%n%n", test, test.matches(aNbN));
}
}
static String repeat(char ch, int n) {
return new String(new char[n]).replace('\0', ch);
}
Le modèle fonctionne comme prévu ( comme vu sur ideone.com ).
Il faut dire que le a*
Dans l'antichambre, et en fait la "boucle principale +
", Permettent tous les deux de revenir en arrière. Les lecteurs sont encouragés à confirmer pourquoi ce n'est pas un problème en termes d'exactitude, et pourquoi en même temps rendre les deux possessifs fonctionnerait également (bien que peut-être mélanger les quantificateurs possessifs obligatoires et non obligatoires dans le même schéma puisse conduire à des perceptions erronées).
Il convient également de dire que, même s'il est soigné, il existe un modèle d'expression régulière qui correspondra à anbn, ce n'est pas toujours la "meilleure" solution en pratique. Une bien meilleure solution consiste simplement à faire correspondre ^(a+)(b+)$
, puis à comparer la longueur des chaînes capturées par les groupes 1 et 2 dans le langage de programmation d'hébergement.
En PHP, cela peut ressembler à ceci ( comme vu dans ideone.com ):
function is_anbn($s) {
return (preg_match('/^(a+)(b+)$/', $s, $groups)) &&
(strlen($groups[1]) == strlen($groups[2]));
}
Le but de cet article est [~ # ~] pas [~ # ~] pour convaincre les lecteurs que l'expression régulière peut faire presque n'importe quoi; elle ne peut clairement pas, et même pour ce qu'elle peut faire, une délégation au moins partielle à la langue d'hébergement devrait être envisagée si elle conduit à une solution plus simple.
Comme mentionné en haut, bien que cet article soit nécessairement étiqueté [regex]
Pour stackoverflow, c'est peut-être plus que cela. Bien qu'il soit certainement utile d'apprendre à connaître les assertions, les références imbriquées, les quantificateurs possessifs, etc., la leçon la plus importante ici est peut-être le processus créatif par lequel on peut essayer de résoudre des problèmes, la détermination et le travail acharné que cela nécessite souvent lorsque vous êtes soumis à diverses contraintes, la composition systématique de différentes parties pour construire une solution de travail, etc.
Depuis que nous avons mis en place PHP, il faut dire que PCRE prend en charge les modèles récursifs et les sous-programmes. Ainsi, le modèle suivant fonctionne pour preg_match
( comme vu sur ideone.com ):
$rRecursive = '/ ^ (a (?1)? b) $ /x';
Actuellement, l'expression régulière de Java ne prend pas en charge les modèles récursifs.
Nous avons donc vu comment faire correspondre anbn qui n'est pas régulier, mais toujours sans contexte, mais peut-on aussi faire correspondre anbncn, qui n'est même pas sans contexte?
La réponse est, bien sûr, OUI ! Les lecteurs sont encouragés à essayer de résoudre ce problème par eux-mêmes, mais la solution est fournie ci-dessous (avec implémentation dans Java sur ideone.com ).
^ (?: a (?= a* (\1?+ b) b* (\2?+ c) ) )+ \1 \2 $
Étant donné qu'aucune mention n'a été faite de PCRE prenant en charge les modèles récursifs, je voudrais simplement citer l'exemple le plus simple et le plus efficace de PCRE qui décrit le langage en question:
/^(a(?1)?b)$/
Comme mentionné dans la question - avec le groupe d'équilibrage .NET, les modèles de type anbncnrén… Zn peut être apparié facilement car
^
(?<A>a)+
(?<B-A>b)+ (?(A)(?!))
(?<C-B>c)+ (?(B)(?!))
...
(?<Z-Y>z)+ (?(Y)(?!))
$
Par exemple: http://www.ideone.com/usuOE
Modifier:
Il existe également un modèle PCRE pour le langage généralisé avec modèle récursif, mais une anticipation est nécessaire. Je ne pense pas que ce soit une traduction directe de ce qui précède.
^
(?=(a(?-1)?b)) a+
(?=(b(?-1)?c)) b+
...
(?=(x(?-1)?y)) x+
(y(?-1)?z)
$
Par exemple: http://www.ideone.com/9gUwF