web-dev-qa-db-fra.com

Pourquoi les moteurs de regex autorisent-ils/essaient-ils automatiquement la correspondance à la fin de la chaîne d'entrée?

Remarque:
* Python est utilisé pour illustrer les comportements, mais cette question n’est pas liée au langage.
* Aux fins de la présente discussion, supposons que une seule ligne entrée seulement , car la présence de nouvelles lignes (entrée sur plusieurs lignes) introduit des variations dans le comportement de $ et . qui sont accessoires aux questions posées.

La plupart des moteurs regex:

  • accepte une expression régulière qui tente explicitement de correspondre à une expression after la fin de la chaîne d'entrée[1].

    $ python -c "import re; print(re.findall('$.*', 'a'))"
    [''] # !! Matched the hypothetical empty string after the end of 'a'
    
  • lors de la recherche/remplacement de globalement _, c'est-à-dire lors de la recherche de toutes les superposations correspondances d'une expression rationnelle donnée et après avoir atteint la fin de la chaîne, essayez de manière inattendue correspondre à encore[2], comme expliqué dans cette réponse à une question connexe :

    $ python -c "import re; print(re.findall('.*$', 'a'))"
    ['a', ''] # !! Matched both the full input AND the hypothetical empty string
    

Il est peut-être inutile de préciser que ces tentatives de correspondance réussissent uniquement si l'expression rationnelle en question correspond à la chaîne vide _ (et l'expression rationnelle par défaut/est configurée pour signaler des correspondances sans longueur).

