Javascript: equivalente negativo di lookbehind?


142

Esiste un modo per ottenere l'equivalente di un aspetto negativo nelle espressioni regolari javascript? Devo abbinare una stringa che non inizia con uno specifico set di caratteri.

Sembra che non riesca a trovare una regex che lo faccia senza fallire se la parte corrispondente viene trovata all'inizio della stringa. I lookbehind negativi sembrano essere l'unica risposta, ma JavaScript non ne ha una.

EDIT: Questa è la regex che mi piacerebbe lavorare, ma non:

(?<!([abcdefg]))m

Quindi corrisponderebbe alla "m" in "jim" o "m", ma non a "jam"


Valuta di pubblicare la regex come sembrerebbe con un aspetto negativo; ciò potrebbe rendere più semplice la risposta.
Daniel LeCheminant,

1
Coloro che vogliono monitorare l'adozione del lookbehind ecc. Si prega di fare riferimento alla tabella di compatibilità ECMAScript 2016+
Wiktor Stribiżew,

@ WiktorStribiżew: Look-behinds sono stati aggiunti nelle specifiche del 2018. Chrome li supporta, ma Firefox non ha ancora implementato le specifiche .
Lonnie Best

Questo ha bisogno di uno sguardo dietro? Che dire (?:[^abcdefg]|^)(m)? Come in"mango".match(/(?:[^abcdefg]|^)(m)/)[1]
slebetman

Risposte:


58

Lookbehind Assertions è stato accettato nelle specifiche ECMAScript nel 2018.

Utilizzo positivo dietro le quinte:

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

Utilizzo negativo del lookbehind:

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

Supporto per la piattaforma:


2
c'è del polyfill?
Killy,

1
@Killy non c'è niente che io sappia, e dubito che ci sarà mai, dato che crearne uno sarebbe potenzialmente molto poco pratico (IE scrivere un'implementazione Regex completa in JS)
Okku,

Che dire dell'utilizzo di un plug-in babel, è possibile essere compilato fino a ES5 o ES6 già supportato?
Stefan J,

1
@IlpoOksanen Penso che intendi estendere l'implementazione di RegEx ... che è ciò che fanno i polyfill .... e non c'è niente di sbagliato nello scrivere la logica in JavaScript
neaumusic

1
Di cosa stai parlando? Quasi tutte le proposte sono ispirate ad altre lingue e preferiranno sempre abbinare la sintassi e la semantica di altre lingue dove ha senso nel contesto di JS idiomatico e retrocompatibilità. Penso di aver affermato chiaramente che lookbehinds sia positivi che negativi sono stati accettati nelle specifiche del 2018 nel 2017 e ho fornito collegamenti a fonti. Inoltre, ho descritto in dettaglio quali piattaforme implementano tali specifiche e qual è lo stato di altre piattaforme - e da allora le ho persino aggiornate. Naturalmente questa non è l'ultima funzionalità di Regexp che vedremo
Okku,

83

Dal 2018, Lookbehind Assertions fanno parte delle specifiche del linguaggio ECMAScript .

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

Risposta pre-2018

Poiché Javascript supporta lookahead negativo , un modo per farlo è:

  1. invertire la stringa di input

  2. abbinare con una regex inversa

  3. invertire e riformattare le partite


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]) : 'Ø');
  });

Esempio 1:

Seguendo la domanda di @ andrew-ensley:

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

Uscite:

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

Esempio 2:

