Die regulären Ausdrücke in der .net-Implementierung (wie auch in PERL, Javascript, PHP, Java, …) folgen schon lange nicht mehr den regulären Sprachen. Das geht zwar mit enormen Performanceeinbußen einher, da man dies nur noch per Backtracking statt als endlichen Automaten umsetzen kann, doch auf die Vielfalt an Möglichkeiten will man offensichtlich auch nicht verzichten.
Eine dieser Erweiterung in .net betreffen die Möglichkeit, einen Stack zu verwalten. Diese Möglichkeit wird häufig unter dem Begriff Balanced Groups geführt. Um damit schön herumspielen zu können, werden aber gleich drei verschiedene Konstrukte benötigt.
Capture Groups
Es gibt benannte (named capture group) und - so lange RegexOptions.ExplicitCapture
nicht verwendet wird - auch unbenannte (unnamed capture group) Gruppierungskonstrukte. Es können zwar beide Varianten für die Stackmanipulation herangezogen werden, aufgrund der Fehleranfälligkeit gehe ich aber hier nur auf die benannten Gruppen ein.
Diese folgen dabei dem Muster:
|
|
Der Text, der durch subpattern gefunden wird, landet auf dem Stack (auch Slot genannt) mit dem angegebenen Namen.
Will man mehr als einen Wert auf den Stack legen, kann dies mit der Angabe des gleichen Namens oder mit Wiederholungen hinbekommen.
So lange für subpattern
keine Entsprechung im Text gefunden, wird auch nichts auf den Stack gelegt.
Grob in Pseudocode übersetzt werden kann der Ausdruck mit
|
|
Beispiele
Mit den Ausdruck
und dem Text "Hallo"
erhält man also einen Stack mit dem Namen test
auf dem 4 Werten liegen.
lo |
l |
a |
H |
test |
Die ersten 3 Werte stammen vom Teilausdruck (?<test>\w)+
. Hierbei wird jedes einzelne Zeichen der Reihe nach auf den Stack gelegt. Der letzte Wert stammt vom hinteren Teilausdruck (?<test>\w{2})
und wird ebenfalls auf den Stack mit dem Namen test
gelegt.
Mit Capture Groups kann man also Daten auf die einzelnen Stacks verteilen (push).
vom Stack entfernen
Genauso, wie man Elemente auf den Stack legen kann, kann man diese auch wieder entfernen (pop). Hierzu wird vor den Namen ein Minus-Zeichen gestellt, also
|
|
Der Wert wird nur entfernt, wenn der subpattern
an der Stelle auch passt. Diese Bedingung darf aber auch leer sein und trifft damit immer zu - mit einer Einschränkung: wenn der Stack bereits leer ist, gilt (?<-name>)
als Nicht-Treffer.
Übersetzt werden kann der Ausdruck in etwa so:
Beispiel
Mit dem regulären Ausdruck
|
|
bekommt man beim Text "Hallo"
den Stack
lo |
a |
H |
test |
zu sehen. Nachdem die einzelnen Buchstaben von Hal
auf dem Stack gelegt wurden, wird der letzte davon (l
) wieder entfernt und danach die letzten beiden Buchstaben (lo
) auf den Stack gelegt.
Die Bedingung von (?<-test>)
ist dabei leer und wird daher immer ausgeführt, wenn etwas auf dem Stack liegt. Ist nichts auf dem Stack, führt der pop-Versuch zu einem Nicht-Treffer.
Balanced Groups
Die Stack-Operationen können auch gemischt werden - die eigentlichen Balanced Groups. Hierbei handelt es sich aber nicht nur um eine verkürzte Schreibweise.
|
|
prüft, ob subpattern
vorhanden ist, entfernt den obersten Eintrag des Stacks name2
und legt den gefundenen Teilstring daraufhin auf den Stack mit dem Namen name1
. Die Reihenfolge der Operationen lässt sich umkehren, indem man <name1>
und <-name2>
vertauscht.
|
|
funktioniert dagegen etwas anders.
Statt den durch subpattern
gefundenen Teil des Textes auf den Stack name1
zu legen, wird dort der Teil des Textes, der zwischen dem obersten Eintrag von name2
und subpattern
liegt hinzugefügt.
Beispiel
Wieder wird der Text "Hallo"
verwendet. Mit dem regulären Ausdruck
|
|
bekommt man den Stack
o |
test |
zu sehen.
Dreht man die Reihenfolge um zu
sieht man dagegen
H |
test |
In diesem Fall wird zuerst der letzte Buchstabe auf den Stack gelegt, danach aber gleich wieder entfernt.
Verwendet man allerdings die gemischte Form
erhält man den Stack
all |
test |
all
entspricht dabei dem, was durch .*
selektiert wurde.
Damit lässt sich z.B. ein Klammernpaar finden (auch verschachtelt) und der dazwischenliegende Teil auf den Stack legen.
Group Test
Mit einem Group Test kann man prüfen, ob mindestens ein Wert auf den Stack liegt und entsprechend darauf reagieren. Leider lässt sich der Inhalt nicht testen (zumindest nicht, ohne den Eingabetext entsprechend zu gestalten).
|
|
Es ist also vollkommen egal, welcher Wert auf dem Stack liegt - sobald “etwas” auf dem Stack mit dem angegebenen Namen liegt, wird die yes-Subexpression verwendet, ansonsten die optionale no-Subexpression.
Auch dieser Ausdruck lässt sich in Pseudocode übersetzen:
Falls name
keiner gültigen Gruppe entspricht, wird der Teil des Ausdrucks als Subexpression gewertet (in dem Fall als Literal), d.h. es wird im Text nach den Zeichen gesucht.
Beispiel
Mit dem regulären Ausdruck
|
|
bekommt man beim Text "Hallo"
den Stack
lo |
l |
test |
(?(test)(?<-test>))
prüft hierbei vor dem drauflegen der einzelnen Buchstaben auf den Stack, ob etwas auf dem Stack ist und löscht das oberste Elemente falls das so ist.
Backreference
Mit dem Rückverweis kann man testen, ob als nächstes im zu prüfenden String die Buchstaben vorkommen, die als letztes auf den Stack gelegt wurden. Der “klassische” Rückverweis macht dies anhand eines Index (z.B. \1
). Daneben gibt es aber auch eine benannte Variante
|
|
Beispiel
|
|
\k<test>
findet wirklich nur den Buchstaben, der zuvor auf den Stack gelegt wurde.
Beim Text aaargh
wäre das demnach nur die Folge aaa
.
Ende der Theorie - was macht man nun damit
So unscheinbar die einzelnen Konstrukte auch sein mögen. Was man hier vorfindet ist ein fast vollständiger Kellerautomat (auch pushdown automaton genannt) mit mehreren Kellerspeichern. Das “fast“ bezieht sich auf das Fehlen der Möglichkeit, den Wert der Kellerstapel direkt zu prüfen (ohne auf die Eingabe zuzugreifen)..
Beispiel
Im Wikipedia-Artikel steht ein Beispiel einer kontextfreien Sprache L = {an bn | n>0}
, was soviel bedeutet, dass nach der Angabe beliebig vieler (>0) a
‘s genauso viele b
‘s vorhanden sein müssen (und sonst nichts), damit das Wort der Sprache entspricht. Somit gehört das Wort aabb
zur Sprache L, aabbb
aber nicht.
Das kann man mit regulären Ausdrücken nicht prüfen - wenn Sie denn regulär sind. Mit Hilfe der Balanced Groups funktioniert es aber.
|
|