Deep JavaScript
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

16 Reguläre Ausdrücke: Lookaround-Assertions anhand von Beispielen



In diesem Kapitel untersuchen wir Lookaround-Assertions in regulären Ausdrücken anhand von Beispielen. Eine Lookaround-Assertion ist nicht erfassend und muss das, was vor oder nach der aktuellen Position in der Eingabezeichenkette kommt, abgleichen (oder nicht abgleichen).

16.1 Referenzblatt: Lookaround-Assertions

Tabelle 4: Übersicht der verfügbaren Lookaround-Assertions.
Muster Name
(?=«muster») Positiver Lookahead ES3
(?!«muster») Negativer Lookahead ES3
(?<=«muster») Positiver Lookbehind ES2018
(?<!«muster») Negativer Lookbehind ES2018

Es gibt vier Lookaround-Assertions (Tab. 4)

16.2 Warnungen für dieses Kapitel

16.3 Beispiel: Festlegen, was vor oder nach einem Treffer kommt (positive Lookaround)

In der folgenden Interaktion extrahieren wir Anführungszeichen-umschlossene Wörter

> 'how "are" "you" doing'.match(/(?<=")[a-z]+(?=")/g)
[ 'are', 'you' ]

Zwei Lookaround-Assertions helfen uns hier

Lookaround-Assertions sind besonders praktisch für .match() im /g-Modus, der ganze Treffer (Erfassungsgruppe 0) zurückgibt. Was auch immer das Muster einer Lookaround-Assertion abgleicht, wird nicht erfasst. Ohne Lookaround-Assertions erscheinen die Anführungszeichen im Ergebnis

> 'how "are" "you" doing'.match(/"([a-z]+)"/g)
[ '"are"', '"you"' ]

16.4 Beispiel: Festlegen, was nicht vor oder nach einem Treffer kommt (negative Lookaround)

Wie können wir das Gegenteil von dem erreichen, was wir im vorherigen Abschnitt getan haben, und alle nicht in Anführungszeichen gesetzten Wörter aus einer Zeichenkette extrahieren?

Unser erster Versuch ist, positive Lookaround-Assertions einfach in negative Lookaround-Assertions umzuwandeln. Leider schlägt das fehl

> 'how "are" "you" doing'.match(/(?<!")[a-z]+(?!")/g)
[ 'how', 'r', 'o', 'doing' ]

Das Problem ist, dass wir Zeichenfolgen extrahieren, die nicht von Anführungszeichen umschlossen sind. Das bedeutet, dass in der Zeichenkette '"are"' das „r“ in der Mitte als nicht in Anführungszeichen gesetzt gilt, da es von einem „a“ vorangestellt und von einem „e“ gefolgt wird.

Wir können dies beheben, indem wir angeben, dass Präfix und Suffix weder ein Anführungszeichen noch ein Buchstabe sein dürfen

> 'how "are" "you" doing'.match(/(?<!["a-z])[a-z]+(?!["a-z])/g)
[ 'how', 'doing' ]

Eine weitere Lösung besteht darin, über \b zu fordern, dass die Zeichenfolge [a-z]+ an Wortgrenzen beginnt und endet

> 'how "are" "you" doing'.match(/(?<!")\b[a-z]+\b(?!")/g)
[ 'how', 'doing' ]

Eine Sache, die bei negativem Lookbehind und negativem Lookahead schön ist, ist, dass sie auch am Anfang oder Ende einer Zeichenkette funktionieren – wie im Beispiel gezeigt.

16.4.1 Es gibt keine einfachen Alternativen zu negativen Lookaround-Assertions

Negative Lookaround-Assertions sind ein mächtiges Werkzeug und normalerweise unmöglich durch andere reguläre Ausdrucksmittel nachzubilden.

Wenn wir sie nicht verwenden wollen, müssen wir normalerweise einen völlig anderen Ansatz wählen. Zum Beispiel könnten wir in diesem Fall die Zeichenkette in (in Anführungszeichen gesetzte und nicht in Anführungszeichen gesetzte) Wörter aufteilen und dann diese filtern.