Seguendo il commento di @neaumusic (corrispondenza max-heightma non line-height, l'essere token height):

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

Uscite:

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

36
il problema con questo approccio è che non funziona quando hai sia lookahead che lookbehind
kboom

3
puoi per favore mostrare un esempio funzionante, dire che voglio abbinare max-heightma non line-heighte voglio solo che la partita siaheight
neaumusica

Non è utile se l'attività consiste nel sostituire due simboli identici consecutivi (e non più di 2) che non sono preceduti da alcuni simboli. ''(?!\()sostituirà gli apostrofi ''(''test'''''''testdall'altra estremità, lasciando così (''test'NNNtestanziché (''testNNN'test.
Wiktor Stribiżew,

61

Supponiamo che tu voglia trovare tutto ciò che intnon è preceduto da unsigned:

Con supporto per look-behind negativo:

(?<!unsigned )int

Senza supporto per look-behind negativo:

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

Fondamentalmente l'idea è quella di prendere n caratteri precedenti ed escludere la corrispondenza con un aspetto negativo, ma anche abbinare i casi in cui non vi sono n caratteri precedenti. (dove n è la lunghezza del look-behind).

Quindi la regex in questione:

(?<!([abcdefg]))m

si tradurrebbe in:

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

Potrebbe essere necessario giocare con i gruppi di acquisizione per trovare il punto esatto della stringa che ti interessa o desideri sostituire una parte specifica con qualcos'altro.


2
Questa dovrebbe essere la risposta corretta. Vedi: "So it would match the 'm' in 'jim' or 'm', but not 'jam'".replace(/(j(?!([abcdefg])).|^)m/g, "$1[MATCH]") ritorni "So it would match the 'm' in 'ji[MATCH]' or 'm', but not 'jam'" È abbastanza semplice e funziona!
Asrail,

41

La strategia di Mijoja funziona per il tuo caso specifico ma non in generale:

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

Ecco un esempio in cui l'obiettivo è abbinare una doppia-l ma non se è preceduto da "ba". Nota la parola "balll" - il vero lookbehind avrebbe dovuto sopprimere i primi 2 ma abbinato alla seconda coppia. Ma abbinando i primi 2 e quindi ignorando quella corrispondenza come falso positivo, il motore regexp procede dalla fine di quella partita e ignora tutti i caratteri all'interno del falso positivo.


5
Ah, hai ragione. Tuttavia, questo è molto più vicino di quanto non fossi prima. Posso accettarlo finché non arriva qualcosa di meglio (come javascript che implementa lookbehinds).
Andrew Ensley,

33

Uso

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

10
Questo non fa nulla: newStringsarà sempre uguale string. Perché così tanti voti positivi?
MikeM,

@MikeM: perché il punto è semplicemente dimostrare una tecnica di abbinamento.
bug

57
@bug. Una dimostrazione che non fa nulla è uno strano tipo di dimostrazione. La risposta si presenta come se fosse stata appena copiata e incollata senza alcuna comprensione di come funziona. Quindi la mancanza di spiegazioni di accompagnamento e l'incapacità di dimostrare che qualcosa è stato abbinato.
MikeM,

2
@MikeM: la regola di SO è che, se risponde alla domanda scritta , è corretta. OP non ha specificato un caso d'uso
bug

7
Il concetto è corretto, ma sì, non è molto ben dimostrato. Provare a eseguire questo nella console JS ... "Jim Jam Momm m".replace(/([abcdefg])?m/g, function($0, $1){ return $1 ? $0 : '[match]'; });. Dovrebbe tornare Ji[match] Jam Mo[match][match] [match]. Ma nota anche che, come ha detto Jason di seguito, può fallire in alcuni casi limite.
Simon East,

11

È possibile definire un gruppo che non cattura catturando il set di caratteri:

(?:[^a-g])m

... che corrisponderebbe a ogni m NON preceduto da nessuna di quelle lettere.


2
Penso che la partita riguarderebbe anche il personaggio precedente.
Sam,

4
^ questo è vero. Una classe di personaggi rappresenta ... un personaggio! Tutto ciò che il tuo gruppo non acquisente sta facendo non sta rendendo quel valore disponibile in un contesto di sostituzione. La tua espressione non sta dicendo "ogni m NON preceduto da nessuna di quelle lettere" sta dicendo "ogni m preceduto da un carattere che NON è una di quelle lettere"
theflowersoftime

5
Affinché la risposta risolva anche il problema originale (inizio della stringa), deve anche includere un'opzione, quindi il regex risultante sarebbe (?:[^a-g]|^)m. Vedere regex101.com/r/jL1iW6/2 per un esempio di esecuzione.
Johny Skovdal,

L'uso della logica vuota non ha sempre l'effetto desiderato.
GoldBishop

2

Ecco come ho ottenuto str.split(/(?<!^)@/)per Node.js 8 (che non supporta lookbehind):

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

Lavori? Sì (unicode non testato). Sgradevole? Sì.


1

seguendo l'idea di Mijoja e attingendo ai problemi esposti da JasonS, ho avuto questa idea; ho controllato un po 'ma non sono sicuro di me stesso, quindi una verifica da parte di qualcuno più esperto di me in js regex sarebbe ottima :)

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)

la mia uscita personale:

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

il principio è chiamare checkerin ogni punto della stringa tra due caratteri qualsiasi, ogni volta che quella posizione è il punto iniziale di:

--- qualsiasi sottostringa della dimensione di ciò che non è voluto (qui 'ba', quindi ..) (se tale dimensione è nota; altrimenti dovrebbe essere più difficile da fare forse)

--- --- o più piccolo di quello se è l'inizio della stringa: ^.?

e, in seguito,

--- cosa si deve effettivamente cercare (qui 'll').

