59 votes

correspondance d'expressions rationnelles "verticales" dans une "image" ASCII

Note : Il s'agit d'une question sur les possibilités des saveurs modernes de regex. Il ne s'agit pas de la meilleure façon de résoudre ce problème en utilisant d'autres méthodes. C'est inspiré par une question précédente mais celle-ci n'est pas limitée aux regex.

Le problème

Dans un ASCII "image"/art/map/string comme :

....X.......
..X..X...X....
X.X...X..X.....
X....XXXXXX.....
X..XXX...........
.....X..........
..............X
..X...........X....
..X...........X....X...
....X.....

J'aimerais trouver une simple formation de ligne verticale de trois X s :

X
X
X

Le nombre de lignes est variable dans l'image, et la largeur de l'image est variable. chaque est également variable.

La ou les questions

Avec regex (PCRE/PHP, Perl, .NET ou similaire) est-il possible de :

  1. Déterminer si une telle formation existe
  2. Comptez le nombre de formations de ce type / faites correspondre le point de départ de chacune d'entre elles (4 dans l'exemple ci-dessus).

0 votes

Quelqu'un peut-il utiliser PHP (fonctions internes) pour calculer N lignes, et la longueur de chacune ? Il existe peut-être un moyen de générer "automatiquement" une regex comme je l'ai fait. ici sous la rubrique "briser les lois de l'expression géographique" ?

1 votes

@HamZa, cela devrait être à propos d'une seule expression régulière générale qui est indépendante de l'entrée. De telles astuces seraient donc de la triche ;-)

1 votes

Sans modifier l'entrée (rotation par exemple) et sans générer "automatiquement" des solutions, je dirais que bonne chance :-)

41voto

Qtax Points 20487

Réponse à la question 1

Pour répondre à la première question, on pourrait utiliser :

(?xm)                    # ignore comments and whitespace, ^ matches beginning of line
^                        # beginning of line
(?:
    .                    # any character except \n
    (?=                  # lookahead
        .*+\n            # go to next line
        ( \1?+ . )       # add a character to the 1st capturing group
        .*+\n            # next line
        ( \2?+ . )       # add a character to the 2nd capturing group
    )
)*?                      # repeat as few times as needed
X .*+\n                  # X on the first line and advance to next line
\1?+                     # if 1st capturing group is defined, use it, consuming exactly the same number of characters as on the first line
X .*+\n                  # X on the 2nd line and advance to next line
\2?+                     # if 2st capturing group is defined, use it, consuming exactly the same number of characters as on the first line
X                        # X on the 3rd line

<a href="http://ideone.com/iUFxBw" rel="nofollow noreferrer"><strong>Online demo</strong></a>

Cette expression fonctionne en Perl, PCRE, Java et devrait fonctionner en .NET.

L'expression utilise des lookaheads avec des groupes de capture auto-référents pour ajouter un caractère à chaque répétition du lookahead (ceci est utilisé pour "compter").

\1?+ signifie que si \1 correspond (ou est défini), consommez-le, et ne le rendez pas (ne revenez pas en arrière). Dans ce cas, c'est équivalent à (?(1) \1 ) . Ce qui signifie que la correspondance \1 si \1 est défini.

polygenelubricants explique très bien ce type de lookaheads avec références arrière dans sa réponse pour Comment faire correspondre a^n b^n avec un regex Java ? . (Il a également écrit sur d'autres astuces impressionnantes pour les regex Java impliquant des références arrière et des lookarounds).

Réponse à la question 2

Appariement uni

Si l'on utilise uniquement la correspondance et que l'on demande que la réponse (le nombre) corresponde au nombre de correspondances, la réponse à la question 2 serait la suivante :

Il peut pas être résolu directement dans les saveurs regex qui ont un lookbehind limité. Alors que d'autres saveurs comme Java et .NET pourraient (comme par exemple dans La solution .NET de m.buettner ).

Ainsi, les correspondances regex simples en Perl et PCRE (PHP, etc.) ne peuvent pas répondre directement à cette question dans ce cas.

(Semi ?)preuve

Supposons qu'il n'existe pas de balises de longueur variable.

Vous devez d'une manière ou d'une autre compter le nombre de caractères sur une ligne avant un X .
Le seul moyen d'y parvenir est de les faire correspondre, et puisqu'il n'existe pas de lookbehinds de longueur variable, vous devez commencer la correspondance (au moins) au début de la ligne.
Si vous commencez la correspondance au début d'une ligne, vous ne pouvez obtenir au maximum qu'une correspondance par ligne.

Comme il peut y avoir plusieurs occurrences par ligne, cela ne les compterait pas toutes et ne donnerait pas une réponse correcte.

Longueur/solution indirecte

D'autre part, si nous acceptons la réponse comme étant la longueur d'un résultat de correspondance ou de substitution, alors la 2ème question peut être répondu dans PCRE et Perl (et d'autres saveurs).

Cette solution est basée sur/inspirée par La sympathique "solution PCRE partielle" de M. Buettner .

On pourrait simplement remplacer toutes les correspondances de l'expression suivante par $3 en obtenant la réponse à la deuxième question (le nombre de motifs d'intérêt) comme étant la longueur de la chaîne résultante.

^
(?:
    (?:                   # match .+? characters
        .
        (?=               # counting the same number on the following two lines
            .*+\n
            ( \1?+ . )
            .*+\n
            ( \2?+ . )
        )
    )+?
    (?<= X )              # till the above consumes an X
    (?=                   # that matches the following conditions
        .*+\n
        \1?+
        (?<= X )
        .*+\n
        \2?+
        (?<= X )
    )
    (?=                   # count the number of matches
        .*+\n
        ( \3?+ . )        # the number of matches = length of $3
    )
)*                        # repeat as long as there are matches on this line
.*\n?                     # remove the rest of the line

Ce qui, en Perl, pourrait être écrit comme suit :

$in =~ s/regex/$3/gmx;
$count = length $in;

<a href="http://ideone.com/FLfOLX" rel="nofollow noreferrer"><strong>Online demo</strong></a>

Cette expression est similaire à la solution à la question 1 ci-dessus, avec quelques modifications pour inclure X dans les caractères trouvés dans le premier lookahead, enveloppé d'un quantificateur et comptant le nombre de correspondances du quantificateur.

À l'exception des correspondances directes, cette solution est aussi proche que possible (en termes de code supplémentaire, en dehors de l'expression rationnelle), et pourrait constituer une réponse acceptable à la question 2.