Ces comportements sont au moins à première vue contre-intuitifs, et je me demande si quelqu'un peut leur fournir un logique de la conception, notamment parce que:

  • le bénéfice de ce comportement n'est pas évident.
  • inversement, dans le contexte de la recherche/du remplacement global avec des modèles tels que .* et .*$, le comportement est carrément surprenant.[3]
    • Pour poser la question plus clairement: pourquoi la fonctionnalité conçue pour rechercher correspondances multiples et non imbriquées d'une regex - c'est-à-dire, global correspondance - décide de même (tentative) une autre correspondance si elle sait que la totalité de l'entrée a déjà été consommée}, indépendamment de ce qu'est la regex (bien que vous ne voyiez jamais le symptôme avec une regex qui n'a pas au moins aussi correspond à la chaîne vide)
    • Les langages/moteurs suivants présentent un comportement surprenant: .NET, Python (2.x et 3.x)[2], Perl (5.x et 6.x), Ruby, Node.js (JavaScript)

Notez que le comportement des moteurs d’expression régulière varie en ce qui concerne où continuer la correspondance après une correspondance longueur nulle _ (chaîne vide).

L’un ou l’autre choix (commencer à la même position de personnage et commencer au suivant) est défendable - voir le chapitre sur les matchs nuls sur www.normale-expressions.info .

En revanche, le cas .*$ discuté ici est différent en ce que, avec toute entrée non vide, la correspondance première pour .*$ est non une correspondance de longueur nulle, la différence de comportement ne _ {pas_applique - au lieu de cela, la position du personnage doit avancer inconditionnellement_ après le premier match, ce qui est bien sûr impossible si vous êtes déjà à la fin.
Encore une fois, ma surprise est de constater que un autre match est tenté malgré tout, même s'il ne reste rien par définition.


[1] J'utilise $ comme marqueur de fin de saisie ici, même si dans certains moteurs, tels que .NET, il peut marquer la fin de la fin de l'entrée éventuellement suivi d'un retour à la ligne final} _. Toutefois, le comportement s'applique également lorsque vous utilisez le marqueur de fin d'entrée inconditionnel, \z.

[2] Python 2.x et 3.x jusqu'à 3.6.x apparemment remplacement comportement apparemment spécial dans ce contexte: python -c "import re; print(re.sub('.*$', '[\g<0>]', 'a'))" ne donnait que [a] - c'est-à-dire, seulement un _ correspondance a été trouvée et remplacée.
Depuis Python 3.7, le comportement est maintenant identique à celui de la plupart des autres moteurs de regex, où deux remplacements sont effectués, générant [a][].

[3] Pour éviter le problème, vous pouvez (a) choisir une méthode de remplacement conçue pour rechercher au plus un} match ou _ (b) utiliser ^.* pour éviter que plusieurs correspondances ne soient trouvées via start-of-. ancrage d'entrée.
(a) peut ne pas être une option, en fonction de la manière dont une langue présente les fonctionnalités; Par exemple, l'opérateur -replace de PowerShell invariablement remplace les occurrences tous; Considérez la tentative suivante pour inclure tous les éléments du tableau dans "...":
'a', 'b' -replace '.*', '"$&"'. En raison de la correspondance deux fois, cela renvoie les éléments "a""" et "b""";
option (b), 'a', 'b' -replace '^.*', '"$&"', corrige le problème.

23
mklement0

Remarque:
* Mon article de question contient deux questions liées, mais distinctes, pour lesquelles j'aurais dû créer des articles distincts, comme je le sais maintenant.
* Les autres réponses concernent ici une des questions, donc cette partie fournit en partie une feuille de route quelles réponses adressent quelle question .


Pourquoi des motifs tels que $<expr> sont-ils autorisés/quand ils ont un sens:

  • La réponse de dawg affirme que des combinaisons absurdes telles que $.+probablement ne sont pas empêchées pour (pragmatique} _ raisons; les exclure peut ne pas en valoir la peine.

  • La réponse de Tim montre comment certaines expressions peut avoir un sens après le $, à savoir des assertions négatives.

  • La seconde partie de la réponse de ivan_pozdeev answer synthétise de manière convaincante les réponses de Dawg et de Tim.


Pourquoi la correspondance globale trouve-t-elle deux correspondances pour des modèles tels que .* et .*$:

  • La réponse de revo contient d'excellentes informations de base sur la correspondance de longueur nulle (chaîne vide), ce à quoi le problème finalement se résume.

Permettez-moi de compléter sa réponse en la rapportant plus directement à la manière dont le comportement contredit mes attentes dans le contexte de la correspondance {globale}:

  • D'un point de vue purement {sens commun} _, il va de soi qu'une fois que l'entrée a été entièrement consommée lors de la mise en correspondance, il y a par définition plus rien, il n'y a donc aucune raison de chercher autres matches.

  • En revanche, la plupart des moteurs d’expression régulière considèrent la position du caractère après le dernier caractère de la chaîne de saisie} - la position connue sous le nom chaîne de fin du sujet dans certains moteurs - une position de départ valide pour une correspondance et tentez donc une autre.

    • Si l'expression régulière à portée de main correspond à la chaîne vide (produit une correspondance de longueur nulle; par exemple, des expressions rationnelles telles que .* ou a?), elle correspond à cette position et renvoie une correspondance de chaîne vide.

    • Inversement, vous ne verrez pas de correspondance supplémentaire si l'expression régulière ne correspond pas (également) à la chaîne vide - bien que la correspondance supplémentaire soit toujours tentée dans tous les cas, aucune correspondance ne sera trouvée dans ce cas, étant donné que la chaîne vide est la seule correspondance possible à la position de fin de chaîne de sujet.

Bien que cela fournisse une explication du comportement {technique} _, il ne nous indique toujours pas pourquoi correspondant à après le dernier caractère a été implémenté.

La chose la plus proche que nous ayons est une conjecture éclairée de Wiktor Stribiżew dans un commentaire (soulignement ajouté), ce qui suggère encore une raison de-{pragmatique}:

... comme lorsque vous obtenez une correspondance de chaîne vide, vous pouvez toujours faire correspondre le caractère suivant qui est toujours au même index dans la chaîne. Si un moteur de regex ne le supportait pas, ces matchs seraient ignorés. Faire une exception pour la fin de chaîne n'était probablement pas aussi critique pour les auteurs de moteurs d'expression régulière.