const str = 'how "are" "you" doing';

const allWords = str.match(/"?[a-z]+"?/g);
const unquotedWords = allWords.filter(
  w => !w.startsWith('"') || !w.endsWith('"'));
assert.deepEqual(unquotedWords, ['how', 'doing']);

Vorteile dieses Ansatzes

16.5 Zwischenspiel: Lookaround-Assertions nach innen zeigen lassen

Alle Beispiele, die wir bisher gesehen haben, haben gemeinsam, dass die Lookaround-Assertions vorschreiben, was vor oder nach dem Treffer kommen muss, ohne diese Zeichen jedoch in den Treffer einzubeziehen.

Die im restlichen Teil dieses Kapitels gezeigten regulären Ausdrücke sind anders: Ihre Lookaround-Assertions zeigen nach innen und schränken ein, was innerhalb des Treffers liegt.

16.6 Beispiel: Zeichenketten finden, die nicht mit 'abc' beginnen

Nehmen wir an, wir möchten alle Zeichenketten abgleichen, die nicht mit 'abc' beginnen. Unser erster Versuch könnte der reguläre Ausdruck /^(?!abc)/ sein.

Das funktioniert gut für .test()

> /^(?!abc)/.test('xyz')
true

.exec() gibt uns jedoch eine leere Zeichenkette zurück

> /^(?!abc)/.exec('xyz')
{ 0: '', index: 0, input: 'xyz', groups: undefined }

Das Problem ist, dass Assertions wie Lookaround-Assertions den abgeglichenen Text nicht erweitern. Das heißt, sie erfassen keine Eingabezeichen, sie stellen nur Anforderungen an die aktuelle Position in der Eingabe.

Daher besteht die Lösung darin, ein Muster hinzuzufügen, das Eingabezeichen erfasst.

> /^(?!abc).*$/.exec('xyz')
{ 0: 'xyz', index: 0, input: 'xyz', groups: undefined }

Wie gewünscht, lehnt dieser neue reguläre Ausdruck Zeichenketten ab, die mit 'abc' vorangestellt sind.

> /^(?!abc).*$/.exec('abc')
null
> /^(?!abc).*$/.exec('abcd')
null

Und er akzeptiert Zeichenketten, die nicht den vollständigen Präfix haben.

> /^(?!abc).*$/.exec('ab')
{ 0: 'ab', index: 0, input: 'ab', groups: undefined }

16.7 Beispiel: Teilzeichenketten finden, die '.mjs' nicht enthalten

Im folgenden Beispiel möchten wir finden

import ··· from '«module-specifier»';

wobei module-specifier nicht auf '.mjs' endet.

const code = `
import {transform} from './util';
import {Person} from './person.mjs';
import {zip} from 'lodash';
`.trim();
assert.deepEqual(
  code.match(/^import .*? from '[^']+(?<!\.mjs)';$/umg),
  [
    "import {transform} from './util';",
    "import {zip} from 'lodash';",
  ]);

Hier fungiert die Lookbehind-Assertion (?<!\.mjs) als Wächter und verhindert, dass der reguläre Ausdruck Zeichenketten abgleicht, die '.mjs' an dieser Stelle enthalten.

16.8 Beispiel: Zeilen mit Kommentaren überspringen

Szenario: Wir möchten Zeilen mit Einstellungen parsen und dabei Kommentare überspringen. Zum Beispiel