Cas de test

Quelques cas de test et résultats pour la solution ci-dessus. Résultat montrant la réponse numérique (longueur de la chaîne résultante) et entre parenthèses la chaîne résultante après la ou les substitutions.

Test #0:
--------------------
X
X
X

result: 1 (X)

Test #1:
--------------------
..X....
..X....
..X....

result: 1 (.)

Test #2:
--------------------
..X.X..
..X.X..
....X..

result: 1 (.)

Test #3:
--------------------
..X....
..X....
...X...

result: 0 ()

Test #4:
--------------------
..X....
...X...
..X....

result: 0 ()

Test #5:
--------------------
....X..
.X..X..
.X.....

result: 0 ()

Test #6:
--------------------
.X..X..
.X.X...
.X.X...

result: 1 (.)

Test #7:
--------------------
.X..X..
.X..X..
.X..X..

result: 2 (.X)

Test #8:
--------------------
XXX
XXX
XXX

result: 3 (XXX)

Test #9:
--------------------
X.X.X
XXXXX
XXXXX
.X.X.

result: 5 (XXXXX)

Test #10:
--------------------
1....X.......
2..X..X...X....
3X.X...X..X.....
4X....XXXXXX.....
5X..XXX...........
6.....X..........
7.........X....X
8..X......X....X....
9..X......X....X....X...
A....X.....
B.X..X..
C.....
XXX
XXX
XXX
.

result: 8 (3458.XXX)

3 votes

+1 ... groupes d'auto-référencement possessifs-optionnels... j'ai appris quelque chose de nouveau aujourd'hui ! :) ... c'est vraiment une belle magie de regex là ;) ... merci aussi pour le lien vers cette série de polygenelubricants - je ne l'avais jamais vue auparavant, et je sais maintenant que Java supporte accidentellement les lookbehinds de longueur non limitée.

0 votes

Êtes-vous intéressé par des solutions partielles de PCRE pour Q2 ? J'ai une solution qui vous donne le compte correct (en surcomptant les chevauchements cependant). Cependant elle ne le fait pas sous la forme de n des allumettes. Au lieu de cela, vous obtenez une correspondance pour chaque ligne qui contient le début d'au moins une colonne, puis il y a une capture dont la longueur indique combien de colonnes commencent dans cette ligne. Normalement, cette capture devrait toujours contenir un caractère, mais si vous avez deux colonnes dans une ligne, la capture de la correspondance de cette ligne contiendra 2 caractères et ainsi de suite. Si vous êtes intéressé, je le posterai demain.

0 votes

@m.buettner, Si vous avez trouvé un moyen d'indiquer (de quelque manière que ce soit) le nombre de correspondances sur chaque ligne (avec PCRE), j'aimerais le voir :-)

29voto

amon Points 42005

Modifier

Les solutions suivantes présentent deux graves problèmes :

  1. Ils ne peuvent pas correspondre à plusieurs XXX qui commencent sur la même ligne, comme les pos avance trop.
  2. La deuxième solution est incorrecte : elle fait correspondre des lignes consécutives où deux X sont au-dessus les uns des autres. Il n'est pas nécessaire qu'il y en ait trois à la suite.

Par conséquent, tous les votes positifs (et la prime) devraient aller à l'un ou l'autre des deux groupes suivants m.buettner 's réponse complète sur .NET ou le réponse fascinante au PCRE de Qtaxe lui-même.


Réponse originale

Il s'agit d'une réponse utilisant l'intégration du code Perl dans les regex. Parce qu'une regex Perl peut utiliser du code pour affirmer des conditions arbitraires à l'intérieur des regex ou émettre des regex partielles, elles ne sont pas limitées à la correspondance des langages réguliers ou des langages sans contexte, mais peuvent correspondre à certaines parties de langages plus élevés dans la hiérarchie de Chomsky.

La langue que vous souhaitez faire correspondre peut être décrite en termes de regex comme suit

^ .{n} X .*\n
  .{n} X .*\n
  .{n} X

n est un nombre. C'est à peu près aussi complexe que de faire correspondre le a n b n c n qui est l'exemple canonique d'un langage sensible au contexte.

Nous pouvons facilement faire correspondre la première ligne, et utiliser du code Perl pour émettre la regex pour les autres lignes :

    /^ (.*?) X
       (?: .*\n (??{"." x length($1)}) X){2}
    /mx

C'était court ! Qu'est-ce que ça fait ?

  • ^ (.*?) X au début d'une ligne, fait correspondre le moins possible de caractères qui ne sont pas des nouvelles lignes, puis la fonction X . Nous nous souvenons de la file d'attente jusqu'au X comme groupe de capture $1 .

  • Nous répétons deux fois un groupe qui correspond au reste de la ligne, à une nouvelle ligne, puis nous injectons une regex qui correspond à une chaîne de la même longueur que $1 . Après cela, il doit y avoir un X .

