Javascript: Negatives Aussehen hinter Äquivalent?

141

Gibt es eine Möglichkeit, das Äquivalent eines negativen Aussehens in regulären Javascript-Ausdrücken zu erreichen? Ich muss eine Zeichenfolge finden, die nicht mit einem bestimmten Zeichensatz beginnt.

Es scheint, dass ich keinen regulären Ausdruck finden kann, der dies ohne Fehler tut, wenn der übereinstimmende Teil am Anfang der Zeichenfolge gefunden wird. Negative Lookbehinds scheinen die einzige Antwort zu sein, aber Javascript hat keine.

EDIT: Dies ist der reguläre Ausdruck, an dem ich gerne arbeiten würde, aber nicht:

(?<!([abcdefg]))m

Es würde also mit dem 'm' in 'jim' oder 'm' übereinstimmen, aber nicht mit 'jam'

Andrew Ensley
quelle
Erwägen Sie, den regulären Ausdruck so zu veröffentlichen, wie er mit einem negativen Lookbehind aussehen würde. das kann es einfacher machen zu reagieren.
Daniel LeCheminant
1
Diejenigen, die das Aussehen usw. verfolgen möchten, beziehen sich bitte auf die Kompatibilitätstabelle ECMAScript 2016+
Wiktor Stribiżew
@ WiktorStribiżew: In der Spezifikation 2018 wurden Look-Behinds hinzugefügt. Chrome unterstützt sie, aber Firefox hat die Spezifikation noch nicht implementiert .
Lonnie Best
Braucht das überhaupt einen Blick zurück? Was ist mit (?:[^abcdefg]|^)(m)? Wie in"mango".match(/(?:[^abcdefg]|^)(m)/)[1]
slebetman

Antworten:

57

Lookbehind Assertions wurde angenommen in der ECMAScript - Spezifikation im Jahr 2018.

Positiver Lookbehind-Gebrauch:

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

Negativer Lookbehind-Gebrauch:

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

Plattformunterstützung:

Okku
quelle
2
Gibt es eine Polyfüllung?
Killy
1
@Killy gibt es nicht so weit ich weiß, und ich bezweifle, dass es jemals geben wird, da das Erstellen einer möglicherweise sehr unpraktisch wäre (IE schreibt eine vollständige Regex-Implementierung in JS)
Okku
Was ist mit einem Babel-Plugin? Ist es möglich, auf ES5 kompiliert zu werden oder ES6 bereits zu unterstützen?
Stefan J
1
@IlpoOksanen Ich denke, Sie meinen, die RegEx-Implementierung zu erweitern. Genau das tun Polyfills. Und es ist nichts Falsches daran, die Logik in JavaScript zu schreiben
neaumusic
1
Worüber redest du? Fast alle Vorschläge sind von anderen Sprachen inspiriert und bevorzugen immer die Übereinstimmung von Syntax und Semantik anderer Sprachen, wenn dies im Zusammenhang mit idiomatischer JS und Abwärtskompatibilität sinnvoll ist. Ich glaube, ich habe ganz klar festgestellt, dass 2017 sowohl positive als auch negative Lookbehinds in die Spezifikation 2018 aufgenommen wurden, und ich habe Links zu Quellen angegeben. Darüber hinaus habe ich ausführlich beschrieben, welche Plattformen diese Spezifikation implementieren und wie der Status anderer Plattformen ist - und habe sie seitdem sogar aktualisiert. Natürlich ist das nicht die letzte Regexp-Funktion, die wir sehen werden
Okku
83

Seit 2018 sind Lookbehind-Zusicherungen Teil der ECMAScript-Sprachspezifikation .

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

Antwort vor 2018

Da Javascript negative Lookahead unterstützt , besteht eine Möglichkeit darin:

  1. Umkehren der Eingabezeichenfolge

  2. Übereinstimmung mit einem umgekehrten regulären Ausdruck

  3. Stornieren und formatieren Sie die Übereinstimmungen neu


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

Beispiel 1:

Folgende Frage von @ andrew-ensley:

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

Ausgänge:

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

Beispiel 2:

Folgender @ neaumusic-Kommentar (Übereinstimmung, max-heightaber nicht line-height, das Token ist height):

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

Ausgänge:

