JavaScript: negatywny odpowiednik lookbehind?


141

Czy istnieje sposób na uzyskanie odpowiednika negatywnego lookbehind w wyrażeniach regularnych javascript? Muszę dopasować ciąg, który nie zaczyna się od określonego zestawu znaków.

Wygląda na to, że nie mogę znaleźć wyrażenia regularnego, które robi to bez błędu, jeśli dopasowana część znajduje się na początku ciągu. Negatywne lookbehinds wydaje się jedyną odpowiedzią, ale javascript go nie ma.

EDYCJA: To jest wyrażenie regularne, nad którym chciałbym pracować, ale tak nie jest:

(?<!([abcdefg]))m

Więc pasowałoby do „m” w „jim” lub „m”, ale nie do „jam”


Rozważ opublikowanie wyrażenia regularnego tak, jak wyglądałoby z negatywnym lookbind; które mogą ułatwić odpowiedź.
Daniel LeCheminant

1
Osoby, które chcą śledzić adopcję lookbehind itp., Zapoznaj się z tabelą kompatybilności ECMAScript 2016+
Wiktor Stribiżew

@ WiktorStribiżew: Look-behinds zostały dodane w specyfikacji 2018. Chrome je obsługuje, ale Firefox nadal nie zaimplementował specyfikacji .
Lonnie Best

Czy to w ogóle wymaga spojrzenia za siebie? O co chodzi (?:[^abcdefg]|^)(m)? Jak w"mango".match(/(?:[^abcdefg]|^)(m)/)[1]
slebetman

Odpowiedzi:


57

Lookbehind Assertions zostało zaakceptowane w specyfikacji ECMAScript w 2018 roku.

Pozytywne użycie lookbehind:

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

Użycie negatywnego lookbehind:

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

Wsparcie platformy:


2
czy jest jakiś polyfill?
Killy,

1
@Killy nie ma tego, o ile wiem, i wątpię, czy kiedykolwiek będzie, ponieważ stworzenie takiego byłoby potencjalnie bardzo niepraktyczne (IE pisanie pełnej implementacji Regex w JS)
Okku

A co z użyciem wtyczki babel, czy można ją skompilować do wersji ES5 lub już obsługiwanej wersji ES6?
Stefan J

1
@IlpoOksanen Myślę, że masz na myśli rozszerzenie implementacji RegEx ... co jest tym, co robią polyfills ... i nie ma nic złego w pisaniu logiki w JavaScript
neaumusic

1
O czym mówisz? Niemal wszystkie propozycje są inspirowane innymi językami i zawsze będą wolały dopasować składnię i semantykę innych języków tam, gdzie ma to sens w kontekście idiomatycznego JS i kompatybilności wstecznej. Myślę, że dość jasno stwierdziłem, że zarówno pozytywne, jak i negatywne lookbehinds zostały zaakceptowane w specyfikacji 2018 w 2017 i podałem linki do źródeł. Ponadto szczegółowo opisałem, które platformy implementują tę specyfikację i jaki jest stan innych platform - i od tego czasu nawet ją aktualizuję. Oczywiście to nie ostatnia funkcja
Regexp,

83

Od 2018 roku Lookbehind Assertions są częścią specyfikacji języka ECMAScript .

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

Odpowiedź przed 2018 rokiem

Ponieważ JavaScript obsługuje negatywne lookahead , jednym ze sposobów jest:

  1. odwrócić ciąg wejściowy

  2. dopasować z odwróconym wyrażeniem regularnym

  3. odwrócić i sformatować dopasowania


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

Przykład 1:

Po pytaniu @ Andrew-Ensleya:

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

Wyjścia:

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

Przykład 2:

Po komentarzu @neaumusic (pasuje, max-heightale nie line-height, token jest height):

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

Wyjścia:

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

36
Problem z tym podejściem polega na tym, że nie działa, gdy masz jednocześnie patrzenie w przód i w tył
kboom