Ad ogni chiamata di checker, ci sarà un test per verificare se il valore precedente llnon è quello che non vogliamo ( !== 'ba'); in tal caso, chiamiamo un'altra funzione e dovrà essere questa ( doer) che apporterà le modifiche su str, se lo scopo è questo, o più genericamente, che entrerà in input i dati necessari per l'elaborazione manuale i risultati della scansione di str.

qui cambiamo la stringa, quindi abbiamo bisogno di tenere traccia della differenza di lunghezza per compensare le posizioni fornite da replace, tutte calcolate su str, che di per sé non cambiano mai.

poiché le stringhe primitive sono immutabili, avremmo potuto usare la variabile strper memorizzare il risultato dell'intera operazione, ma ho pensato che l'esempio, già complicato dalle sostituzioni, sarebbe stato più chiaro con un'altra variabile ( str_done).

Immagino che in termini di prestazioni debba essere piuttosto duro: tutte quelle inutili sostituzioni di '' in '', i this str.length-1tempi, più qui la sostituzione manuale di doer, il che significa un sacco di tagli ... probabilmente in questo caso specifico sopra che potrebbe essere raggruppati, tagliando la stringa una sola volta in pezzi attorno a dove vogliamo inserirla [match]e inserendola .join()con [match]sé.

l'altra cosa è che non so come gestirà casi più complessi, ovvero valori complessi per il falso lookbehind ... la lunghezza è forse il dato più problematico da ottenere.

e, in checkercaso di più possibilità di valori non desiderati per $ behind, dovremo fare un test su di esso con l'ennesima regex (essere memorizzati nella cache (creati) all'esterno checkerè la cosa migliore, per evitare che lo stesso oggetto regex venga creato ad ogni richiesta di checker) sapere se è o meno ciò che cerchiamo di evitare.

spero di essere stato chiaro; se non esitare, proverò meglio. :)


1

Utilizzando la custodia, se si desidera sostituire m qualcosa, ad esempio convertirlo in maiuscolo M, è possibile annullare l'insieme nel gruppo di acquisizione.

abbina ([^a-g])m, sostituisci con$1M

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

([^a-g])corrisponderà a qualsiasi carattere non ( ^) a-gnell'intervallo e lo memorizzerà nel primo gruppo di acquisizione, quindi è possibile accedervi con $1.

Così troviamo imin jime sostituirlo con iMche si traduce in jiM.


1

Come accennato in precedenza, JavaScript ora consente lookbehinds. Nei browser meno recenti è ancora necessaria una soluzione alternativa.

Scommetto che la mia testa non c'è modo di trovare una regex senza lookbehind che dia esattamente il risultato. Tutto quello che puoi fare è lavorare con i gruppi. Supponiamo di avere una regex (?<!Before)Wanted, dov'è Wantedla regex che si desidera abbinare ed Beforeè la regex che conta ciò che non dovrebbe precedere la corrispondenza. Il meglio che puoi fare è annullare la regex Beforee usare la regex NotBefore(Wanted). Il risultato desiderato è il primo gruppo $1.

Nel tuo caso, Before=[abcdefg]è facile negarlo NotBefore=[^abcdefg]. Quindi la regex sarebbe [^abcdefg](m). Se hai bisogno della posizione di Wanted, devi NotBeforeanche raggruppare , in modo che il risultato desiderato sia il secondo gruppo.

Se le corrispondenze del Beforemodello hanno una lunghezza fissa n, ovvero se il modello non contiene token ripetitivi, è possibile evitare di annullare il Beforemodello e utilizzare l'espressione regolare (?!Before).{n}(Wanted), ma è comunque necessario utilizzare il primo gruppo o utilizzare l'espressione regolare (?!Before)(.{n})(Wanted)e utilizzare il secondo gruppo. In questo esempio, il modello in Beforerealtà ha una lunghezza fissa, vale a dire 1, quindi usa regex (?![abcdefg]).(m)o (?![abcdefg])(.)(m). Se sei interessato a tutte le partite, aggiungi la gbandiera, vedi il mio frammento di codice:

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

Questo lo fa efficacemente

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

Cerca e sostituisci esempio

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

Nota che la stringa negativa dietro deve essere lunga 1 carattere perché funzioni.


1
Non proprio. In "jim", non voglio la "i"; solo la "m". E "m".match(/[^a-g]m/)yeilds nullpure. Voglio anche la "m" in quel caso.
Andrew Ensley,

-1

/(?![abcdefg])[^abcdefg]m/gi si questo è un trucco.


5
Il controllo (?![abcdefg])è totalmente ridondante, poiché [^abcdefg]fa già il suo lavoro per impedire a quel personaggio di abbinarsi.
nhahtdh,

2
Questo non corrisponderà ad una 'm' senza caratteri precedenti.
Andrew Ensley,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.