Cette regex va maintenant correspondre à toutes les chaînes de caractères qui ont 3 X l'un sur l'autre.

Si nous voulons extraire tous de telles séquences, nous devrons être astucieux. Parce que les séquences peuvent se chevaucher, par ex.

.X
XX
XX
X.

la position où commence le match suivant ne doit pas dépasser la première position. X . Nous pouvons le faire via un lookbehind et un lookahead. Perl ne supporte que les lookbehind de longueur constante, mais dispose de la fonction \K qui fournit une sémantique similaire. Ainsi,

/^ (.*?) \K X
   (?=( (?: .*\n (??{"."x length($1)}) X ){2} ))
/gmx

correspondra à toute séquence de trois verticaux X es. Temps de test :

$ perl -E'my$_=join"",<>; say "===\n$1X$2" while /^(.*?)\KX(?=((?:.*\n(??{"."x length($1)})X){2}))/gmx' <<'END'
....X.......
..X..X...X....
X.X...X..X.....
X....XXXXXX.....
X..XXX...........
.....X..........
..............X
..X...........X....
..X...........X....X...
....X.....
END
===
..X..X...X....
X.X...X..X.....
X....XXXXX
===
X.X...X..X.....
X....XXXXXX.....
X
===
X....XXXXXX.....
X..XXX...........
.....X
===
..............X
..X...........X....
..X...........X

Note : ceci repose sur des fonctionnalités expérimentales de regex qui sont disponibles au moins à partir de Perl 5, v10. Le code a été testé avec un Perl v16.


Solution sans code intégré

Examinons les lignes

...X...\n
...X..\n

Nous voulons affirmer que le leader ... La partie de chaque ligne est de même longueur. Nous pouvons le faire par récursion avec le cas de base X.*\n :

(X.*\n|.(?-1).)X

Si nous ancrons cela au début d'une ligne, nous pouvons faire correspondre deux verticales X es. Pour faire correspondre plus de deux lignes, nous devons faire la récursion dans un lookahead, puis avancer la position de correspondance à la ligne suivante, et répéter. Pour cela, nous faisons simplement correspondre .*\n .

Il en résulte la regex suivante, qui peut correspondre à une chaîne de caractères comportant trois verticales X es :

/ ^
  (?:
    (?=( X.*\n | .(?-1). ) X)
    .*\n # go to next line
  ){2}
/mx

Mais ce n'est pas suffisant, car nous voulons faire correspondre toutes ces séquences. Pour ce faire, nous plaçons essentiellement l'ensemble de la regex dans un lookahead. Le moteur de regex s'assure d'avancer la position à chaque fois pour produire une nouvelle correspondance.

/ ^
  (?=
    (
      (?:
          (?= (X.*\n | .(?-1). ) X)
          .*\n # go to next line
      ){2}
      .* # include next line in $1
    )
  )
/mx

Temps de test :

$ perl -E'my$_=join"",<>; say "===\n$1" while /^(?=((?:(?=(X.*\n|.(?-1).)X).*\n){2}.*))/gmx' <<'END'
....X.......
..X..X...X....
X.X...X..X.....
X....XXXXXX.....
X..XXX...........
.....X..........
..............X
..X...........X....
..X...........X....X...
....X.....
END
===
..X..X...X....
X.X...X..X.....
X....XXXXXX.....
===
X.X...X..X.....
X....XXXXXX.....
X..XXX...........
===
X....XXXXXX.....
X..XXX...........
.....X..........
===
..............X
..X...........X....
..X...........X....X...

Cela fonctionne donc aussi bien que la solution avec le code incorporé, c'est-à-dire qu'elle fait correspondre chaque groupe de lignes avec des lignes verticales. X es, et non pas chaque groupe de X es. (En fait, cette solution me semble plus fragile que le code embarqué)

1 votes

Si vous regardez attentivement, il y a un bug : Cela ne peut pas correspondre à plusieurs XXX cols commençant sur la même ligne. Je ne sais pas vraiment comment résoudre ce problème. Peut-être avec un pos mission ? Un lookbehind dans le lookbehind ?

0 votes

Très bien. Il faudra que je m'en souvienne quand j'arriverai au chapitre sur les regex dans la mise à jour de Mastering Perl :)

0 votes

Joli, mais je considère que c'est une tricherie car elle utilise du code (qui peut faire n'importe quoi) et non une pure regex (qui est la partie délicate) ;-)

28voto

Martin Büttner Points 26511

Tout d'abord : excellente question. Je pense qu'il peut être très instructif d'essayer d'amener les moteurs regex à leurs limites.

La solution .NET de base

Vous avez dit dans les commentaires que ce serait facile avec .NET, mais comme il n'y a pas encore de réponse à ce sujet, j'ai pensé en écrire une.

Vous pouvez résoudre les questions 1 et 2 à l'aide des groupes d'équilibrage et du lookbehind à longueur variable de .NET. La plupart du travail est effectué par les groupes d'équilibrage, mais le lookbehind à longueur variable est crucial pour pouvoir détecter les correspondances multiples commençant sur la même ligne.

Bref, voici le modèle :

(?<=                  # lookbehind counts position of X into stack
  ^(?:(?<a>).)*       # push an empty capture on the 'a' stack for each character
                      # in front of X
)                     # end of lookbehind

X                     # match X