max-height true token: height
line-height false token: Ø
JBE
quelle
36
Das Problem mit diesem Ansatz ist, dass es nicht funktioniert, wenn Sie sowohl Lookahead als auch Lookbehind haben
kboom
3
max-heightline-heightheight
Können
Es hilft nicht, wenn die Aufgabe darin besteht, zwei aufeinanderfolgende identische Symbole (und nicht mehr als 2) zu ersetzen, denen kein Symbol vorangestellt ist. ''(?!\()wird die Apostrophe ''(''test'''''''testvom anderen Ende ersetzen und somit (''test'NNNtesteher verlassen als (''testNNN'test.
Wiktor Stribiżew
60

Angenommen, Sie möchten alle finden, denen intnicht Folgendes vorangestellt ist unsigned:

Mit Unterstützung für negative Rückblicke:

(?<!unsigned )int

Ohne Unterstützung für negative Rückblicke:

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

Grundsätzlich besteht die Idee darin, n vorhergehende Zeichen zu erfassen und Übereinstimmungen mit negativem Look-Ahead auszuschließen, aber auch die Fälle abzugleichen, in denen keine vorangestellten n Zeichen vorhanden sind. (wobei n die Länge des Rückblicks ist).

Also der fragliche Regex:

(?<!([abcdefg]))m

würde übersetzen zu:

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

Möglicherweise müssen Sie mit der Erfassung von Gruppen spielen, um die genaue Stelle der Zeichenfolge zu finden, die Sie interessiert, oder Sie möchten einen bestimmten Teil durch etwas anderes ersetzen.

Kamil Szot
quelle
2
Dies sollte die richtige Antwort sein. Siehe: "So it would match the 'm' in 'jim' or 'm', but not 'jam'".replace(/(j(?!([abcdefg])).|^)m/g, "$1[MATCH]") Rückkehr "So it would match the 'm' in 'ji[MATCH]' or 'm', but not 'jam'" Es ist ziemlich einfach und es funktioniert!
Asrail
41

Die Strategie von Mijoja funktioniert für Ihren speziellen Fall, aber nicht allgemein:

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

Hier ist ein Beispiel, bei dem das Ziel darin besteht, einem Doppel-L zu entsprechen, jedoch nicht, wenn "ba" vorangestellt ist. Beachten Sie das Wort "balll" - echtes Aussehen sollte die ersten 2 l unterdrücken, aber mit dem zweiten Paar übereinstimmen. Wenn Sie jedoch die ersten 2 ls abgleichen und diese Übereinstimmung dann als falsch positiv ignorieren, geht die Regexp-Engine vom Ende dieser Übereinstimmung aus und ignoriert alle Zeichen innerhalb des falsch positiven Werts .

Jason S.
quelle
5
Ah, du hast recht. Dies ist jedoch viel näher als zuvor. Ich kann das akzeptieren, bis etwas Besseres eintritt (wie Javascript, das tatsächlich Lookbehinds implementiert).
Andrew Ensley
33

Verwenden

newString = string.replace(/([abcdefg])?m/, function($0,$1){ return $1?$0:'m';});
Mijoja
quelle
10
Das macht nichts: newStringwird immer gleich sein string. Warum so viele positive Stimmen?
MikeM
@ MikeM: weil es einfach darum geht, eine passende Technik zu demonstrieren.
Fehler
57
@Fehler. Eine Demonstration, die nichts tut, ist eine seltsame Art von Demonstration. Die Antwort sieht so aus, als wäre sie nur kopiert und eingefügt worden, ohne zu verstehen, wie sie funktioniert. Daher das Fehlen einer begleitenden Erklärung und das Versäumnis nachzuweisen, dass etwas übereinstimmt.
MikeM
2
@ MikeM: Die Regel von SO ist, wenn es die Frage wie geschrieben beantwortet , ist es richtig. OP hat keinen Anwendungsfall angegeben
Fehler
7
Das Konzept ist korrekt, aber es ist nicht sehr gut vorgeführt. Versuchen Sie, dies in der JS - Konsole ausgeführt wird ... "Jim Jam Momm m".replace(/([abcdefg])?m/g, function($0, $1){ return $1 ? $0 : '[match]'; });. Es sollte zurückkehren Ji[match] Jam Mo[match][match] [match]. Beachten Sie aber auch, dass es, wie Jason unten erwähnte, in bestimmten Randfällen fehlschlagen kann.
Simon East
11

Sie können eine nicht erfassende Gruppe definieren, indem Sie Ihren Zeichensatz negieren:

(?:[^a-g])m

... die mit jedem m NICHT übereinstimmen würden, dem einer dieser Buchstaben vorangestellt ist.

Klemen Slavič
quelle
2
Ich denke, das Match würde tatsächlich auch den vorhergehenden Charakter abdecken.
Sam
4
^ das ist wahr. Eine Charakterklasse repräsentiert ... einen Charakter! Alles, was Ihre nicht erfassende Gruppe tut, ist, diesen Wert nicht in einem Ersetzungskontext verfügbar zu machen. Ihr Ausdruck sagt nicht "jedem m, dem keiner dieser Buchstaben vorangestellt ist", sondern "jedem m, dem ein Zeichen vorangestellt ist, das keiner dieser Buchstaben ist"
theflowersoftime
5
Damit die Antwort auch das ursprüngliche Problem (Anfang der Zeichenfolge) löst, muss sie auch eine Option enthalten, sodass der resultierende reguläre Ausdruck lautet (?:[^a-g]|^)m. Ein Beispiel finden Sie unter regex101.com/r/jL1iW6/2 .
Johny Skovdal
Die Verwendung der Void-Logik hat nicht immer den gewünschten Effekt.
GoldBishop
2

So habe ich es str.split(/(?<!^)@/)für Node.js 8 erreicht (das Lookbehind nicht unterstützt):

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

Funktioniert? Ja (Unicode ungetestet). Unangenehm? Ja.

Fishrock123
quelle
1

Ich folgte der Idee von Mijoja und schöpfte aus den von JasonS aufgedeckten Problemen. Ich habe ein bisschen nachgesehen, bin mir aber nicht sicher, daher wäre eine Überprüfung durch jemanden, der mehr Experten als ich in js regex ist, großartig :)

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)

meine persönliche Ausgabe:

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

Das Prinzip besteht darin, checkeran jedem Punkt in der Zeichenfolge zwischen zwei beliebigen Zeichen aufzurufen , wenn diese Position der Ausgangspunkt von:

--- jede Teilzeichenfolge von der Größe dessen, was nicht erwünscht ist (hier 'ba'also ..) (wenn diese Größe bekannt ist; sonst muss es vielleicht schwieriger sein, dies zu tun)

--- --- oder kleiner als das, wenn es der Anfang der Zeichenfolge ist: ^.?

und danach

--- was ist eigentlich zu suchen (hier 'll').

Bei jedem Aufruf von checkerwird ein Test durchgeführt, um zu überprüfen, ob der vorherige Wert llnicht dem entspricht, was wir nicht wollen ( !== 'ba'). Wenn dies der Fall ist, rufen wir eine andere Funktion auf, und es muss diese ( doer) sein, die die Änderungen an str vornimmt. Wenn der Zweck dies ist, oder allgemeiner, werden die erforderlichen Daten für die manuelle Verarbeitung eingegeben die Ergebnisse des Scannens vonstr .

Hier ändern wir die Zeichenfolge, sodass wir den Längenunterschied verfolgen müssen, um die Positionen zu versetzen, die durch replacealle berechnet werden strund die sich selbst nie ändern.

Da primitive Zeichenfolgen unveränderlich sind, hätten wir die Variable verwenden können str, um das Ergebnis der gesamten Operation zu speichern, aber ich dachte, das Beispiel, das bereits durch die Ersetzungen kompliziert wurde, wäre mit einer anderen Variablen klarer (str_done ) .

Ich denke, dass es in Bezug auf die Leistung ziemlich hart sein muss: all diese sinnlosen Ersetzungen von '' in '', this str.length-1Zeiten, plus hier manuelles Ersetzen durch Macher, was viel Schneiden bedeutet ... wahrscheinlich in diesem speziellen obigen Fall, das könnte gruppiert werden, indem die Schnur nur einmal in Stücke geschnitten wird, um die wir sie einfügen [match]und .join()mit sich [match]selbst verbinden möchten .

Die andere Sache ist, dass ich nicht weiß, wie es mit komplexeren Fällen umgehen würde, dh mit komplexen Werten für das falsche Aussehen ... die Länge ist vielleicht die problematischste Daten, die es zu bekommen gilt.

und checkerim Falle mehrerer Möglichkeiten von unerwünschten Werten für $ Behind müssen wir einen Test mit einem weiteren regulären Ausdruck (der zwischengespeichert (erstellt)) checkerist, am besten durchführen, um zu vermeiden, dass dasselbe reguläre Ausdrucksobjekt erstellt wird bei jedem Aufruf checkerzu wissen, ob es das ist, was wir vermeiden wollen oder nicht.

hoffe ich war klar; Wenn nicht, zögern Sie nicht, ich werde es besser versuchen. :) :)