const RE_SETTING = /^(?!#)([^:]*):(.*)$/

const lines = [
  'indent: 2', // setting
  '# Trim trailing whitespace:', // comment
  'whitespace: trim', // setting
];
for (const line of lines) {
  const match = RE_SETTING.exec(line);
  if (match) {
    const key = JSON.stringify(match[1]);
    const value = JSON.stringify(match[2]);
    console.log(`KEY: ${key} VALUE: ${value}`);
  }
}

// Output:
// 'KEY: "indent" VALUE: " 2"'
// 'KEY: "whitespace" VALUE: " trim"'

Wie sind wir zum regulären Ausdruck RE_SETTING gekommen?

Wir begannen mit dem folgenden regulären Ausdruck für Einstellungen

/^([^:]*):(.*)$/

Intuitiv besteht er aus einer Sequenz der folgenden Teile

Dieser reguläre Ausdruck lehnt einige Kommentare ab

> /^([^:]*):(.*)$/.test('# Comment')
false

Aber er akzeptiert andere (die Doppelpunkte enthalten).

> /^([^:]*):(.*)$/.test('# Comment:')
true

Wir können das beheben, indem wir (?!#) als Wächter voranstellen. Intuitiv bedeutet dies: „Die aktuelle Position in der Eingabezeichenkette darf nicht vom Zeichen # gefolgt werden.“

Der neue reguläre Ausdruck funktioniert wie gewünscht.

> /^(?!#)([^:]*):(.*)$/.test('# Comment:')
false

16.9 Beispiel: Intelligente Anführungszeichen

Nehmen wir an, wir möchten Paare von geraden doppelten Anführungszeichen in geschwungene Anführungszeichen umwandeln

Das ist unser erster Versuch

> `The words "must" and "should".`.replace(/"(.*)"/g, '“$1”')
'The words “must" and "should”.'

Nur das erste und das letzte Anführungszeichen sind geschwungen. Das Problem hier ist, dass der *-Quantifizierer gierig (so viel wie möglich) übereinstimmt.

Wenn wir ein Fragezeichen nach dem * setzen, stimmt es widerwillig überein.

> `The words "must" and "should".`.replace(/"(.*?)"/g, '“$1”')
'The words “must” and “should”.'

16.9.1 Escaping mit Backslashes unterstützen

Was ist, wenn wir das Escaping von Anführungszeichen mit Backslashes erlauben möchten? Das können wir erreichen, indem wir den Wächter (?<!\\) vor den Anführungszeichen verwenden.

> const regExp = /(?<!\\)"(.*?)(?<!\\)"/g;
> String.raw`\"straight\" and "curly"`.replace(regExp, '“$1”')
'\\"straight\\" and “curly”'

Als Nachbearbeitungsschritt müssten wir immer noch Folgendes tun

.replace(/\\"/g, `"`)

Dieser reguläre Ausdruck kann jedoch fehlschlagen, wenn ein mit Backslash maskierter Backslash vorhanden ist.

> String.raw`Backslash: "\\"`.replace(/(?<!\\)"(.*?)(?<!\\)"/g, '“$1”')
'Backslash: "\\\\"'

Der zweite Backslash verhinderte, dass die Anführungszeichen geschwungen wurden.

Wir können das beheben, indem wir unseren Wächter anspruchsvoller gestalten (?: macht die Gruppe nicht erfassend).

(?<=[^\\](?:\\\\)*)

Der neue Wächter erlaubt Paare von Backslashes vor Anführungszeichen.

> const regExp = /(?<=[^\\](?:\\\\)*)"(.*?)(?<=[^\\](?:\\\\)*)"/g;
> String.raw`Backslash: "\\"`.replace(regExp, '“$1”')
'Backslash: “\\\\”'

Ein Problem bleibt. Dieser Wächter verhindert, dass das erste Anführungszeichen abgeglichen wird, wenn es am Anfang einer Zeichenkette steht.

> const regExp = /(?<=[^\\](?:\\\\)*)"(.*?)(?<=[^\\](?:\\\\)*)"/g;
> `"abc"`.replace(regExp, '“$1”')
'"abc"'

Wir können das beheben, indem wir den ersten Wächter ändern zu: (?<=[^\\](?:\\\\)*|^)

> const regExp = /(?<=[^\\](?:\\\\)*|^)"(.*?)(?<=[^\\](?:\\\\)*)"/g;
> `"abc"`.replace(regExp, '“$1”')
'“abc”'

16.10 Danksagungen

16.11 Weiterführende Lektüre