Javascript: echivalent negativ al lookbehind? (Programare, Javascript, Regex, Lookbehind Negativ)

Andrew Ensley a intrebat.
a intrebat.

Există o modalitate de a obține echivalentul unui lookbehind negativ în expresiile regulate din javascript? Am nevoie să potrivesc un șir de caractere care nu începe cu un anumit set de caractere.

Se pare că nu reușesc să găsesc un regex care să facă acest lucru fără să eșueze dacă partea care se potrivește este găsită la începutul șirului. Lookbehind-urile negative par a fi singurul răspuns, dar javascript nu are unul.

EDIT:Acesta este regex-ul pe care aș vrea să funcționeze, dar nu funcționează:

(?<!([abcdefg]))m

Deci, s-ar potrivi cu „m” din „jim” sau „m”, dar nu și cu „jam”.

Comentarii

  • Luați în considerare postarea regex-ului așa cum ar arăta cu un lookbehind negativ; asta ar putea face mai ușor de răspuns. –  > Por Daniel LeCheminant.
  • Cei care doresc să urmărească adoptarea lookbehind etc. vă rugăm să consultați Tabelul de compatibilitate ECMAScript 2016+ –  > Por Wiktor Stribiżew.
  • @WiktorStribiżew : Look-behinds au fost adăugate în specificația din 2018. Chrome le acceptă, dar Firefox încă nu a implementat specificația. –  > Por Lonnie Best.
  • Are nevoie de o privire în spate? Ce ziceți de (?:[^abcdefg]|^)(m)? Cum ar fi "mango".match(/(?:[^abcdefg]|^)(m)/)[1] –  > Por slebetman.
12 răspunsuri
Okku

Aserțiuni Lookbehind a primit acceptate în specificația ECMAScript în 2018.

Utilizare pozitivă a lookbehind:

Utilizare negativă a lookbehind:

Suport pentru platforme:

JBE

Din 2018, Lookbehind Assertions fac parte din specificațiile limbajului ECMAScript.

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

Răspuns înainte de 2018

Deoarece Javascript suportă lookahead negativ, , o modalitate de a face acest lucru este:

  1. inversați șirul de intrare

  2. se potrivește cu un regex inversat

  3. inversați și reformatați corespondențele


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

Exemplul 1:

În urma întrebării lui @andrew-ensley:

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

Ieșiri:

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

Exemplul 2:

Urmează comentariul lui @neaumusic (match max-height dar nu line-height, , simbolul fiind height):

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

Ieșiri:

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