Homer Simpson
quelle
1

Wenn Sie in Ihrem Fall etwas ersetzen möchten m , konvertieren Sie es beispielsweise in GroßbuchstabenM , können Sie den Satz in der Erfassungsgruppe negieren.

übereinstimmen ([^a-g])m, ersetzen durch$1M

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

([^a-g])stimmt mit jedem Zeichen überein, das nicht ( ^) im a-gBereich liegt, und speichert es in der ersten Erfassungsgruppe, damit Sie mit darauf zugreifen können $1.

So finden wir imin jimund ersetzen sie durch iMin der die Ergebnisse jiM.

Traxo
quelle
1

Wie bereits erwähnt, erlaubt JavaScript jetzt Lookbehinds. In älteren Browsern benötigen Sie noch eine Problemumgehung.

Ich wette, es gibt keine Möglichkeit, einen regulären Ausdruck ohne Lookbehind zu finden, der genau das Ergebnis liefert. Sie können nur mit Gruppen arbeiten. Angenommen, Sie haben eine Regex (?<!Before)Wanted, wobei Wantedes sich um die Regex handelt, die Sie abgleichen möchten, und Beforeum die Regex, die zählt, was nicht vor der Übereinstimmung stehen soll. Das Beste, was Sie tun können, ist, den regulären BeforeAusdruck zu negieren und den regulären Ausdruck zu verwenden NotBefore(Wanted). Das gewünschte Ergebnis ist die erste Gruppe$1 .