3
czy możesz pokazać działający przykład, powiedzieć, że chcę dopasować, max-heightale nie line-heightchcę i chcę tylko, aby mecz byłheight
neaumusic

Nie pomaga, jeśli zadaniem jest zastąpienie dwóch następujących po sobie identycznych symboli (i nie więcej niż 2), które nie są poprzedzone jakimś symbolem. ''(?!\()zastąpi apostrofy ''(''test'''''''testz drugiego końca, pozostawiając w ten sposób (''test'NNNtestzamiast (''testNNN'test.
Wiktor Stribiżew

61

Załóżmy, że chcesz znaleźć wszystkie intnie poprzedzone przez unsigned:

Z obsługą negatywnego spojrzenia do tyłu:

(?<!unsigned )int

Bez obsługi negatywnego spojrzenia wstecz:

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

Zasadniczo chodzi o to, aby pobrać n poprzedzających znaków i wykluczyć dopasowanie z ujemnym wyprzedzeniem, ale także dopasować przypadki, w których nie ma poprzedzających n znaków. (gdzie n jest długością patrzenia do tyłu).

A więc odnośne wyrażenie regularne:

(?<!([abcdefg]))m

przetłumaczyłoby się na:

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

Być może będziesz musiał bawić się grupami przechwytującymi, aby znaleźć dokładne miejsce struny, która Cię interesuje, lub chcesz zastąpić określoną część czymś innym.


2
To powinna być prawidłowa odpowiedź. Zobacz: "So it would match the 'm' in 'jim' or 'm', but not 'jam'".replace(/(j(?!([abcdefg])).|^)m/g, "$1[MATCH]") zwroty "So it would match the 'm' in 'ji[MATCH]' or 'm', but not 'jam'" To całkiem proste i działa!
Asrail

41

Strategia Mijoja działa w Twoim konkretnym przypadku, ale nie ogólnie:

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

Oto przykład, w którym celem jest dopasowanie podwójnego l, ale nie, jeśli jest ono poprzedzone „ba”. Zwróć uwagę na słowo „balll” - prawdziwe spojrzenie w tył powinno było stłumić pierwsze 2 litry, ale dopasować drugą parę. Ale dopasowując pierwsze 2 l, a następnie ignorując to dopasowanie jako fałszywie dodatni, silnik wyrażeń regularnych kontynuuje pracę od końca tego dopasowania i ignoruje wszystkie znaki z fałszywie dodatnich.


5
Ach, masz rację. Jednak to jest znacznie bliżej niż wcześniej. Mogę to zaakceptować, dopóki nie pojawi się coś lepszego (np. Javascript faktycznie implementujący lookbehinds).
Andrew Ensley,

33

Posługiwać się

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

10
To nic nie da: newStringzawsze będzie równe string. Skąd tyle głosów za?
MikeM,

@MikeM: ponieważ chodzi po prostu o zademonstrowanie dopasowanej techniki.
błąd

57
@pluskwa. Demonstracja, która nic nie robi, jest dziwnym rodzajem demonstracji. Odpowiedź brzmi tak, jakby została po prostu skopiowana i wklejona bez żadnego zrozumienia, jak to działa. Stąd brak towarzyszącego wyjaśnienia i brak wykazania, że ​​cokolwiek zostało dopasowane.
MikeM

2
@MikeM: zasada SO jest taka, że ​​jeśli odpowie na pytanie tak , jak napisano , jest poprawna. OP nie określił przypadku użycia
błąd

7
Pomysł jest poprawny, ale tak, to nie jest zbyt dobrze pokazane. Spróbuj uruchomić to w konsoli JS ... "Jim Jam Momm m".replace(/([abcdefg])?m/g, function($0, $1){ return $1 ? $0 : '[match]'; });. Powinien powrócić Ji[match] Jam Mo[match][match] [match]. Ale pamiętaj również, że jak Jason wspomniał poniżej, może to zawieść w niektórych skrajnych przypadkach.
Simon East,

11

Możesz zdefiniować grupę bez przechwytywania, negując swój zestaw znaków:

(?:[^a-g])m

... które pasowałoby do wszystkich znaków m NIE poprzedzonych którąkolwiek z tych liter.


2
Myślę, że dopasowanie obejmowałoby również poprzedni znak.
Sam,

4
^ to prawda. Klasa postaci reprezentuje ... postać! Wszystko, co robi twoja grupa nie przechwytująca, nie udostępnia tej wartości w kontekście zamiany. Twoje wyrażenie nie mówi „każde m NIE jest poprzedzone żadną z tych liter”, lecz „każde m jest poprzedzone znakiem, który NIE jest żadną z tych liter”
theflowersoftime

5
Aby odpowiedź rozwiązała również pierwotny problem (początek ciągu), musi również zawierać opcję, a więc wynikowe wyrażenie regularne będzie (?:[^a-g]|^)m. Zobacz regex101.com/r/jL1iW6/2, aby zapoznać się z przykładem działania.
Johny Skovdal

Używanie logiki pustki nie zawsze daje pożądany skutek.
GoldBishop

2

Oto, jak osiągnąłem str.split(/(?<!^)@/)dla Node.js 8 (który nie obsługuje lookbehind):

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

Pracuje? Tak (niesprawdzony kod Unicode). Nieprzyjemny? Tak.


1

podążając za ideą Mijoja i czerpiąc z problemów ujawnionych przez JasonS, wpadłem na ten pomysł; trochę sprawdziłem, ale nie jestem pewien siebie, więc weryfikacja przez kogoś bardziej eksperta ode mnie w js regex byłaby świetna :)

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)

moje osobiste wyniki:

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

zasadą jest wywołanie checkerw każdym punkcie ciągu między dowolnymi dwoma znakami, ilekroć ta pozycja jest punktem początkowym:

--- dowolny fragment wielkości co nie jest pożądane (tutaj 'ba', w ten sposób ..) (jeśli rozmiar jest znany, w przeciwnym razie musi to być trudniejsze do zrobienia chyba)

--- --- lub mniejsza, jeśli to początek ciągu: ^.?

a następnie

--- czego faktycznie należy szukać (tutaj 'll').

Przy każdym wywołaniu funkcji checkerbędzie test sprawdzający, czy poprzednia wartość llnie jest tym, czego nie chcemy ( !== 'ba'); jeśli tak jest, wywołujemy inną funkcję i będzie to ta ( doer), która wprowadzi zmiany na str, jeśli celem jest ten lub bardziej ogólnie, wprowadzi dane niezbędne do ręcznego przetworzenia wyniki skanowania str.

tutaj zmieniamy ciąg, więc musieliśmy zachować ślad różnicy długości, aby zrównoważyć lokalizacje podane przez replace, wszystkie obliczone na podstawie str, które same się nigdy nie zmieniają.

ponieważ ciągi pierwotne są niezmienne, moglibyśmy użyć zmiennej strdo przechowywania wyniku całej operacji, ale pomyślałem, że przykład, już skomplikowany przez zamiany, byłby bardziej przejrzysty z inną zmienną ( str_done).

wydaje mi się, że jeśli chodzi o występy, to musi być dość surowe: wszystkie te bezsensowne zamiany this str.length-1czasów `` na '' plus tutaj ręczna wymiana przez wykonawcę, co oznacza dużo krojenia ... prawdopodobnie w tym konkretnym przypadku, który mógłby być zgrupowane, przecinając sznurek tylko raz na kawałki wokół miejsca, w którym chcemy wstawić, [match]i .join()łącząc go ze [match]sobą.

Inną rzeczą jest to, że nie wiem, jak poradziłby sobie z bardziej złożonymi przypadkami, to znaczy ze złożonymi wartościami dla fałszywego lookbehind ... długość jest prawdopodobnie najbardziej problematycznymi danymi do uzyskania.

a checkerw przypadku wielu możliwości niepotrzebnych wartości dla $ za nimi, będziemy musieli wykonać test z jeszcze innym wyrażeniem regularnym ( checkernajlepiej buforowanym (utworzonym) na zewnątrz , aby uniknąć tworzenia tego samego obiektu wyrażenia regularnego na każde wezwanie checker), aby wiedzieć, czy jest to to, czego staramy się unikać.

mam nadzieję, że wyraziłem się jasno; jeśli nie, nie wahaj się, spróbuję lepiej. :)