Comentarii

    36

  • problema cu această abordare este că nu funcționează atunci când aveți atât lookahead, cât și lookbehind –  > Por kboom.
  • vă rog să ne arătați un exemplu funcțional, să zicem că vreau să potrivesc max-height dar nu și line-height și vreau doar ca potrivirea să fie height –  > Por neaumusic.
  • Nu ajută dacă sarcina este de a înlocui două simboluri identice consecutive (și nu mai mult de 2) care nu sunt precedate de vreun simbol. ''(?!() va înlocui apostrofele din ''(''test'''''''test de la celălalt capăt, lăsând astfel (''test'NNNtest în loc de (''testNNN'test. –  > Por Wiktor Stribiżew.
Kamil Szot

Să presupunem că doriți să găsiți toate int care nu sunt precedate de unsigned:

Cu suport pentru căutarea negativă:

(?<!unsigned )int

Fără suport pentru căutarea negativă:

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

În principiu, ideea este de a lua n caractere precedente și de a exclude corespondența cu look-ahead negativ, dar și de a găsi corespondența pentru cazurile în care nu există n caractere precedente. (unde n este lungimea de căutare în urmă).

Așadar, regex-ul în cauză:

(?<!([abcdefg]))m

s-ar traduce prin:

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

S-ar putea să fie nevoie să vă jucați cu grupurile de captură pentru a găsi locul exact al șirului care vă interesează sau dacă doriți să înlocuiți o anumită parte cu altceva.

Comentarii

  • Acesta ar trebui să fie răspunsul corect. Vedeți: "So it would match the 'm' in 'jim' or 'm', but not 'jam'".replace(/(j(?!([abcdefg])).|^)m/g, "$1[MATCH]") return "So it would match the 'm' in 'ji[MATCH]' or 'm', but not 'jam'" Este destul de simplu și funcționează! –  > Por Asrail.
  • Genial! Utilizați un look-ahead negativ ca o soluție pentru JavaScript mai vechi! –  > Por Peter Thoeny.
Jason S

Strategia lui Mijoja funcționează pentru cazul tău specific, dar nu și în general:

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

Iată un exemplu în care scopul este să se potrivească un dublu-l, dar nu și dacă este precedat de „ba”. Observați cuvântul „balll” – true lookbehind ar fi trebuit să elimine primii 2 l, dar să se potrivească cu a doua pereche. Dar, prin potrivirea primilor 2 l și apoi ignorarea acestei potriviri ca fiind un fals pozitiv, motorul regexp continuă de la end de la finalul acestei potriviri și ignoră toate caracterele din interiorul falsului pozitiv.

Comentarii

  • Ah, aveți dreptate. Cu toate acestea, acest lucru este mult mai aproape decât am fost înainte. Pot să accept acest lucru până când apare ceva mai bun (cum ar fi javascript care să implementeze efectiv lookbehinds). –  > Por Andrew Ensley.
Mijoja

Utilizați

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

Comentarii

  • Acest lucru nu face nimic: newString va fi întotdeauna egal cu string. De ce atâtea upvotes? –  > Por MikeM.
  • @MikeM: pentru că scopul este pur și simplu de a demonstra o tehnică de potrivire. –  > Por bug.
  • 58

  • @bug. O demonstrație care nu face nimic este un tip ciudat de demonstrație. Răspunsul apare ca și cum ar fi fost doar copiat și lipit fără să se înțeleagă cum funcționează. De aici și lipsa explicației care îl însoțește și imposibilitatea de a demonstra că s-a potrivit ceva. –  > Por MikeM.
  • @MikeM: regula SO este că, dacă răspunde la întrebare așa cum a fost scrisă, , este corectă. OP nu a specificat un caz de utilizare –  > Por bug.
  • Conceptul este corect, dar da, nu este foarte bine demonstrat. Încercați să rulați acest lucru în consola JS… "Jim Jam Momm m".replace(/([abcdefg])?m/g, function($0, $1){ return $1 ? $0 : '[match]'; });. Ar trebui să returneze Ji[match] Jam Mo[match][match] [match]. Dar, de asemenea, rețineți că, așa cum a menționat Jason mai jos, poate eșua în anumite cazuri limită. –  > Por Simon East.
Klemen Slavič

Ați putea defini un grup de necapturare prin negarea setului de caractere:

(?:[^a-g])m

…care s-ar potrivi cu fiecare m NOT precedat de oricare dintre aceste litere.

Comentarii

  • Cred că potrivirea ar acoperi de fapt și caracterul precedent. –  > Por Sam.
  • ^ acest lucru este adevărat. O clasă de caractere reprezintă… un caracter! Tot ceea ce face grupul tău de necapturare este să nu facă acea valoare disponibilă într-un context de înlocuire. Expresia dvs. nu spune „fiecare m care NU este precedat de niciuna dintre aceste litere”, ci spune „fiecare m precedată de un caracter care NU este niciuna dintre aceste litere” –  > Por theflowersoftime.
  • Pentru ca răspunsul să rezolve și problema inițială (începutul șirului), trebuie să includă și o opțiune, astfel încât regexul rezultat ar fi (?:[^a-g]|^)m. A se vedea regex101.com/r/jL1iW6/2 pentru un exemplu de funcționare. –  > Por Johny Skovdal.
  • Utilizarea logicii void nu are întotdeauna efectul dorit. –  > Por GoldBishop.
Fishrock123

Iată cum am obținut str.split(/(?<!^)@/) pentru Node.js 8 (care nu suportă lookbehind):

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

Funcționează? Da (unicode netestat). Neplăcut? Da.

Homer Simpson

urmând ideea lui Mijoja, și pornind de la problemele expuse de JasonS, am avut această idee; am verificat puțin, dar nu sunt sigur de mine, așa că o verificare de către cineva mai expert decât mine în js regex ar fi grozavă 🙂

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)

rezultatul meu personal:

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

principiul este de a apela checker în fiecare punct din șirul de caractere dintre două caractere oarecare, ori de câte ori acea poziție este punctul de plecare al:

— orice subșir de mărimea a ceea ce nu se dorește (aici 'ba', , astfel ..) (în cazul în care această dimensiune este cunoscută; în caz contrar, trebuie să fie mai greu de realizat, poate)

— — sau mai mică decât aceasta, dacă este începutul șirului: ^.?

și, urmând aceasta,

— ceea ce trebuie căutat de fapt (aici 'll').

La fiecare apel al lui checker, , se va efectua un test pentru a verifica dacă valoarea de dinaintea lui ll nu este ceea ce nu ne dorim (!== 'ba'); dacă este cazul, apelăm o altă funcție, iar aceasta va trebui să fie aceasta (doer) care va face modificările pe str, dacă scopul este acesta, sau, mai generic, care va primi în intrare datele necesare pentru a prelucra manual rezultatele scanării lui str.

aici modificăm șirul, astfel că a fost nevoie să păstrăm o urmă a diferenței de lungime pentru a compensa locațiile date de replace, , toate calculate pe str, , care la rândul său nu se modifică niciodată.

Deoarece șirurile primitive sunt imuabile, am fi putut folosi variabila str pentru a stoca rezultatul întregii operații, dar am considerat că exemplul, deja complicat de înlocuiri, ar fi mai clar cu o altă variabilă (str_done).

bănuiesc că din punct de vedere al performanțelor trebuie să fie destul de dur: toate acele înlocuiri inutile ale lui ” în ”, this str.length-1 ori, plus aici înlocuirea manuală de către doer, ceea ce înseamnă o mulțime de felieri… probabil că în acest caz specific de mai sus care ar putea fi grupate, prin tăierea șirului o singură dată în bucăți în jurul locului unde vrem să inserăm [match] și .join()și să o introducem cu [match] însuși.

celălalt lucru este că nu știu cum s-ar descurca în cazuri mai complexe, adică valori complexe pentru lookbehind-ul fals… lungimea fiind poate cea mai problematică dată de obținut.

și, în checker, , în cazul mai multor posibilități de valori nedorite pentru $behind, va trebui să facem un test pe el cu încă un regex (care să fie pus în cache (creat) în afara checker este cel mai bine, pentru a evita ca același obiect regex să fie creat la fiecare apel pentru checker) pentru a ști dacă este sau nu ceea ce căutăm să evităm.

sper că am fost clar; dacă nu, nu ezitați, o să încerc mai bine. 🙂

Traxo

Folosind cazul tău, dacă doriți să înlocuiți m cu ceva, de exemplu, să-l convertești în majusculă M, , poți nega setul în capturarea grupului.

match ([^a-g])m, , înlocuiți cu $1M

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

([^a-g]) se va potrivi cu orice caracter not(^) din a-g și îl va stoca în primul grup de captură, astfel încât să îl puteți accesa cu $1.

Deci, găsim im în jim și îl înlocuim cu iM ceea ce duce la jiM.

Dietrich Baumgarten

După cum am menționat anterior, JavaScript permite acum lookbehinds. În browserele mai vechi aveți încă nevoie de o soluție de rezolvare.

Pun pariu pe capul meu că nu există nicio modalitate de a găsi un regex fără lookbehind care să livreze exact rezultatul. Tot ce puteți face este să lucrați cu grupuri. Să presupunem că aveți un regex (?<!Before)Wanted, , unde Wanted este regex-ul pe care doriți să îl potriviți și Before este regexul care numără ceea ce nu ar trebui să precedă potrivirea. Cel mai bun lucru pe care îl puteți face este să negați regex-ul Before și să folosiți regex-ul NotBefore(Wanted). Rezultatul dorit este primul grup $1.

În cazul dvs. Before=[abcdefg] care este ușor de negat NotBefore=[^abcdefg]. Deci, regex-ul ar fi [^abcdefg](m). Dacă aveți nevoie de poziția lui Wanted, , trebuie să grupați NotBefore astfel încât rezultatul dorit să fie al doilea grup.

În cazul în care se potrivește cu Before au o lungime fixă n, , adică dacă modelul nu conține simboluri repetitive, puteți evita negarea Before și să utilizați expresia regulată (?!Before).{n}(Wanted), , dar tot trebuie să utilizați primul grup sau să folosiți expresia regulată (?!Before)(.{n})(Wanted) și utilizați cel de-al doilea grup. În acest exemplu, modelul Before are de fapt o lungime fixă, și anume 1, așa că se utilizează regex-ul (?![abcdefg]).(m) sau (?![abcdefg])(.)(m). Dacă vă interesează toate corespondențele, adăugați g (a se vedea fragmentul meu de cod):

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 
s = "" + s + """;
  var match = reg.exec(s);
  while(match) {
    var start = match.index + match[1].length;
    out += "
Whole match: " + match[0] + ", starts at: " + match.index
        +  ". Desired match: " + match[2] + ", starts at: " + start + ".";   
    match = reg.exec(s);
  }
  out += "
Resulting string after statement s.replace(reg, "$1*$2*")
"
         + s.replace(reg, "$1*$2*");
  alert(out);
}

Curtis Yallop

Acest lucru este eficient

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

Exemplu de căutare și înlocuire

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

Rețineți că șirul de căutare negativă trebuie să aibă 1 caracter pentru ca acest lucru să funcționeze.

Comentarii

  • Nu este chiar așa. În „jim”, nu vreau „i”, ci doar „m”. Și "m".match(/[^a-g]m/) rezultă null la fel de bine. Vreau „m” și în acest caz. –  > Por Andrew Ensley.
Techsin

/(?![abcdefg])[^abcdefg]m/gida, acesta este un truc.

Comentarii

  • Verificarea (?![abcdefg]) este complet redundantă, deoarece [^abcdefg] își face deja treaba pentru a împiedica potrivirea acestor caractere. –  > Por nhahtdh.
  • Acest lucru nu se va potrivi cu un „m” fără caractere precedente. –  > Por Andrew Ensley.