La première moitié de La réponse de ivan_pozdeev explique le comportement de manière plus détaillée en nous indiquant que le void à la fin de la chaîne [input] est une position valide pour la correspondance, comme tout autre caractère-limite position.
Cependant, bien que traiter toutes ces positions de la même manière est certainement cohérent en interne et simplifie vraisemblablement le implémentation, le comportement défie toujours le sens commun et n'a aucun avantage évident pour le utilisateur.


Autres observations sur la correspondance chaîne vide:

Remarque: Dans tous les extraits de code ci-dessous, la chaîne globale remplacement est utilisée pour mettre en évidence les correspondances résultantes: chaque correspondance est placée dans [...], alors que les parties non correspondantes de l'entrée sont transmises telles quelles .

Notez cependant que la correspondance à la position de fin de chaîne de sujet est non limitée aux moteurs où la correspondance se poursuit à la position identique après un vide rencontre.

Par exemple, le moteur de regex .NET fait pas pour le faire (exemple PowerShell):

PS> 'a1' -replace '\d*|a', '[$&]'
[]a[1][]

C'est:

  • \d* correspond à la chaîne vide avanta
  • a a fait elle-même pas correspondance, ce qui implique que la position du caractère était avancée après la correspondance vide.
  • 1 a été apparié par \d*
  • La position de fin de chaîne de sujet a de nouveau été mise en correspondance avec \d*, entraînant une autre correspondance de chaîne vide.

Perl 5 est un exemple de moteur qui fait reprend la correspondance à la position identique du caractère:

$ "a1" | Perl -ple "s/\d*|a/[$&]/g"
[][a][1][]

Notez que a a également été mis en correspondance.

Fait intéressant, Perl 6 _ ne se comporte pas seulement différemment, mais présente une autre variante de comportement:

$ "a1" | Perl6 -pe "s:g/\d*|a/[$/]/"
[a][1][]

Apparemment, si une alternance trouve les deux et une correspondance vide et non vide, seule la non-vide est signalée - voir le commentaire de revo ci-dessous.

3
mklement0

Je donne cette réponse juste pour montrer pourquoi une regex voudrait autoriser tout code apparaissant après la dernière ancre $ dans le motif. Supposons que nous devions créer une expression rationnelle pour faire correspondre une chaîne avec les règles suivantes:

  • commence par trois chiffres
  • suivi d'une ou plusieurs lettres, chiffres, trait d'union ou trait de soulignement
  • se termine avec seulement des lettres et des chiffres

Nous pourrions écrire le modèle suivant:

^\d{3}[A-Za-z0-9\-_]*[A-Za-z0-9]$

Mais ceci est un peu volumineux, car nous devons utiliser deux classes de caractères similaires adjacentes. Au lieu de cela, nous pourrions écrire le motif comme suit:

^\d{3}[A-Za-z0-9\-_]+$(?<!_|-)

ou

^\d{3}[A-Za-z0-9\-_]+(?<!_|-)$

Ici, nous avons éliminé l'une des classes de caractères et utilisé un point d'ancrage négatif après le $ pour affirmer que le dernier caractère n'était ni un trait de soulignement ni un trait d'union.

Hormis un simple retour en arrière, la raison pour laquelle un moteur de regex permettrait à quelque chose d'apparaître après l'ancre $ n'a pas de sens. Ce que je veux dire ici, c’est qu’un moteur de regex peut permettre à un regard d’apparaître de se placer après le $, et il existe des cas où il est logique de le faire.

5
Tim Biegeleisen