(?=.*\n               # lookahead checks that there are two more Xs right below
  (?:(?<-a>)(?<b>).)* # while we can pop an element from stack 'a', push an
                      # element onto 'b' and consume a character
  (?(a)(?!))          # make sure that stack 'a' is empty
  X.*\n               # match X and the rest of the line
  (?:(?<-b>).)*       # while we can pop an element from stack 'b', and consume
                      # a character
  (?(b)(?!))          # make sure that stack 'b' is empty
  X                   # match a final X
)                     # end of lookahead

Ce modèle doit être utilisé avec RegexOptions.Multiline pour le ^ pour correspondre aux débuts de lignes (et évidemment avec RegexOptions.IgnorePatternWhitespace pour que le mode freespacing fonctionne).

Voici quelques commentaires supplémentaires :

En plaçant tout, sauf le X initial, dans des listes de contrôle, nous n'avons aucun problème de chevauchement des correspondances ou même de correspondances commençant sur la même ligne. Cependant, le lookbehind doit être de longueur variable, ce qui limite certainement toute solution de ce type à .NET.

Le reste repose sur une bonne maîtrise des groupes d'équilibrage. Je n'entrerai pas dans les détails ici, parce que cela rend le jeu plus difficile. des réponses assez longues en soi . (voir MSDN et cet article de blog pour plus d'informations)

Le lookbehind correspond juste ^.* donc tout jusqu'au début de la ligne, mais pour chaque . nous poussons une capture vide sur la pile a , comptant ainsi la position de notre X comme la taille de la pile.

Ensuite, après avoir consommé le reste de la ligne dans le lookahead, nous correspondons à nouveau juste .* mais avant de consommer chaque . nous sortons un élément de la pile a (ce qui conduit à l'échec, une fois a est vide) et pousser une capture vide sur b (afin de ne pas oublier le nombre de caractères qu'il doit y avoir pour la troisième ligne).

Pour s'assurer que nous vidons réellement la pile entière, nous utilisons (?(a)(?!)) . Il s'agit d'un modèle conditionnel, qui essaie de correspondre à (?!) si la pile a n'est pas vide (et est simplement ignoré sinon). Et (?!) est un lookahead négatif vide, qui échoue toujours. Par conséquent, cela encode simplement "est a non vide ? échouer. sinon, continuer".

Maintenant que nous savons que nous avons consommé exactement le bon nombre de caractères dans la nouvelle ligne, nous essayons de faire correspondre un fichier X et le reste de la ligne. Ensuite, nous répétons le même processus avec la pile b . Maintenant, il n'est pas nécessaire de pousser sur une nouvelle pile, car si cela fonctionne, nous avons terminé. Nous vérifions que b est vide après cela, et correspond à un troisième X .

Enfin, une note d'optimisation : ce modèle fonctionne toujours si toutes les répétitions sont enveloppées dans des groupes atomiques (émulant ainsi les quantificateurs possessifs, qui ne sont pas supportés par .NET) ! Cela permettrait d'économiser beaucoup de retours en arrière. En outre, si si nous plaçons au moins les quantificateurs empilables dans des groupes atomiques, nous pouvons nous débarrasser des deux éléments suivants (?(...)(?!)) (car ils ne sont nécessaires que dans les cas où la répétition précédente a dû revenir en arrière).

La solution .NET complète

(Seuls les plus courageux des aventuriers devraient me suivre dans la grotte très sombre dans laquelle je suis sur le point de descendre...)

Comme discuté dans les commentaires, cette solution a un inconvénient : elle compte les correspondances qui se chevauchent. Par exemple

..X..
..X..
..X..
..X..

Donne deux correspondances, une dans la première et une dans la deuxième ligne. Nous aimerions éviter cela, et ne signaler qu'une seule correspondance (ou deux s'il y a 6 à 8 lignes). X et trois s'il y a 9 à 11 X et ainsi de suite). De plus, nous voulons rapporter les matchs aux 1er, 4ème, 7ème, ... X .