1

Używając swojej wielkości liter, jeśli chcesz m coś zastąpić , np. Zamienić na wielkie litery M, możesz zanegować zbiór w grupie przechwytywania.

dopasuj ([^a-g])m, zamień na$1M

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

([^a-g])dopasuje dowolny znak nie ( ^) w a-gzakresie i zapisze go w pierwszej grupie przechwytywania, aby można było uzyskać do niego dostęp za pomocą $1.

Więc znaleźć imw jimi zastąpić go iMco skutkuje jiM.


1

Jak wspomniano wcześniej, JavaScript umożliwia teraz lookbehinds. W starszych przeglądarkach nadal potrzebujesz obejścia.

Założę się, że nie ma sposobu, aby znaleźć wyrażenie regularne bez lookbehind, które zapewnia dokładnie wynik. Wszystko, co możesz zrobić, to pracować z grupami. Załóżmy, że masz wyrażenie regularne (?<!Before)Wanted, gdzie Wantedjest wyrażenie regularne, które chcesz dopasować, a Beforejest to wyrażenie regularne obliczające to, co nie powinno poprzedzać dopasowania. Najlepsze, co możesz zrobić, to zanegować wyrażenie regularne Beforei użyć wyrażenia regularnego NotBefore(Wanted). Pożądany wynik to pierwsza grupa $1.