Rappelons plusieurs choses:

  1. ^ et $ sont assertions de largeur nulle - ils correspondent juste après le début logique de la chaîne (ou après chaque ligne se terminant en mode multiligne avec l'indicateur m dans la plupart des implémentations regex) ou à la fin logique de la chaîne ( ou fin de ligne AVANT le ou les caractères de fin de ligne en mode multiligne.)

  2. .* est potentiellement un correspondance de longueur nulle sans correspondance. La version zéro seule longueur serait $(?:end of line){0}DEMO (ce qui est utile comme commentaire, je suppose ...)

  3. . ne correspond pas à \n (sauf si vous avez l'indicateur s) mais correspond à la fin de la ligne \r dans Windows CRLF. Donc, $.{1} correspond uniquement aux fins de ligne Windows par exemple (mais ne le faites pas. Utilisez plutôt le \r\n littéral.)

Il n'y a pas de bénéfice particulier autre que de simples cas d'effets secondaires. 

  1. Le regex $ est utile; 
  2. .* est utile. 
  3. Les expressions régulières ^(?a lookahead) et (?a lookbehind)$ sont courantes et utiles. 
  4. Les expressions régulières (?a lookaround)^ ou $(?a lookaround) sont potentiellement utiles. 
  5. La regex $.* n'est pas utile et suffisamment rare pour ne pas justifier la mise en œuvre d'une optimisation afin que le moteur cesse de regarder avec ce cas Edge. La plupart des moteurs regex font un travail décent d'analyse syntaxique; une attelle ou une parenthèse manquante, par exemple. Pour que le moteur analyse $.* ne soit pas utile, il faudrait analyser la signification de cette expression rationnelle comme différente de $(something else).
  6. Ce que vous obtiendrez dépendra beaucoup de la saveur des expressions rationnelles et de l’état des indicateurs s et m.

Pour des exemples de remplacements, considérez la sortie de script Bash suivante issue de plusieurs types de regex principaux:

#!/bin/bash

echo "Perl"
printf  "123\r\n" | Perl -lnE 'say if s/$.*/X/mg' | od -c
echo "sed"
printf  "123\r\n" | sed -E 's/$.*/X/g' | od -c
echo "python"
printf  "123\r\n" | python -c "import re, sys; print re.sub(r'$.*', 'X', sys.stdin.read(),flags=re.M) " | od -c
echo "awk"
printf  "123\r\n" | awk '{gsub(/$.*/,"X")};1' | od -c
echo "Ruby"
printf  "123\r\n" | Ruby -lne 's=$_.gsub(/$.*/,"X"); print s' | od -c

Impressions:

Perl
0000000    X   X   2   X   3   X  \r   X  \n                            
0000011
sed
0000000    1   2   3  \r   X  \n              
0000006
python
0000000    1   2   3  \r   X  \n   X  \n                                
0000010
awk
0000000    1   2   3  \r   X  \n                                        
0000006
Ruby
0000000    1   2   3   X  \n                                            
0000005
4
dawg

Quelle est la raison derrière .* avec modificateur global sur? Parce que quelqu'un s'attend à ce qu'une chaîne vide soit renvoyée sous forme de correspondance ou ne sait pas ce qu'est le quantificateur *, sinon le modificateur global ne devrait pas être défini. .* sans g ne renvoie pas deux correspondances.

ce n'est pas évident de savoir quel est l'avantage de ce comportement.

Il ne devrait pas y avoir d'avantage. En fait, vous vous interrogez sur l'existence d'une correspondance de longueur nulle. Vous demandez pourquoi existe-t-il une chaîne de longueur nulle?

Nous avons trois endroits valides pour lesquels une chaîne de longueur nulle existe:

  • Début de la chaîne de sujet
  • Entre deux personnages
  • Chaîne de fin de sujet

Nous devrions rechercher la raison plutôt que l'avantage de cette seconde sortie de correspondance de longueur zéro en utilisant .* avec le modificateur g (ou une fonction qui recherche toutes les occurrences). Cette position de longueur zéro après une chaîne d'entrée a des utilisations logiques. Le diagramme d'état ci-dessous est extrait de debuggex avec .*, mais j'ai ajouté epsilon lors de la transition directe de l'état de démarrage à l'état d'acceptation pour illustrer une définition:

 enter image description here

C'est une correspondance de longueur nulle (pour en savoir plus sur epsilon transition ).

Tous ces éléments sont liés à la gourmandise et à la non-gourmandise. Sans les positions de longueur nulle, une expression rationnelle telle que .?? n'aurait aucune signification. Il ne tente pas le point en premier, il le saute. Il correspond à une chaîne de longueur zéro à cette fin pour transférer l'état actuel à un état acceptable temporaire. 

Sans une position de longueur nulle, .?? ne pourrait jamais ignorer un caractère dans la chaîne d'entrée, ce qui donnerait une toute nouvelle saveur.

La définition de la gourmandise/la paresse conduit à des correspondances sans longueur.

3
revo

"Nul à la fin de la chaîne" est une position distincte pour les moteurs de regex car un moteur de regex traite les positions entre caractères de saisie:

|a|b|c|   <- input line

^ ^ ^ ^
positions at which a regex engine can "currently be"

Toutes les autres positions peuvent être décrites comme "avant le Nième caractère", mais pour la fin, il n’ya pas de caractère auquel se référer.

Comme indiqué dans Correspondances de longueur nulle avec Regex - Regular-expressions.info , il est également nécessaire de prendre en charge les correspondances sans longueur (qui ne sont pas toutes prises en charge par les saveurs de regex):

  • Par exemple. une expression régulière \d* sur la chaîne abc correspondrait 4 fois: avant chaque lettre et à la fin.

$ est autorisé n'importe où dans la regex pour des raisons d'uniformité: il est traité de la même manière comme tout autre jeton et correspond à cette position magique de "fin de chaîne". Le fait de "finaliser" le travail sur les expressions rationnelles conduirait à une incohérence inutile dans le travail du moteur et empêcherait d’autres choses utiles qui pourraient s’y correspondre, comme par exemple, lookbehind ou \b (en gros, tout ce qui peut être une correspondance de longueur nulle) - c’est-à-dire qu’il s’agirait à la fois d’une complication de conception et d’une limitation fonctionnelle sans aucun avantage.


Enfin, pour répondre à la question pourquoi un moteur d’expression rationnelle peut ou non essayer d’apparier "à nouveau" à la même position, nous renvoyons à Avancer après une correspondance regex de longueur nulle - Correspondances de regex de longueur nulle - Normal -expressions.info :

Supposons que nous avons la regex \d*|x, la chaîne de sujet x1

La première correspondance est une correspondance vide au début de la chaîne. Maintenant, comment donnons-nous une chance aux autres jetons sans nous coincer dans une boucle infinie?

La solution la plus simple, utilisée par la plupart des moteurs regex, consiste à lancer la tentative de correspondance suivante avec un caractère après la fin de la correspondance précédente.

Cela peut donner des résultats contre-intuitifs - par exemple la regex ci-dessus correspondra à '' au début, 1 et '' à la fin - mais pas à x.

L’autre solution, utilisée par Perl, consiste à toujours lancer la tentative de correspondance suivante à la fin du match précédent, qu’elle ait été nulle ou non. Si elle était de longueur nulle, le moteur en prend note, car il ne doit pas permettre une correspondance de longueur nulle à la même position.

Ce qui "saute" correspond moins au coût d'une complexité supplémentaire. Par exemple. la regex ci-dessus produira '', x, 1 et '' à la fin.

L'article montre ensuite que il n'y a pas de meilleures pratiques établies ici et divers moteurs de regex testent activement de nouvelles approches pour tenter de produire des résultats plus "naturels":

Le moteur JGsoft constitue une exception. Le moteur JGsoft avance d'un caractère après une correspondance de longueur nulle, comme le font la plupart des moteurs. Mais il a une règle supplémentaire pour ignorer les matchs de longueur nulle à la position où le le match précédent est terminé, vous ne pouvez donc jamais avoir un match nul immédiatement adjacente à une correspondance de longueur non nulle. Dans notre exemple, le Le moteur JGsoft ne trouve que deux correspondances: la correspondance de longueur nulle au début de la chaîne, et 1.

Python 3.6 et l’avance précédente après les matchs nuls. Le gsub () La fonction de recherche-remplacement ignore les correspondances de longueur zéro au niveau de la la position où le match précédent de longueur non nulle s'est terminé, mais le La fonction finditer () renvoie ces correspondances. Donc, une recherche et remplacement dans Python donne les mêmes résultats que les applications Just Great Software, mais la liste de toutes les correspondances ajoute la correspondance de longueur zéro à la fin du chaîne.

Python 3.7 a tout changé. Il gère les correspondances sans longueur comme Perl . gsub () remplace maintenant les correspondances de longueur nulle adjacentes à un autre match. Cela signifie des expressions régulières qui peuvent trouver les correspondances de longueur nulle ne sont pas compatibles entre Python 3.7 et les versions antérieures versions de Python.

PCRE 8.00 et versions ultérieures et PCRE2 gèrent les correspondances nuls comme Perl par revenir en arrière. Ils ne font plus avancer un caractère après une longueur nulle correspond comme le faisait PCRE 7.9.

Les fonctions regexp dans R et PHP sont basées sur PCRE, elles évitent donc rester coincé dans un match nul en faisant marche arrière comme PCRE le fait . Mais la fonction gsub () pour rechercher et remplacer dans R saute également correspond à la longueur zéro à la position où la précédente avait une longueur différente de zéro match terminé, comme gsub () dans Python 3.6 et versions antérieures. L'autre Les expressions rationnelles dans R et toutes les fonctions dans PHP autorisent correspondances de longueur zéro immédiatement adjacentes à des correspondances de longueur différente, tout comme PCRE lui-même.

1
ivan_pozdeev

Je ne sais pas d'où vient la confusion.
Les moteurs de regex sont fondamentalement stupides.
Ils sont comme Mikey, ils mangent n'importe quoi. 

$ python -c "import re; print(re.findall('$.*', 'a'))"
[''] # !! Matched the hypothetical empty string after the end of 'a'

Vous pouvez mettre mille expressions facultatives après $ et cela correspondra toujours à la
EOS. Les moteurs sont stupides. 

$ python -c "import re; print(re.findall('.*$', 'a'))"
['a', ''] # !! Matched both the full input AND the hypothetical empty string

Pensez-y de cette façon, il y a deux expressions indépendantes ici
.* | $. La raison en est que la première expression est facultative.
Il arrive juste de buter contre l'affirmation EOS.
Ainsi, vous obtenez 2 correspondances sur une chaîne non vide. 

Pourquoi la fonctionnalité conçue pour rechercher plusieurs correspondances d'une regex, c'est-à-dire une correspondance globale - décide même d'essayer une autre correspondance si elle sait que la totalité de l'entrée a déjà été utilisée,}

La classe de choses appelée assertions n'existe pas aux positions des personnages.
Ils n’existent que ENTRE positions de caractères.
Si elles existent dans la regex, vous ne savez pas si la totalité de l'entrée a été consommée.
Si elles peuvent être satisfaites comme une étape indépendante, mais seulement une fois, elles seront identiques
indépendamment. 

Rappelez-vous, regex est une proposition left-to-right.
Souvenez-vous aussi que les moteurs sont stupides.
C'est par conception.
Chaque construction est un état dans le moteur, c'est comme un pipeline.
Ajouter de la complexité va sûrement le ruiner. 

En passant, est-ce que .*a commence réellement au début et vérifie chaque caractère?
Non. .* commence immédiatement à la fin de la chaîne (ou de la ligne, selon) et commence
revenir en arrière. 

Une autre chose amusante. Je vois beaucoup de novices qui utilisent .*? au fin de leur
regex, en pensant que tout le kruft restant de la chaîne sera récupéré.
C'est inutile, ça ne correspondra jamais à rien.
Même une regex autonome .*? ne correspondra jamais à autant de caractères
il y a dans la chaîne. 

Bonne chance! Ne vous inquiétez pas, les moteurs de regex sont juste ... eh bien, stupide.

0
sln