web-dev-qa-db-fra.com

Comment pouvons-nous faire correspondre un ^ n b ^ n avec Java regex?

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 = { anbn: 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.?

Références

Questions liées

95
polygenelubricants

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.


Étape 1: Lookahead pour l'assertion

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.


Étape 2: Capture à l'aide d'une tête de lecture (et en mode avant - mode p a c i n g)

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é.


Étape 3: Refactorisation de l'anticipation dans la "boucle"

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:

  • Premier refactoriseur a+ En (?: a )+ (Notez que (?:…) Est un groupe non capturant)
  • Déplacez ensuite l'antichambre à l'intérieur de ce groupe qui ne capture pas
    • Notez que nous devons maintenant "sauter" a* Avant de pouvoir "voir" le b+, Donc modifiez le modèle en conséquence

Nous 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.


Étape 4: C'est l'étape où nous commençons à compter

Voici ce que nous allons faire: nous réécrirons le groupe 1 de sorte que:

  • À la fin de la première itération du +, Lorsque le premier a est mis en correspondance, il doit capturer b
  • À la fin de la deuxième itération, lorsqu'un autre a correspond, il doit capturer bb
  • À la fin de la troisième itération, il devrait capturer bbb
  • ...
  • À la fin de l'itération n - e, le groupe 1 doit capturer bn
  • S'il n'y a pas assez de b pour capturer dans le groupe 1, alors l'assertion échoue simplement

Le 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 bs, 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.


Étape 4½: Comprendre ce qui n'a pas fonctionné

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.


Étape 5: La possession de soi à la rescousse!

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.


Étape 6: touches finales

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 .


Étape 7: Le test Java

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 ).


Et maintenant, nous arrivons à la conclusion ...

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.


Matériel bonus! Motif récursif PCRE!

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.


Encore plus de bonus! Correspondance anbncn !!

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 $

136
polygenelubricants

É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)$/
20
jaytea

Comme mentionné dans la question - avec le groupe d'équilibrage .NET, les modèles de type anbncnn… 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

11
kennytm