Nous pouvons ajuster le schéma ci-dessus pour permettre cette solution en exigeant que le premier X est précédé d'un multiple entier de 3 autre X qui répondent à nos exigences. L'idée de base pour vérifier ceci utilise la même manipulation de pile que précédemment (sauf que nous déplaçons les choses entre 3 piles de sorte qu'après avoir trouvé trois X s nous nous retrouvons là où nous avons commencé). Pour ce faire, nous devons modifier un peu le lookbehind.

Il y a cependant un hic. Le lookbehind à longueur variable de .NET utilise une autre fonctionnalité unique à .NET, RightToLeftMode dans lequel le modèle est lu (et correspond) de droite à gauche. Normalement, cela ne doit pas nous déranger, mais lorsque nous combinons cela avec les groupes d'équilibrage, nous pourrions être dans une situation où nous aurions besoin d'une aide supplémentaire. pour quelques surprises désagréables . En particulier, lorsque nous examinons l'évolution de nos piles de capture, nous devons également construire (et lire) l'expression de droite à gauche (ou de bas en haut).

Ainsi, lorsque vous lisez l'expression suivante (et mes annotations), commencez à la fin du lookbehind le plus externe (vous devrez faire défiler un peu) - c'est-à-dire juste avant le seul élément de niveau supérieur X puis lire tout le chemin jusqu'en haut. Et puis continuez après le retour en arrière.

(?<=                  
  # note that the lookbehind below does NOT affect the state of stack 'a'!
  # in fact, negative lookarounds can never change any capturing state.
  # this is because they have to fail for the engine to continue matching.
  # and if they fail, the engine needs to backtrack out of them, in which
  # case the previous capturing state will be restored.
  (?<!                # if we get here, there is another X on top of the last
                      # one in the loop, and the pattern fails
    ^                 # make sure we reached the beginning of the line
    (?(a)(?!))        # make sure that stack 'a' is empty
    (?:(?<-a>).)*     # while we can pop an element from stack 'a', and consume
                      # a character
    X.*\n             # consume the next line and a potential X
  )
  # at this point we know that there are less than 3 Xs in the same column
  # above this position. but there might still be one or two more. these
  # are the cases we now have to eliminate, and we use a nested negative
  # lookbehind for this. the lookbehind simply checks the next row and
  # asserts that there is no further X in the same column.
  # this, together with the loop, below means that the X we are going to match
  # is either the topmost in its column or preceded by an integer multiple of 3
  # Xs - exactly what we are looking for.
  (?:

    # at this point we've advanced the lookbehind's "cursor" by exactly 3 Xs
    # in the same column, AND we've restored the same amount of captures on
    # stack 'a', so we're left in exactly the same state as before and can
    # potentially match another 3 Xs upwards this way.
    # the fact that stack 'a' is unaffected by a full iteration of this loop is
    # also crucial for the later (lookahead) part to work regardless of the
    # amount of Xs we've looked at here.

    ^                 # make sure we reached the beginning of the line
    (?(c)(?!))        # make sure that stack 'a' is empty
    (?:(?<-c>)(?<a>).)* # while we can pop an element from stack 'c', push an
                      # element onto 'a' and consume a character
    X.*\n             # consume the next line and a potential X
    (?(b)(?!))        # make sure that stack 'b' is empty
    (?:(?<-b>)(?<c>).)* # while we can pop an element from stack 'b', push an
                      # element onto 'c' and consume a character
    X.*\n             # consume the next line and a potential X
    (?(a)(?!))        # make sure that stack 'a' is empty
    (?:(?<-a>)(?<b>).)* # while we can pop an element from stack 'a', push an
                      # element onto 'b' and consume a character
    X.*\n             # consume the next line and a potential X
  )*                  # this non-capturing group will match exactly 3 leading
                      # Xs in the same column. we repeat this group 0 or more
                      # times to match an integer-multiple of 3 occurrences.
  ^                   # make sure we reached the beginning of the line
  (?:(?<a>).)*        # push an empty capture on the 'a' stack for each
                      # character in front of X
)                     # end of lookbehind (or rather beginning)

# the rest is the same as before    

X                     # match X
(?=.*\n               # lookahead checks that there are two more Xs right below
  (?:(?<-a>)(?<b>).)* # while we can pop an element from stack 'a', push an
                      # element onto 'b' and consume a character
  (?(a)(?!))          # make sure that stack 'a' is empty
  X.*\n               # match X and the rest of the line
  (?:(?<-b>).)*       # while we can pop an element from stack 'b', and consume
                      # a character
  (?(b)(?!))          # make sure that stack 'b' is empty
  X                   # match a final X
)                     # end of lookahead

Démonstration de travail sur RegexHero.net.

J'ai intercalé toutes les explications correctes avec le motif cette fois. Ainsi, si vous lisez le modèle de la manière que j'ai recommandée ci-dessus, vous obtiendrez l'explication au moment où vous en aurez besoin...

C'était une sacrée bête. Mais il satisfait maintenant à l'ensemble des spécifications et montre à quel point la saveur regex de .NET est puissante. Et, bien que cela semble assez horrible, je pense (une fois que vous avez réalisé le truc de droite à gauche) que c'est beaucoup plus facile à comprendre qu'une solution comparable avec PCRE (utilisant la récursion ou autre).

Comme Kobi l'a mentionné dans un commentaire ci-dessous, cela pourrait être raccourci de beaucoup, si vous acceptez que vos résultats soient trouvés dans des captures multiples d'une seule correspondance (par exemple, si vous avez une colonne de 7 X s vous n'obtenez qu'un seul match, mais avec 2 captures dans un certain groupe). Vous pouvez le faire en répétant la partie principale (lookahead) une ou plusieurs fois et en capturant l'élément initial X (tout mettre dans un lookahead cependant). Ainsi, le lookbehind n'a pas besoin de compter les triples de X mais il doit seulement vérifier qu'il n'y a pas de première ligne X . Cela permettrait probablement de réduire de moitié la taille du motif.

La solution partielle de la PCRE

(Si seuls les plus courageux des aventuriers m'ont suivi dans la dernière solution, il ne me reste probablement que des fous pour le prochain voyage...)

Pour prouver ce que je viens de dire à propos de la comparaison entre la solution ci-dessus et le PCRE, voyons comment nous pouvons, même de loin, résoudre le problème complet en PCRE. Nous devrons travailler un peu plus dur sans les lookbehinds à longueur variable et les groupes d'équilibrage.

Qtax (l'OP) a fourni une solution brillante à sa première question (vérifier si la chaîne de caractères contient un quelconque X -colonne) en utilisant des groupes auto-référents pour compter. Il s'agit d'une solution très élégante et compacte. Mais comme chaque correspondance va du début de la ligne jusqu'à la colonne X qui commence la colonne, et les correspondances ne peuvent pas se chevaucher, nous ne pouvons pas obtenir plusieurs correspondances par ligne. Nous pourrions essayer de tout mettre dans un lookahead (de sorte que rien n'est réellement mis en correspondance), mais deux correspondances de largeur nulle ne commenceront jamais à la même position - nous n'obtiendrons donc toujours qu'une seule correspondance par ligne candidate.

Cependant, il est possible de résoudre au moins la première partie de la question 2 avec PCRE : compter le nombre de colonnes commençant dans chaque ligne (et donc le nombre total de lignes). X colonnes). Puisque nous ne pouvons pas obtenir ce compte sous la forme de correspondances individuelles (voir le paragraphe précédent), et nous ne pouvons pas obtenir ce compte sous la forme de groupes individuels ou de captures (puisque PCRE ne fournit qu'un nombre fixe et fini de captures, contrairement à .NET). Ce que nous pouvons faire à la place est de coder le nombre de colonnes dans les correspondances.

Voici comment : pour chaque ligne, nous vérifions si une colonne commence. Si c'est le cas, nous incluons un caractère dans un certain groupe de capture. Ensuite, avant de signaler une correspondance réussie, nous essayons de trouver autant de colonnes supplémentaires que possible - pour chacune d'entre elles, nous ajoutons un caractère à ce groupe particulier. Ce faisant, nous codons le nombre de colonnes commençant dans chaque ligne dans la longueur de cette capture particulière.

En fait, la réalisation de ce concept dans une regex est beaucoup plus compliquée qu'il n'y paraît à première vue (et cela semble déjà assez compliqué). Quoi qu'il en soit, voici ce qu'il en est :

^                        
(?:(?|
  (?(5)(?![\s\S]*+\5))      
  (?!(?!)()()) 
  (?=
    (?:
      .                  
      (?=                
        .*+\n            
        ( \3? . )   
        .*+\n        
        ( \4? . )    
      )
    )*?              
    X .*+\n          
    \3               
    X .*+\n          
    \4               
  )
  ()
|
  (?(5)(?=[\s\S]*+\5)|(?!))
  (?:
    .
    (?=
      .*+\n
      ( \1? .)
      .*+\n
      ( \2? .)
    )
  )+?
  (?=
    (?<=X).*+\n
    (\1)         
    (?<=X).*+\n
    (\2)         
    (?<=X)     
  )
  (?=
    ([\s\S])   
    [\s\S]*
    ([\s\S] (?(6)\6))
  )
){2})+

(En fait, c'est un peu plus simple que cela - voir la réponse de Qtax pour savoir comment simplifier cette approche. Je vais quand même laisser cette approche ici pour des raisons académiques, car certaines techniques très avancées et intéressantes peuvent être apprises à partir d'elle - voir le résumé à la fin).

Oui, il n'y a pas d'annotations. Je me suis dit que personne ne les lirait de toute façon, alors je vais plutôt essayer de décomposer cette expression en plusieurs parties (je vais opter pour une approche descendante).

Alors regardons la couche extérieure de l'oignon de l'enfer :

^                        
(?:(?|
  checkForNextColumn
|
  countAndAdvance
){2})+

Nos correspondances sont donc à nouveau ancrées sur les débuts de lignes. Ensuite, nous avons un (?:...{2})+ ce qui signifie un nombre pair de répétitions de quelque chose. Et ce quelque chose est une alternance de deux sous-modèles. Ces sous-modèles représentent les étapes que j'ai mentionnées ci-dessus. Le premier vérifie qu'il y a une autre colonne qui commence dans la ligne actuelle, le second enregistre un compte et prépare l'état du moteur pour une autre application du premier sous-modèle. Le contrôle est donc donné au second motif - le premier vérifie simplement la présence d'une autre colonne en utilisant un lookahead et est donc un motif de largeur nulle. C'est pourquoi je ne peux pas simplement tout emballer dans + mais doivent faire le {2})+ sinon le composant de largeur zéro ne serait essayé qu'une seule fois ; c'est une optimisation nécessaire appliquée par presque tous les moteurs pour éviter les boucles infinies avec des motifs comme (a*)+ .

Il y a encore un détail (très important) : J'ai utilisé (?|...) pour l'alternance. Dans ce type de regroupement, chaque alternative commence par le même numéro de groupe. Ainsi, dans /(?|(a)|(b))/ les deux a et b peuvent être capturés dans le groupe 1 . C'est l'astuce cruciale qui permet la "communication" entre les sous-modèles, puisqu'ils peuvent modifier les mêmes groupes.

Bref... nous avons donc ces deux sous-modèles. Nous aimerions nous assurer que le contrôle alterne vraiment entre eux. Pour que chaque groupe échoue si c'est le dernier qui correspond. Nous faisons cela en enveloppant le motif dans une magie de regroupement et de référencement :

^(?:(?|
  (?(5)(?![\s\S]*+\5))       # if group 5 has matched before make sure that
                             # it didn't match empty
  checkForNextColumn         # contains 4 capturing groups
  ()                         # this is group 5, match empty
|
  (?(5)(?=[\s\S]*+\5)|(?!))  # make sure that group 5 is defined and that it
                             # matched empty
  advanceEngineState         # contains 4 capturing groups
  (?=
    ([\s\S])                 # this is group 5, match non-empty
    [\s\S]*                  # advance to the end very end of the string
    ([\s\S] (?(6)\6))             # add a character from the end of the string to
                             # group 6
  )
){2})+

Ainsi, à la fin de chaque alternative, nous invaliderons la condition pour que cette alternative puisse même commencer à correspondre. À la fin de la deuxième alternative, nous inclurons également un caractère dans le groupe 6 en utilisant la technique décrite par Qtax. C'est l'étape du comptage. C'est-à-dire que le groupe 6 contiendra autant de caractères qu'il y a de colonnes commençant dans la ligne actuelle.

Maintenant checkForNextColumn ne sera en fait que la solution de Qtax dans un lookahead. Elle a cependant besoin d'une modification supplémentaire et pour la justifier, nous allons examiner les points suivants advanceEngineState d'abord.

Réfléchissons à la façon dont nous voudrions modifier l'état, pour que la solution de Qtax corresponde à une deuxième colonne dans une rangée. Disons que nous avons l'entrée

..X..X..
..X..X..
..X..X..

et nous voulons trouver la deuxième colonne. Cela peut être fait en commençant la recherche à partir de la position juste après la première colonne. X et avoir des groupes \1 et \2 déjà initialisé aux trois premiers caractères ( ..X ) des rangées 2 et 3, respectivement (au lieu qu'elles soient vides).

Maintenant, essayons de faire ça : faire correspondre tout jusqu'à et y compris le prochain X qui commence une colonne, puis remplir deux groupes avec les préfixes de ligne correspondants pour les utiliser dans la fonction checkForNextColumn modèle. C'est à nouveau à peu près le modèle de Qtax, sauf que nous comptons les X (au lieu de s'arrêter juste avant), et que nous devons ajouter les captures dans un groupe séparé. Voici donc advanceEngineState :

(?:
  .
  (?=
    .*+\n
    ( \1? .)
    .*+\n
    ( \2? .)
  )
)+?
(?=
  (?<=X) .*+\n
  (\1)        
  (?<=X) .*+\n
  (\2)        
  (?<=X)
)

Notez comment j'ai tourné le X en lookbehinds, pour aller un caractère plus loin, et comment je copie efficacement le contenu final de \1 en \3 et ceux de \2 en \4 .

Donc si nous utilisons maintenant la solution de Qtax comme checkForNextColumn dans un lookahead, en utilisant des groupes \3 et \4 au lieu de \1 et \2 nous devrions avoir terminé.

Mais comment faire pour que ces groupes \3 et \4 au lieu de \1 et \2 ? Nous pourrions commencer le modèle par ()() ce qui correspondrait toujours, sans affecter le curseur du moteur, mais augmenterait le nombre de groupes de 2. Cependant, cela pose un problème : cela remet les groupes à zéro. 1 et 2 à des chaînes vides, ce qui si nous trouvons une deuxième colonne, advanceEngineState sera dans un état incohérent (car la position globale du moteur a été avancée, mais les groupes de comptage sont à nouveau à zéro). Nous voulons donc faire entrer ces deux groupes dans le modèle, mais sans affecter ce qu'ils capturent actuellement. Nous pouvons le faire en utilisant quelque chose que j'ai déjà mentionné avec les solutions .NET : les groupes dans les lookarounds négatifs n'affectent pas le contenu capturé (parce que le moteur doit revenir en arrière hors du lookaround pour continuer). Nous pouvons donc utiliser (?!(?!)()()) (un lookahead négatif qui ne peut jamais faire échouer le motif) pour inclure deux jeux de parenthèses dans notre motif, qui ne sont jamais utilisés. Cela nous permet de travailler avec des groupes 3 et 4 dans notre premier sous-modèle, tout en conservant les groupes 1 et 2 intacte pour la prochaine itération du second sous-modèle. En conclusion, ceci est checkForNextColumn :

(?!(?!)()()) 
(?=
  (?:
    .                  
    (?=                
      .*+\n            
      ( \3? . )   
      .*+\n        
      ( \4? . )    
    )
  )*?              
  X .*+\n          
  \3               
  X .*+\n          
  \4               
)

Qui, pour la plupart, semble vraiment familier.

C'est donc ça. En le comparant à d'autres données, on obtiendra un groupe 6 qui contient une capture pour chaque ligne où une colonne commence - et la longueur de la capture nous dira combien de colonnes ont commencé là.

Oui, cela fonctionne vraiment (démo en direct).

Notez que cette solution (comme la solution de base de .NET) surcomptera les colonnes qui sont plus de 3 X s long. Je suppose qu'il est possible de corriger ce nombre avec des lookaheads (d'une manière similaire au lookbehind de la solution .NET complète), mais ceci est laissé comme un exercice au lecteur.

Il est un peu dommage que le problème de base de cette solution soit déjà très alambiqué et gonfle la solution (75% des lignes ne sont pour la plupart que des copies de la solution de Qtax). Parce que le cadre environnant a quelques techniques et leçons vraiment intéressantes :

  • Nous pouvons avoir de multiples sous-modèles qui accomplissent des tâches spécifiques de correspondance/comptage, et les faire "communiquer" par le biais de groupes de capture mutuels, en les plaçant dans une (?|...) en alternance et en boucle.
  • Nous pouvons forcer les alternatives de largeur nulle à être exécutées encore et encore en les enveloppant dans un quantificateur fini tel que {2} avant de tout mettre dans + .
  • Les numéros de groupe peuvent être ignorés dans un sous-modèle (sans affecter le contenu capturé) en les plaçant dans un lookahead négatif infaillible tel que (?!(?!)()) .
  • Le contrôle peut passer d'un sous-modèle à l'autre en capturant quelque chose ou rien dans un certain groupe qui est vérifié lors de l'entrée dans l'alternance.

Cela permet des calculs très puissants (j'ai vu des affirmations selon lesquelles PCRE est en fait Turing-complet) - bien que ce soit certainement la mauvaise approche pour une utilisation productive. Mais essayer de comprendre (et de trouver) de telles solutions peut être un exercice de résolution de problèmes très stimulant et en quelque sorte gratifiant.

11voto

brian d foy Points 71781

Si vous voulez trouver un seul motif "vertical", voici une solution. Si vous souhaitez également faire correspondre un motif "horizontal", essayez de le faire avec une correspondance séparée, en vérifiant peut-être que les positions de correspondance se chevauchent. N'oubliez pas que l'ordinateur n'a aucune idée de ce qu'est une ligne. C'est une chose arbitraire créée par les humains. La chaîne n'est qu'une séquence unidimensionnelle dans laquelle nous désignons un ou plusieurs caractères comme étant une fin de ligne.

#!/usr/local/perls/perl-5.18.0/bin/perl
use v5.10;

my $pattern = qr/XXX/p;

my $string =<<'HERE';
....X.......
..X..X...X....
X.X...X..X.....
X....XXXXXX.....
X..XXX...........
.....X..........
..............X
..X...........X....
..X...........X....X...
....X.....
HERE

$transposed = transpose_string( $string );

open my $tfh, '<', \ $transposed;
while( <$tfh> ) {
    while( /$pattern/g ) {
        my $pos = pos() - length( ${^MATCH} );
        push @found, { row => $pos, col => $. - 1 };
        pos = $pos + 1; # for overlapping matches
        }
    }

# row and col are 0 based
print Dumper( \@found ); use Data::Dumper;

sub transpose_string {
    my( $string ) = @_;

    open my $sfh, '<', \ $string;

    my @transposed;
    while( <$sfh> ) {
        state $row = 0;
        chomp;
        my @chars = split //;

        while( my( $col, $char ) = each @chars ) {
            $transposed[$col][$row] = $char;
            }

        $row++;
        }

    my @line_end_positions = ( 0 );
    foreach my $col ( 0 .. $#transposed ) {
        $transposed .= join '', @{ $transposed[$col] };
        $transposed .= "\n";
        }
    close $sfh;

    return $transposed;
    }

3 votes

J'aime mieux la simplicité de la transposition de l'image en premier que la regex ésotérique...

1 votes

Ce sont des exemples étonnants qui exercent les moteurs regex ( .NET - qui l'aurait cru ?) et je suis impressionné. Dans une application réelle (pratique), la rotation ne serait-elle pas préférable à ces solutions plutôt baroques parce qu'elle est plus simple et donc moins "fragile" et plus facile à documenter ? Je ne fais que demander.

6voto

jaytea Points 290

Solution de travail à la question 2

C'est tout à fait possible en Perl/PCRE ! :)

Désolé, je suis un peu J'arrive extrêmement tard dans la soirée, mais je voudrais juste signaler que vous pouvez en fait compter le nombre de formations XXX trouvées, c'est-à-dire structurer l'expression de telle sorte qu'il y ait exactement une correspondance par formation lors d'une correspondance globale. Mais c'est assez délicat.

C'est ici :

\A(?:(?=(?(3)[\s\S]*(?=\3\z))(?|.(?=.*\n(\1?+.).*\n(\2?+.))|.*\n()())+?(?<=X)(?=.*\n\1(?<=X).*\n\2(?<=X))(?=([\s\S]*\z)))(?=[\s\S]*([\s\S](?(4)\4)))[\s\S])+[\s\S]*(?=\4\z)|\G(?!\A|[\s\S]?\z)

En action sur regex101

Je devrais probablement ajouter quelques commentaires à cela ! Voici, pour ceux qui sont intéressés :

\A(?:
    (?=
        (?(3)[\s\S]*(?=\3\z))                   # Resume from where we ended last iteration

        (?|                                     # Branch-reset group used to reset \1
            .(?=.*\n(\1?+.).*\n(\2?+.))         # and \2 to empty values when a new line
            |                                   # is reached. ".*\n" is used to skip the
            .*\n()()                            # rest of a line that is longer than the
        )+?                                     # ones below it.

        (?<=X)(?=.*\n\1(?<=X).*\n\2(?<=X))      # Find a XXX formation

        (?=([\s\S]*\z))                         # Save the rest of the line in \3 for 
    )                                           # when we resume looking next iteration

    (?=[\s\S]*([\s\S](?(4)\4)))                 # For every formation found, consume another 
                                                # character at the end of the subject

    [\s\S]                                      # Consume a character so we can move on

    )+

[\s\S]*(?=\4\z)                                 # When all formations around found, consume
                                                # up to just before \4 at the subject end.

|

\G(?!\A|[\s\S]?\z)                              # Now we just need to force the rest of the 
                                                # matches. The length of \4 is equal to the 
                                                # number of formations. So to avoid overmatching,
                                                # we need to exclude a couple of cases.

J'ai peur, qu'est-ce qui se passe ?

En gros, nous traversons l'ensemble du sujet en un groupe répété de non-capture, passant d'une formation XXX à une autre. Pour chaque formation trouvée, on ajoute un autre personnage sur un compteur à la fin du sujet (stocké dans le fichier \4 ). Il y avait quelques obstacles à surmonter :

  • Si nous faisons tout correspondre en une seule fois, comment pouvons-nous passer d'une ligne à l'autre ? Solution : utiliser un groupe de réinitialisation de branche pour réinitialiser \1 et \2 lorsqu'une nouvelle ligne est rencontrée.

  • Et si nous avions une grande grille ASCII avec un petit " \nX\nX\nX "à la fin ? Si nous consommons le sujet d'une formation à l'autre, nous commencerons à manger dans notre compteur. Solution : ne consommer qu'un seul caractère à la fois, et envelopper la logique de recherche de formation dans un lookahead.

  • Mais alors comment savoir où reprendre la recherche sur la prochaine itération du groupe de non-capture si nous ne consommons pas d'une formation à l'autre ? Solution : lorsqu'une formation est trouvée, capturer les caractères de cette position jusqu'à la toute fin. fin du sujet - un point fixe auquel on peut toujours se référer. C'est la même astuce que j'ai utilisée pour correspond à des parenthèses imbriquées sans récurrence ce qui illustre bien le pouvoir des références en avant.

C'était très amusant et j'adorerais voir plus de postes comme celui-ci !

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