Regex Lookaheads und Lookbehinds in 5 Minuten verstehen
Lookarounds sind die Funktion, die Reguläre Ausdrücke vom Spielzeug zum Profi-Werkzeug machen. Sie erlauben Bedingungen wie "prüfe, ob ein Wort folgt, ohne es mitzunehmen" oder "finde ein Komma, das nicht von Anführungszeichen umschlossen ist". Klingt unspektakulär, ist aber der Unterschied zwischen einem Regex, der manchmal funktioniert, und einem, der korrekt ist. Dieser Artikel erklärt die vier Lookaround-Varianten, warum sie als Zero-Width Assertions zählen, zeigt fünf praxisnahe Muster und schließt mit Browser-Support, Performance und den häufigsten Stolperfallen. Wer Reguläre Ausdrücke nur als kryptische Bordwerkzeuge kennt, bekommt nach diesem Text eine deutlich praktischere Vorstellung davon, was man damit tatsächlich strukturieren, validieren und extrahieren kann.
Zero-Width Assertions: Warum Lookarounds keine Zeichen konsumieren
Eine normale Regex-Klasse wie \d macht zwei Dinge: sie prüft, ob das aktuelle Zeichen eine Ziffer ist, und sie schiebt die Position der Engine um eine Stelle weiter. Lookarounds dagegen sind Zero-Width Assertions: sie prüfen eine Bedingung an der aktuellen Position, ohne die Position zu verändern. Die Engine bleibt stehen, schaut nach vorne oder zurück, und entscheidet nur ja oder nein. Dadurch lassen sich mehrere Bedingungen auf dieselbe Stelle stapeln, was mit normalen Quantifikatoren unmöglich wäre.
Konkret heißt das: ein Pattern wie (?=.*\d)(?=.*[A-Z]) prüft am Stringanfang, ob irgendwo eine Ziffer und (separat) irgendwo ein Großbuchstabe vorkommt — beide Tests beginnen an derselben Position. Ohne Zero-Width-Assertions müsste man umständlich alle Reihenfolgen ausschreiben (\d.*[A-Z]|[A-Z].*\d), was bei drei oder mehr Bedingungen kombinatorisch explodiert. Genau hier glänzen Lookarounds.
Die vier Varianten im Überblick
Es gibt genau vier Lookaround-Operatoren — kombiniert aus zwei Achsen (vorwärts oder rückwärts) und zwei Polaritäten (positiv oder negativ):
- Positive Lookahead
(?=...): passt, wenn nach der aktuellen Position das Muster...folgt. Beispiel:\d+(?= EUR)matcht die Ziffern in"42 EUR", aberEURbleibt im Reststring. - Negative Lookahead
(?!...): passt, wenn nach der Position das Muster...nicht folgt. Beispiel:foo(?!bar)matchtfooin"foobaz", aber nicht in"foobar". - Positive Lookbehind
(?<=...): passt, wenn vor der Position das Muster...steht. Beispiel:(?<=\$)\d+matcht die Zahl nach einem Dollarzeichen, ohne das Zeichen selbst zu konsumieren. - Negative Lookbehind
(?<!...): passt, wenn vor der Position das Muster...nicht steht. Beispiel:(?<!@)\bfoo\bmatchtfoo, aber nicht@foo— praktisch zum Filtern von Erwähnungen.
Beispiel 1: Passwort-Validierung
Ein klassisches Muster: ein Passwort muss mindestens 10 Zeichen lang sein und mindestens einen Großbuchstaben, eine Ziffer und ein Sonderzeichen enthalten. Ohne Lookaheads müsste man jede Reihenfolge der drei Klassen abdecken. Mit Lookaheads bleibt der Regex kurz und lesbar: ^(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{10,}$. Jedes (?=...) ist ein unabhängiger Test, der am Stringanfang beginnt; der eigentliche Match ist nur .{10,} mit Mindestlänge.
Achtung: Lookaheads sagen nichts über Reihenfolge oder Position der gefundenen Zeichen aus — nur, dass sie irgendwo im Reststring vorkommen. Wenn du "Großbuchstabe muss am Anfang stehen" willst, brauchst du keinen Lookahead, sondern eine normale Anker-Regel. Lookaheads sind für "existiert irgendwo"-Bedingungen ideal, nicht für "steht genau hier".
Beispiel 2: Währungsbeträge extrahieren
Du willst aus einem Fließtext wie "Der Preis ist $19.99, plus $2.50 Versand" nur die Zahlen extrahieren, die hinter einem Dollarzeichen stehen. Die naive Lösung \$\d+\.\d+ matcht $19.99 inklusive Zeichen. Mit positivem Lookbehind: (?<=\$)\d+\.\d+ matcht nur 19.99 und 2.50. Das ist Gold wert, wenn du die Zahlen anschließend in parseFloat() wirfst, ohne den String vorher zu säubern.
Spiegelvariante mit Lookahead, wenn die Währung nachgestellt ist (deutsche Schreibweise "19,99 EUR"): \d+,\d+(?= EUR). Beide Varianten lassen die Engine die Position auf dem Zahlenanfang stehen, der Rest des Strings bleibt ungekürzt — ideal, wenn man im selben Pass mehrere unterschiedliche Token einsammeln will.
Beispiel 3: CamelCase in Wörter zerlegen
Ein praktischer Trick aus dem Entwickleralltag: "getUserProfile" in ["get", "User", "Profile"] zerlegen. Die Idee: an jeder Position trennen, wo ein Kleinbuchstabe (oder Wortanfang) auf einen Großbuchstaben trifft, aber ohne dabei einen Buchstaben zu verlieren. Genau dafür ist ein Split mit Lookaround gemacht: str.split(/(?<=[a-z])(?=[A-Z])/). Der Lookbehind sichert ab, dass links ein Kleinbuchstabe steht, der Lookahead, dass rechts ein Großbuchstabe folgt — und beide bleiben im Ergebnis erhalten.
Mit derselben Logik trennt man auch Akronyme sauber: "XMLHttpRequest" ergibt mit /(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/ die Stücke ["XML", "Http", "Request"]. Wer Slugs oder snake_case-Namen aus CamelCase generieren will, hat damit die saubere Grundlage — der CalcSI Slug-Generator und der Case-Converter nutzen genau diese Patterns intern.
Beispiel 4: CSV mit eingebetteten Anführungszeichen
Klassische Falle: ein CSV-Splitter, der am Komma trennt, scheitert an "Smith, John",42,Berlin, weil das Komma im Namen den Datensatz zerschneidet. Eine pragmatische Regex-Lösung nutzt einen Lookahead, der prüft, dass auf das Komma eine gerade Anzahl Anführungszeichen bis zum Stringende folgt — was bedeutet: das Komma steht außerhalb einer geöffneten Quote. Das Muster lautet sinngemäß: ,(?=(?:[^"]*"[^"]*")*[^"]*$).
Wichtige Einschränkung: für Produktion sollte man richtige CSV-Parser nutzen (RFC 4180 erlaubt mehrzeilige Felder, Escape-Quotes, BOM und mehr — das deckt diese Regex nicht ab). Aber für schnelle Skripte und Log-Auswertungen ist das Pattern unschlagbar. Mit dem Regex-Tester kannst du sofort sehen, an welchen Kommas der Match auslöst — gerade bei zusammengesetzten Lookarounds spart der Live-Test viel Debug-Zeit.
Beispiel 5: Wortgrenzen jenseits von \b
\b ist praktisch, aber Unicode-blind: \bcafé\b matcht in vielen Engines nicht zuverlässig, weil é per Default kein Wortzeichen ist. Mit Lookarounds kann man eine präzise eigene Wortgrenze bauen, etwa: (?<![\p{L}\d])café(?![\p{L}\d]) — das ist eine "keine Buchstaben oder Ziffern davor und dahinter"-Regel mit Unicode-Property \p{L}. Lookarounds machen Wortgrenzen explizit und kontrollierbar.
Ein weiterer typischer Einsatz: ein Wort matchen, das nicht von einem Punkt gefolgt wird, um TLDs auszuschließen — example(?!\.). Oder ein Wort nur dann matchen, wenn ein bestimmtes Vorgängerwort steht: (?<=Dr\. )Müller. Beide Beispiele zeigen, wie Lookarounds Kontextbedingungen ausdrücken, die mit klassischen Quantifikatoren nur über Capture-Gruppen und Nachbearbeitung lösbar wären.
Browser- und Sprach-Support 2026
JavaScript hat Lookaheads seit ECMAScript 3 (1999), aber Lookbehinds kamen erst mit ECMAScript 2018 (ES9). Das hat lange Probleme verursacht, weil Safari sie erst spät unterstützte: Chrome/V8 ab Version 62 (2017), Firefox ab 78 (2020), Safari erst mit Version 16.4 (März 2023). Für variable Länge im Lookbehind — etwa (?<=\w+ ) — gilt dasselbe; in V8 funktioniert das seit Anfang an, in Safari seit 16.4. Stand 2026 ist die Engine-Lage homogen genug, dass Lookbehinds in modernen Web-Apps ohne Sondertests genutzt werden können.
Backends sind entspannter: Python hat Lookaheads und Lookbehinds seit Version 1.5, allerdings nur fixe Länge im Lookbehind (das neuere regex-Modul erlaubt variable Länge). Java unterstützt beide seit JDK 1.4, Lookbehinds mit variabler Länge bis zu einem deklarierten Maximum. PHP nutzt PCRE, das alle vier Varianten plus variable Lookbehinds via \K oder PCRE2 kennt. Perl ist seit jeher die Referenzimplementierung. .NET ist mit Abstand am liberalsten: vollwertige variable-Länge-Lookbehinds out of the box.
Performance: Wann Lookarounds langsam werden
Lookarounds an sich sind nicht teuer — eine konstante Erweiterung der Engine-Logik. Teuer werden sie, wenn das Innenmuster komplex und unbeschränkt ist. Ein Pattern wie (?=.*\d.*\d.*\d) mit drei verschachtelten .*-Quantifikatoren kann auf langen Strings katastrophal backtracken ("catastrophic backtracking"), weil die Engine alle möglichen Aufteilungen durchprobiert. Faustregel: jedes .* oder .+ in einem Lookaround ist ein Warnsignal.
Gegenmittel: spezifischere Klassen statt . nutzen ([^x]* statt .*), atomare Gruppen oder Possessive Quantifier (in PCRE und Java), und vor allem Patterns mit dem Regex-Tester auf realistischen Testdaten profilen. Wenn ein Regex auf 1 MB Text mehr als 100 ms braucht, ist meistens ein Lookaround mit unbegrenztem Innenmuster die Ursache — und ein Rewrite spart oft zwei Größenordnungen. Eine zweite Faustregel: wenn dein Pattern mehr als drei verschachtelte Lookarounds enthält, ist die Wahrscheinlichkeit hoch, dass eine zweistufige Verarbeitung (erst tokenisieren, dann pro Token validieren) leichter wartbar und schneller ist. Reguläre Ausdrücke sind ein scharfes Werkzeug, aber kein Parser — sobald du tief in Lookaround-Türmen versinkst, ist meistens ein anderer Algorithmus die bessere Wahl.
Häufige Fragen
Warum ist mein Regex mit Lookaround so langsam?
Meistens steckt ein .* oder .+ im Inneren des Lookarounds, und die Engine probiert beim Scheitern alle Aufteilungen durch (Catastrophic Backtracking). Ersetz . durch eine spezifische Negativklasse (z. B. [^,] beim CSV-Parsen), verwende possessive Quantoren wenn die Engine sie kennt, und teste mit dem CalcSI Regex-Tester gegen lange Eingaben. Wenn das nicht reicht: das Pattern algorithmisch in zwei Schritte zerlegen (erst grobes Match, dann Validation) ist oft schneller als ein Mega-Regex.
Welche Sprachen unterstützen variable-Länge-Lookbehinds?
.NET unterstützt sie vollständig. JavaScript (V8, SpiderMonkey, JavaScriptCore seit Safari 16.4) ebenfalls. Java erlaubt sie bis zu einer maximal deklarierten Länge. Python ab Python 3.7 mit dem Standardmodul re nur fix; das externe regex-Modul erlaubt variable Länge. PCRE/PHP erlauben fixe Länge plus eine Alternation mit fixen Alternativen — also (?<=ab|cde) geht, aber nicht (?<=\w+). Ruby (Onigmo) hat fixe Länge. Wer portable Patterns schreibt, sollte fixe Länge annehmen.
Was ist der Unterschied zwischen Lookbehind und Capture-Gruppe?
Eine Capture-Gruppe (\$)\d+ konsumiert das $ und stellt es als Gruppe 1 zur Verfügung — der Match-String enthält das Zeichen. Ein Lookbehind (?<=\$)\d+ testet auf das $, lässt es aber außerhalb des Matches. Praktischer Unterschied: bei String.replace oder beim Slicen des Match-Strings bekommst du beim Lookbehind direkt das Sauberprodukt, bei der Capture-Gruppe musst du nachträglich die Gruppe rauswerfen. Lookbehind ist die richtige Wahl, wenn das Vorzeichen Bedingung, aber nicht Bestandteil des Ergebnisses ist.
Kommentare
Die Kommentare werden von Disqus bereitgestellt. Bevor sie geladen werden, brauchen wir deine Einwilligung — Disqus ist ein Drittanbieter und setzt eigene Cookies.