W twoim przypadku, Before=[abcdefg]który łatwo zanegować NotBefore=[^abcdefg]. Więc regex będzie [^abcdefg](m). Jeśli potrzebujesz pozycji Wanted, musisz NotBeforerównież zgrupować , aby pożądany wynik był drugą grupą.

Jeśli dopasowania Beforewzorca mają stałą długość n, to znaczy, jeśli wzorzec nie zawiera powtarzających się tokenów, możesz uniknąć negowania Beforewzorca i użyć wyrażenia regularnego (?!Before).{n}(Wanted), ale nadal musisz użyć pierwszej grupy lub użyć wyrażenia regularnego (?!Before)(.{n})(Wanted)i użyć drugiej Grupa. W tym przykładzie wzorzec Beforema w rzeczywistości stałą długość, a mianowicie 1, więc użyj wyrażenia regularnego (?![abcdefg]).(m)lub (?![abcdefg])(.)(m). Jeśli interesują Cię wszystkie dopasowania, dodaj gflagę, zobacz mój fragment kodu:

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

To skutecznie to robi

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

Wyszukaj i zamień przykład

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

Zwróć uwagę, że ujemny ciąg znaków ostrzegawczych musi mieć 1 znak, aby to zadziałało.


1
Nie do końca. W „jim” nie chcę „i”; tylko im". I "m".match(/[^a-g]m/)yeilds nullrównież. W tym przypadku też chcę mieć „m”.
Andrew Ensley,

-1

/(?![abcdefg])[^abcdefg]m/gi tak to jest sztuczka.


5
Sprawdzenie (?![abcdefg])jest całkowicie zbędne, ponieważ [^abcdefg]już wykonuje swoje zadanie, aby zapobiec dopasowaniu tych znaków.
nhahtdh

2
To nie będzie pasować do litery „m” bez poprzedzających znaków.
Andrew Ensley
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.