Javascript: équivalent lookbehind négatif?


142

Existe-t-il un moyen d'obtenir l'équivalent d'un regard négatif dans les expressions régulières javascript? J'ai besoin de faire correspondre une chaîne qui ne commence pas par un ensemble spécifique de caractères.

Il semble que je ne puisse pas trouver une expression régulière qui fasse cela sans échouer si la partie correspondante se trouve au début de la chaîne. Les regards négatifs semblent être la seule réponse, mais javascript n'en a pas.

EDIT: C'est le regex que j'aimerais travailler, mais ce n'est pas le cas:

(?<!([abcdefg]))m

Donc, cela correspondrait au 'm' dans 'jim' ou 'm', mais pas à 'jam'


Pensez à publier l'expression régulière telle qu'elle apparaîtrait avec un regard négatif en arrière; cela peut faciliter la réponse.
Daniel LeCheminant

1
Ceux qui veulent suivre l'adoption de lookbehind,
Wiktor Stribiżew

@ WiktorStribiżew: des look-behind ont été ajoutés dans la spécification 2018. Chrome les prend en charge, mais Firefox n'a toujours pas implémenté la spécification .
Lonnie Best

Cela a-t-il même besoin d'un regard derrière? Et quoi (?:[^abcdefg]|^)(m)? As in"mango".match(/(?:[^abcdefg]|^)(m)/)[1]
slebetman

Réponses:


58

Lookbehind Assertions a été accepté dans la spécification ECMAScript en 2018.

Utilisation positive du regard en arrière:

console.log(
  "$9.99  €8.47".match(/(?<=\$)\d+(\.\d*)?/) // Matches "9.99"
);

Utilisation négative du regard en arrière:

console.log(
  "$9.99  €8.47".match(/(?<!\$)\d+(?:\.\d*)/) // Matches "8.47"
);

Prise en charge de la plate-forme:


2
y a-t-il un polyfill?
Killy

1
@Killy il n'y en a pas pour autant que je sache, et je doute qu'il y en ait un jamais, car en créer un serait potentiellement très peu pratique (IE écrivant une implémentation complète de Regex dans JS)
Okku

Qu'en est-il de l'utilisation d'un plugin babel, est-il possible d'être compilé vers ES5 ou déjà pris en charge ES6?
Stefan J

1
@IlpoOksanen Je pense que vous voulez dire étendre l'implémentation RegEx ... ce que font les polyfills .... et il n'y a rien de mal à écrire la logique en JavaScript
neaumusic

1
Qu'est-ce que tu racontes? Presque toutes les propositions sont inspirées par d'autres langages et elles préféreront toujours faire correspondre la syntaxe et la sémantique d'autres langages là où cela a du sens dans le contexte du JS idiomatique et de la rétrocompatibilité. Je pense avoir clairement déclaré que les perspectives positives et négatives avaient été acceptées dans la spécification 2018 en 2017 et j'ai donné des liens vers des sources. De plus, j'ai décrit en détail quelles plates-formes implémentent ladite spécification et quel est le statut des autres plates-formes - et je l'ai même mise à jour depuis. Naturellement, ce n'est pas la dernière fonctionnalité Regexp que nous verrons
Okku

83

Depuis 2018, Lookbehind Assertions fait partie de la spécification du langage ECMAScript .

// positive lookbehind
(?<=...)
// negative lookbehind
(?<!...)

Réponse avant 2018

Comme Javascript prend en charge la recherche négative , une façon de le faire est:

  1. inverser la chaîne d'entrée

  2. correspondance avec une regex inversée

  3. inverser et reformater les allumettes


const reverse = s => s.split('').reverse().join('');

const test = (stringToTests, reversedRegexp) => stringToTests
  .map(reverse)
  .forEach((s,i) => {
    const match = reversedRegexp.test(s);
    console.log(stringToTests[i], match, 'token:', match ? reverse(reversedRegexp.exec(s)[0]) : 'Ø');
  });

Exemple 1:

Suite à la question de @ andrew-ensley:

test(['jim', 'm', 'jam'], /m(?!([abcdefg]))/)

Les sorties:

jim true token: m
m true token: m
jam false token: Ø

Exemple 2:

Après le commentaire @neaumusic (correspond max-heightmais pas line-height, le jeton étant height):

test(['max-height', 'line-height'], /thgieh(?!(-enil))/)

Les sorties:

max-height true token: height
line-height false token: Ø

36
le problème avec cette approche est qu'elle ne fonctionne pas lorsque vous avez à la fois regarder en avant et en arrière
kboom

