Regex-Tester

Muster an echtem Text testen, mit hervorgehobenen Treffern und hartem Stopp bei ausuferndem Backtracking.

g

Warum ein Regex-Tester einen Worker braucht

Die meiste Mechanik dieser Seite existiert wegen eines einzigen Fehlerfalls. Nehmen Sie /(a+)+b/ gegen dreißig a ohne ein b. Die Engine muss entscheiden, wie sie diese dreißig Zeichen zwischen innerem und äußerem Quantor aufteilt, und dafür gibt es 2³⁰ Wege. Jeder wird probiert, bevor der Treffer für unmöglich erklärt werden kann.

Entscheidend ist: ein laufender regulärer Ausdruck lässt sich nicht unterbrechen. JavaScript ist einfädig, ein setTimeout-Wächter kann also erst feuern, wenn der Ausdruck zurückkehrt — bei dieser Eingabe irgendwann nach dem Wärmetod des Universums. Der Tab ist weg.

Deshalb läuft das Muster in einem Web Worker auf eigenem Thread, und die Hauptseite hält einen Timer. Gewinnt der Timer, beendet worker.terminate() den Thread rundheraus. Das ist der einzige Mechanismus, der wirklich funktioniert, und deshalb sagt Ihnen dieser Tester, dass Ihr Muster gefährlich ist, statt zu einem hängenden Tab zu werden.

Das lohnt sich zu verinnerlichen, denn dasselbe Muster in Produktionscode ist ein Denial of Service: Nutzereingaben gegen einen anfälligen Ausdruck geprüft, und eine einzige Anfrage belegt einen CPU-Kern auf unbestimmte Zeit. Diese Fehlerklasse heißt ReDoS.

Die Formen, die man meiden sollte

GefährlichWarumSicherer
(a+)+Verschachtelte Quantoren — exponentiell viele Aufteilungena+
(\w|\s)*Alternative innerhalb eines Quantors, überlappende Zweige[\w\s]*
(\d+)*$Quantifizierte Gruppe mit Anker, der vollständiges Durchsuchen erzwingt\d*$
.*.*=Zwei unbegrenzte Platzhalter, die um denselben Text konkurrieren[^=]*=

Der gemeinsame Nenner ist Mehrdeutigkeit: Es gibt mehr als einen Weg, wie das Muster dieselben Zeichen aufnimmt. Eine Zeichenklasse leistet dasselbe wie eine Alternative, ohne die Mehrdeutigkeit — deshalb ersetzt die sichere Spalte im Wesentlichen nur | durch [].

Gierig, genügsam, und der Fehler, den jeder einmal schreibt

Quantoren sind standardmäßig gierig: .+ nimmt alles, was es bekommt, und gibt Zeichen nur unter Protest zurück. Gegen <b>bold</b>:

MusterTrifft
<.+><b>bold</b> — alles
<.+?><b> — genügsam, hält beim ersten >
<[^>]+><b> — und schneller, weil es nichts zurückzusetzen gibt

Die dritte Form ist die, zu der man greifen sollte. Eine negierte Zeichenklasse kann nicht überschießen, es gibt also kein Backtracking rückgängig zu machen — sie ist zugleich klarer in der Absicht und billiger im Lauf als die genügsame Variante.

lastIndex, und warum jeder zweite Treffer verschwindet

Ein RegExp mit dem Flag g ist zustandsbehaftet. Es führt einen lastIndex mit, der nach jedem exec oder test weiterrückt; ein über mehrere Aufrufe wiederverwendetes Objekt setzt also dort fort, wo es aufgehört hat:

const re = /\d+/g;
re.test('123');  // true,  lastIndex is now 3
re.test('123');  // false! it resumed from index 3

Erzeugen Sie den Ausdruck innerhalb der Schleife, setzen Sie lastIndex = 0 zurück oder nehmen Sie matchAll, das sich darum kümmert. Der Tester oben erzeugt für jeden Lauf ein frisches Objekt, Sie sehen hier also das Verhalten des ersten Aufrufs.

Worin sich JavaScript-Regex unterscheidet

