Lookahead und Lookbehind
Lookahead und Lookbehind, gemeinsam auch als Lookaround bezeichnet, sind eine universelle Form der Anker. Zur Erinnerung – ein Anker, wie z.B. der Zeilenanfang "^", markiert die Stelle, an der die Zeile anfängt, ohne selbst ein Zeichen zu sein und ohne im Ergebnissatz zu erscheinen. Er markiert einfach eine Position. Mit Lookahead und Lookbehind stehen Suchmuster zur Verfügung, die das Gleiche tun, sich allerdings universeller einsetzen lassen. Genau wie ein Anker extrahieren sie ebenfalls keinen Text. Sie sind unentbehrlich, um Zeichen auszuschließen, die vor oder nach einer Zeichenkombination nicht stehen dürfen, oder um Zeichen in ein Muster einzufügen, ohne andere Zeichen zu ersetzen.
Betrachten Sie dazu den Beispieltext "adam und eva". Sie möchten in diesem Text alle a finden, die nicht hinter einem b stehen. Das naheliegende Suchmuster ist [^b]a oder "Nicht b gefolgt vom Zeichen a".
[regex]$regex = '[^b]a'
$regex.Matches("adam und eva") | Select-Object -Property Value
> Value
> -----
> da va
Diese Kombination kommt in im Beispieltext drei Mal vor. Allerdings findet der reguläre Ausdruck nur zwei Ergebnisse, nämlich adam und eva. Das erste a in Adam wird nicht erkannt, obwohl es auch kein führendes b hat.
Die Ursache liegt in der Funktionsweise regulärer Ausdrücke begründet. Genau heißt [^b] nämlich nicht "Nicht b" sondern "Alle Zeichen, die nicht b sind". Der Zeilenanfang ist kein Zeichen, sondern eine Positionsbezeichnung, und die wird von diesem Ausdruck nicht erfasst. Mit einem Lookbehind ("zurück schauen") beschreibt man eine Position und löst damit dieses Problem.
Lookahead und Lookbehind werden wie Gruppen in Klammern eingeschlossen. Da sie aber keine Grup-pen sind, werden sie nicht durch $matches extrahiert! Ein Lookahead ("vorwärts schauen") wird mit ?= eingeführt, ein negativer Lookahead mit ?!. Für den Lookbehind zeigt eine öffnende spitze Klammer an, dass die Position vor dem folgenden Ausdruck gesucht wird, also ?<= bzw. ?<!.
|
Lookaround |
Beispiel |
Bedeutung |
|
(?=Muster) |
\d+(?=GB) |
Sucht alle Ziffern, auf die "GB" folgt. "GB" wird nicht extrahiert, sondern "markiert nur die Position" |
|
(?!Muster) |
\d+(?!KB) |
Sucht allen Ziffern, die nicht von KB gefolgt sind. KB wird nicht ex-trahiert. |
|
( ? < = M u s - ter) |
(?<=\d)GB |
Sucht GB, das hinter einer Zahl steht. Die Zahl wird nicht extrahiert. |
|
(?<!Muster) |
(?<!\d) GB |
Sucht GB, das nicht direkt hinter einer Zahl steht. Das vorstehende Zeichen wird nicht extrahiert. |
Um das adam und eva-Problem zu lösen, muss das Suchmuster also (?<!b)a heißen.
[regex]$regex = '(?<!b)a'
$regex.Matches('adam und eva') | Select-Object -Property Value
Value
------
a
a
a
Achten Sie darauf, dass die Ausgabe den vor dem a gefunden Buchstaben nicht mit ausgibt – Lookahead und Lookbehind markieren eine Position (die Position vor dem a) und "treffen" kein Zeichen.
Ein tolles Beispiel für den Einsatz von Lookahead und Lookbehind stammt aus dem Standardwerk "Mastering Regular Expressions". Die Aufgabe lautet, Tausendertrennzeichen in Zahlen einzufügen. Da die Trennzeichen an der jeweils dritten Position eingefügt werden sollen, drängt sich ein Positionsmarker hier geradezu auf. Um drei Zahlen zu beschreiben, wird \d als Platzhalter für Zahlen mit dem Quantifizierer {3} verwendet. Da die Trennzeichen "von hinten" gezählt werden, wird außerdem ein Positionsmarker für das Ende der Zahl benötigt. "\b" beschreibt nicht nur den Anfang und das Ende eines Wortes, sondern auch das von Zahlen. Das Suchmuster sieht also so aus:
$null = '100000' -match '\d{3}\b'
$matches
Name Value
---- -----
0 000
Wir haben jetzt allerdings ein Suchergebnis zurückerhalten, das wir nicht nur nicht benötigen, sondern dass uns auch nicht weiterhilft, denn wir wollen den Fundort per -replace ersetzen. Tun wir das, ersetzen wir das Suchmuster 000 durch das Dezimaltrennzeichen.
'100000' -replace '\d{3}\b','.'
> 100.
Da der Lookahead nicht das Suchmuster selbst beschreibt, sondern nur eine Position angibt, funktioniert hier alles einwandfrei:
'100000' -replace '(?=\d{3}\b)','.'
> 100.000
Ganz korrekt ist das Suchmuster allerdings noch nicht, denn es wird nur nach den ersten drei Stellen das Trennzeichen gesetzt. Um eine beliebige Anzahl von Ziffern mit Trennzeichen zu versehen, müssen die drei Ziffern mit einer runden Klammer gruppiert und mit einem Quantifizierer versehen werden:
'100000' -replace '(?=(\d{3})+\b)','.'
> .100.000
Einen letzten kosmetischen Fehler gilt es zu beheben, denn wenn die Zahl aus einem Vielfachen von drei besteht, wird auch vor der ersten Ziffer ein Punkt. Um das zu verhindern, wird ein Lookahead benötigt, der auf weitere Zahlen vor den Dreiergruppen prüft.
'100000' -replace '(?<=\d)(?=(\d{3})+\b)','.'
> 100.000
Achten Sie darauf, dass die Gruppe im Quantifizierer jetzt wieder durch $matches erfasst wird!
'100000' -match '(?<=\d)(?=(\d{3})+\b)'
$Matches
> Name Value
> ---- -----
> 1 000
> 0
Um das zu verhindern, können Sie wieder auf eine Non-Capturing Group zugreifen.
'100000' -match '(?<=\d)(?=(?:\d{3})+\b)'