In Ihrem Fall Before=[abcdefg]ist das leicht zu negieren NotBefore=[^abcdefg]. Also wäre der reguläre Ausdruck [^abcdefg](m). Wenn Sie die Position von benötigen Wanted, müssen Sie gruppierenNotBefore , damit das gewünschte Ergebnis die zweite Gruppe ist.

Wenn Übereinstimmungen des BeforeMusters eine feste Länge haben n, dh wenn das Muster keine sich wiederholenden Token enthält, können Sie das Negieren des BeforeMusters vermeiden und den regulären Ausdruck verwenden (?!Before).{n}(Wanted), müssen jedoch weiterhin die erste Gruppe oder den regulären Ausdruck (?!Before)(.{n})(Wanted)und den zweiten verwenden Gruppe. In diesem Beispiel hat das Muster Beforetatsächlich eine feste Länge, nämlich 1, verwenden Sie also den regulären Ausdruck (?![abcdefg]).(m)oder (?![abcdefg])(.)(m). Wenn Sie an allen Übereinstimmungen interessiert sind, fügen Sie die gFlagge hinzu, siehe mein Code-Snippet:

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);
}
Dietrich Baumgarten
quelle
0

Dies macht es effektiv

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

Beispiel suchen und ersetzen

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

Beachten Sie, dass die negative Look-Behind-Zeichenfolge 1 Zeichen lang sein muss, damit dies funktioniert.

Curtis Yallop
quelle
1
Nicht ganz. In "jim" möchte ich nicht das "i"; nur sie". Und "m".match(/[^a-g]m/)ihr nullauch. Ich möchte auch in diesem Fall das "m".
Andrew Ensley
-1

/(?![abcdefg])[^abcdefg]m/gi Ja, das ist ein Trick.

Techsin
quelle
5
Die Prüfung (?![abcdefg])ist völlig redundant, da sie [^abcdefg]bereits ihre Aufgabe erfüllt, um zu verhindern, dass diese Zeichen übereinstimmen.
nhahtdh
2
Dies stimmt nicht mit einem 'm' ohne vorhergehende Zeichen überein.
Andrew Ensley