Die Ergebnisse hier stimmen exakt mit Node und Browsercode überein, denn es ist dieselbe Engine. Gegenüber anderen Sprachen sind es diese Unterschiede, über die man stolpert:

  • Keine atomaren Gruppen, keine possessiven Quantoren. PCRE hat (?>...) und a++, um Backtracking rundweg zu verhindern; JavaScript hat beides nicht, weshalb sich ReDoS hier leichter schreibt.
  • Keine Rekursion. Ausbalancierte Klammern lassen sich mit JavaScript-Regex nicht treffen. Ist das die Anforderung, brauchen Sie einen Parser.
  • Lookbehind kam spät. (?<=...) unterstützen aktuelle Browser, älteres Safari nicht — prüfen Sie Ihre Zielplattformen.
  • Gos RE2 ist eine andere Maschine. Garantierte lineare Laufzeit, keine Rückwärtsverweise, kein Lookaround. Wenn Sie sich je gefragt haben, warum Go Funktionen weggelassen hat: Diese Seite ist der Grund.

Verwandt

Ist der Text, gegen den Sie prüfen, JSON, macht ihn zuerst zu formatieren das Muster meist überflüssig — bei strukturierten Daten schlägt ein Parser einen regulären Ausdruck jedes Mal. Geht es ums Planen statt ums Treffen, ist der Cron-Parser das Nachbarwerkzeug.

Fragen zu regulären Ausdrücken

Warum wurde mein Muster nach zwei Sekunden gestoppt?

Weil es katastrophal zurückgesetzt hat. Bestimmte Formen — verschachtelte Quantoren wie (a+)+ oder (\w|\s)* — bringen die Engine dazu, exponentiell viele Wege zu probieren, sodass eine Zeichenkette aus vierzig Zeichen länger dauern kann, als das Universum existiert. Aus JavaScript lässt sich ein laufender regulärer Ausdruck nicht unterbrechen; deshalb läuft das Muster hier in einem Web Worker: Der Worker kann beendet werden, und die Seite bleibt bedienbar. Ein Timeout im Hauptthread griffe erst, wenn der Ausdruck fertig ist — bei solchen Mustern also nie.

Ist das dieselbe Regex-Engine wie in meinem Code?

Es ist die Ihres Browsers, also die JavaScript-Engine — Ergebnisse stimmen daher exakt mit Node und Browsercode überein. Mit PCRE, Python, Go oder Java stimmen sie nicht immer überein. JavaScript kennt keine atomaren Gruppen, keine possessiven Quantoren und keine Rekursion; Lookbehind unterstützen aktuelle Browser, es kam aber spät. Gos RE2 ist ein völlig anderer Entwurf und hat bewusst keine Rückwärtsverweise — genau damit umgeht es das oben beschriebene Backtracking-Problem.

Warum findet mein globaler Ausdruck nur jeden zweiten Treffer?

Ein RegExp-Objekt mit dem Flag g führt einen lastIndex mit, der bei jedem Aufruf weiterrückt; verwendet man dasselbe Objekt über mehrere exec- oder test-Aufrufe hinweg, wird also dort fortgesetzt, wo es aufgehört hat. Das ist einer der zuverlässig verwirrendsten Teile dieser API. Erzeugen Sie den Ausdruck jedes Mal neu, setzen Sie lastIndex vor der Nutzung auf 0, oder nehmen Sie matchAll, das sich darum kümmert.

Was unterscheidet einen gierigen von einem genügsamen Quantor?

Ein gieriger Quantor nimmt so viel wie möglich und gibt Zeichen nur unter Zwang zurück; ein genügsamer (mit angehängtem ?) nimmt so wenig wie möglich und dehnt sich nur aus, wenn es sein muss. Gegen <b>bold</b> trifft das Muster <.+> die ganze Zeichenkette, weil .+ alles verschlingt und gerade so weit zurücksetzt, dass es ein abschließendes > findet. <.+?> trifft nur <b>. Das ist der mit Abstand häufigste Grund, warum ein Muster viel mehr erfasst als beabsichtigt.

Kann ich hier echte Daten einfügen?

Ja. Muster und Text werden an einen Worker innerhalb dieser Seite übergeben und dort ausgewertet. Nichts geht an einen Server — und genau das macht das Abbrechen eines entlaufenen Musters überhaupt möglich, denn es gibt keine Anfrage, auf die zu warten wäre.

Zuletzt geprüft . Etwas veraltet gefunden? Sagen Sie uns Bescheid.