3
pouvez-vous s'il vous plaît montrer un exemple de travail, dire que je veux correspondre max-heightmais pas line-heightet je veux seulement que le match soitheight
neaumusic

Cela n'aide pas si la tâche consiste à remplacer deux symboles identiques consécutifs (et pas plus de 2) qui ne sont pas précédés d'un symbole. ''(?!\()remplacera les apostrophes à ''(''test'''''''testpartir de l'autre extrémité, laissant ainsi (''test'NNNtestplutôt que (''testNNN'test.
Wiktor Stribiżew

61

Supposons que vous vouliez tout trouver intnon précédé de unsigned:

Avec prise en charge du look-behind négatif:

(?<!unsigned )int

Sans prise en charge de la rétroaction négative:

((?!unsigned ).{9}|^.{0,8})int

Fondamentalement, l'idée est de saisir n caractères précédents et d'exclure la correspondance avec une anticipation négative, mais également de faire correspondre les cas où il n'y a pas de n caractères précédents. (où n est la longueur du regard derrière).

Donc le regex en question:

(?<!([abcdefg]))m

se traduirait par:

((?!([abcdefg])).|^)m

Vous devrez peut-être jouer avec la capture de groupes pour trouver l'endroit exact de la chaîne qui vous intéresse ou vous souhaitez remplacer une partie spécifique par autre chose.


2
Cela devrait être la bonne réponse. Voir: "So it would match the 'm' in 'jim' or 'm', but not 'jam'".replace(/(j(?!([abcdefg])).|^)m/g, "$1[MATCH]") retourne "So it would match the 'm' in 'ji[MATCH]' or 'm', but not 'jam'" C'est assez simple et ça marche!
Asrail

41

La stratégie de Mijoja fonctionne pour votre cas spécifique mais pas en général:

js>newString = "Fall ball bill balll llama".replace(/(ba)?ll/g,
   function($0,$1){ return $1?$0:"[match]";});
Fa[match] ball bi[match] balll [match]ama

Voici un exemple où le but est de faire correspondre un double-l mais pas s'il est précédé de "ba". Notez le mot «balll» - le vrai regard en arrière aurait dû supprimer les 2 premiers l mais correspondre à la 2ème paire. Mais en faisant correspondre les 2 premiers l et en ignorant ensuite cette correspondance comme un faux positif, le moteur d'expression régulière part de la fin de cette correspondance et ignore tous les caractères dans le faux positif.


5
Ah, vous avez raison. Cependant, c'est beaucoup plus proche que je ne l'étais auparavant. Je peux accepter cela jusqu'à ce que quelque chose de mieux arrive (comme javascript implémentant réellement lookbehinds).
Andrew Ensley

33

Utilisation

newString = string.replace(/([abcdefg])?m/, function($0,$1){ return $1?$0:'m';});

10
Cela ne fait rien: newStringsera toujours égal string. Pourquoi tant de votes positifs?
MikeM

@MikeM: parce que le but est simplement de démontrer une technique d'appariement.
bug du

57
@punaise. Une démonstration qui ne fait rien est une étrange sorte de démonstration. La réponse apparaît comme s'il s'agissait simplement de copier-coller sans aucune compréhension de son fonctionnement. Ainsi, le manque d'explication d'accompagnement et le fait de ne pas démontrer que quoi que ce soit a été apparié.
MikeM

2
@MikeM: la règle de SO est, si elle répond à la question telle qu'elle est écrite , elle est correcte. OP n'a pas spécifié de cas d'utilisation
bogue du

7
Le concept est correct, mais oui, ce n'est pas très bien démo. Essayez de lancer dans la console JS ... "Jim Jam Momm m".replace(/([abcdefg])?m/g, function($0, $1){ return $1 ? $0 : '[match]'; });. Il devrait revenir Ji[match] Jam Mo[match][match] [match]. Mais notez également que, comme Jason l'a mentionné ci-dessous, il peut échouer sur certains cas extrêmes.
Simon East

11

Vous pouvez définir un groupe non capturant en annulant votre jeu de caractères:

(?:[^a-g])m

... qui correspondrait à chaque m NON précédé de l'une de ces lettres.


2
Je pense que le match couvrirait également le personnage précédent.
Sam le

4
^ c'est vrai. Une classe de personnage représente ... un personnage! Tout ce que votre groupe non capturant fait, c'est de ne pas rendre cette valeur disponible dans un contexte de remplacement. Votre expression ne dit pas "chaque m n'est précédé d'aucune de ces lettres", mais "chaque m précédé d'un caractère qui n'est PAS l'une de ces lettres"
theflowersoftime

5
Pour que la réponse résolve également le problème d'origine (début de chaîne), elle doit également inclure une option, de sorte que l'expression régulière résultante le serait (?:[^a-g]|^)m. Voir regex101.com/r/jL1iW6/2 pour un exemple d'exécution.
Johny Skovdal

L'utilisation de la logique vide n'a pas toujours l'effet souhaité.
GoldBishop

2

Voici comment j'ai réalisé str.split(/(?<!^)@/)pour Node.js 8 (qui ne prend pas en charge lookbehind):

str.split('').reverse().join('').split(/@(?!$)/).map(s => s.split('').reverse().join('')).reverse()

Travaux? Oui (unicode non testé). Désagréable? Oui.


1

en suivant l'idée de Mijoja, et en m'inspirant des problèmes exposés par JasonS, j'ai eu cette idée; J'ai vérifié un peu mais je ne suis pas sûr de moi, donc une vérification par quelqu'un de plus expert que moi dans js regex serait géniale :)

var re = /(?=(..|^.?)(ll))/g
         // matches empty string position
         // whenever this position is followed by
         // a string of length equal or inferior (in case of "^")
         // to "lookbehind" value
         // + actual value we would want to match

,   str = "Fall ball bill balll llama"

,   str_done = str
,   len_difference = 0
,   doer = function (where_in_str, to_replace)
    {
        str_done = str_done.slice(0, where_in_str + len_difference)
        +   "[match]"
        +   str_done.slice(where_in_str + len_difference + to_replace.length)

        len_difference = str_done.length - str.length
            /*  if str smaller:
                    len_difference will be positive
                else will be negative
            */

    }   /*  the actual function that would do whatever we want to do
            with the matches;
            this above is only an example from Jason's */



        /*  function input of .replace(),
            only there to test the value of $behind
            and if negative, call doer() with interesting parameters */
,   checker = function ($match, $behind, $after, $where, $str)
    {
        if ($behind !== "ba")
            doer
            (
                $where + $behind.length
            ,   $after
                /*  one will choose the interesting arguments
                    to give to the doer, it's only an example */
            )
        return $match // empty string anyhow, but well
    }
str.replace(re, checker)
console.log(str_done)

ma sortie personnelle:

Fa[match] ball bi[match] bal[match] [match]ama

le principe est d'appeler checkerà chaque point de la chaîne entre deux caractères quelconques, chaque fois que cette position est le point de départ de:

--- toute sous-chaîne de la taille de ce qui n'est pas voulu (ici 'ba', donc ..) (si cette taille est connue; sinon, cela doit être plus difficile à faire peut-être)

--- --- ou plus petit que cela si c'est le début de la chaîne: ^.?

et, à la suite de cela,

--- ce qui doit être réellement recherché (ici 'll').

A chaque appel de checker, il y aura un test pour vérifier si la valeur avant lln'est pas ce que nous ne voulons pas ( !== 'ba'); si c'est le cas, nous appelons une autre fonction, et ce sera celle-ci ( doer) qui fera les changements sur str, si le but est celui-ci, ou plus génériquement, qui entrera en entrée les données nécessaires pour traiter manuellement les résultats de la numérisation de str.

ici nous changeons la chaîne donc nous devions garder une trace de la différence de longueur afin de décaler les emplacements donnés par replace, tous calculés sur str, qui lui-même ne change jamais.

puisque les chaînes primitives sont immuables, nous aurions pu utiliser la variable strpour stocker le résultat de toute l'opération, mais j'ai pensé que l'exemple, déjà compliqué par les remplacements, serait plus clair avec une autre variable ( str_done).

Je suppose qu'en termes de performances, cela doit être assez dur: tous ces remplacements inutiles de `` into '', this str.length-1temps, plus ici le remplacement manuel par un exécutant, ce qui signifie beaucoup de tranchage ... probablement dans ce cas spécifique ci-dessus qui pourrait être regroupés, en coupant la corde une seule fois en morceaux autour de l'endroit où nous voulons l'insérer [match]et .join()en la coupant avec [match]elle-même.

l'autre chose est que je ne sais pas comment il traiterait des cas plus complexes, c'est-à-dire des valeurs complexes pour le faux regard en arrière ... la longueur étant peut-être les données les plus problématiques à obtenir.

et, dans le checkercas de multiples possibilités de valeurs non désirées pour $ behind, nous devrons faire un test dessus avec encore une autre regex (il est préférable d'être mis en cache (créé) à l'extérieur checker, pour éviter que le même objet regex ne soit créé à chaque appel pour checker) savoir si c'est ce que nous cherchons à éviter ou non.

j'espère avoir été clair; sinon n'hésitez pas, j'essaierai mieux. :)


1

En utilisant votre cas, si vous voulez remplacer m par quelque chose, par exemple le convertir en majuscules M, vous pouvez annuler l'ensemble dans le groupe de capture.

match ([^a-g])m, remplacer par$1M

"jim jam".replace(/([^a-g])m/g, "$1M")
\\jiM jam

([^a-g])correspondra à n'importe quel caractère not ( ^) de la a-gplage, et le stockera dans le premier groupe de capture, afin que vous puissiez y accéder avec $1.

Nous trouvons donc imdans jimet le remplacer par ce iMqui entraîne jiM.


1

Comme mentionné précédemment, JavaScript permet désormais de regarder en arrière. Dans les navigateurs plus anciens, vous avez toujours besoin d'une solution de contournement.

Je parie ma tête qu'il n'y a aucun moyen de trouver une expression régulière sans regarder en arrière qui donne exactement le résultat. Tout ce que vous pouvez faire, c'est travailler avec des groupes. Supposons que vous ayez une regex (?<!Before)Wanted, où Wantedest l'expression régulière que vous voulez faire correspondre et Beforeest l'expression régulière qui compte ce qui ne doit pas précéder la correspondance. Le mieux que vous puissiez faire est d'annuler le regex Beforeet d'utiliser le regex NotBefore(Wanted). Le résultat souhaité est le premier groupe $1.

Dans votre cas, Before=[abcdefg]ce qui est facile à nier NotBefore=[^abcdefg]. Ainsi le regex serait [^abcdefg](m). Si vous avez besoin de la position de Wanted, vous devez NotBeforeégalement regrouper , de sorte que le résultat souhaité soit le deuxième groupe.

Si les correspondances du Beforemodèle ont une longueur fixe n, c'est-à-dire si le modèle ne contient aucun jeton répétitif, vous pouvez éviter de nier le Beforemodèle et utiliser l'expression régulière (?!Before).{n}(Wanted), mais vous devez toujours utiliser le premier groupe ou utiliser l'expression régulière (?!Before)(.{n})(Wanted)et utiliser le second groupe. Dans cet exemple, le motif a Beforeen fait une longueur fixe, à savoir 1, utilisez donc l'expression régulière (?![abcdefg]).(m)ou (?![abcdefg])(.)(m). Si vous êtes intéressé par toutes les correspondances, ajoutez le gdrapeau, voir mon extrait de code:

function TestSORegEx() {
  var s = "Donald Trump doesn't like jam, but Homer Simpson does.";
  var reg = /(?![abcdefg])(.{1})(m)/gm;
  var out = "Matches and groups of the regex " + 
            "/(?![abcdefg])(.{1})(m)/gm in \ns = \"" + s + "\"";
  var match = reg.exec(s);
  while(match) {
    var start = match.index + match[1].length;
    out += "\nWhole match: " + match[0] + ", starts at: " + match.index
        +  ". Desired match: " + match[2] + ", starts at: " + start + ".";   
    match = reg.exec(s);
  }
  out += "\nResulting string after statement s.replace(reg, \"$1*$2*\")\n"
         + s.replace(reg, "$1*$2*");
  alert(out);
}

0

Cela le fait effectivement

"jim".match(/[^a-g]m/)
> ["im"]
"jam".match(/[^a-g]m/)
> null

Rechercher et remplacer un exemple

"jim jam".replace(/([^a-g])m/g, "$1M")
> "jiM jam"

Notez que la chaîne de recherche négative doit comporter 1 caractère pour que cela fonctionne.


1
Pas assez. Dans "jim", je ne veux pas du "i"; juste eux". Et les "m".match(/[^a-g]m/)yeilds nullaussi. Je veux aussi le "m" dans ce cas.
Andrew Ensley

-1

/(?![abcdefg])[^abcdefg]m/gi oui c'est un truc.


5
La vérification (?![abcdefg])est totalement redondante, car elle fait [^abcdefg]déjà son travail pour empêcher ces caractères de correspondre.
nhahtdh

2
Cela ne correspondra pas à un «m» sans caractère précédent.
Andrew Ensley
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.