Weitere Anmerkungen zum .NET-Dialekt

(Auszug aus "Reguläre Ausdrücke" von Jeffrey E. F. Friedl)

Einige Merkmale verdienen eine längere Betrachtung als nur gerade einen Punkt in der obigen Liste.

Benannte Klammerausdrücke

In .NET kann man Klammern einen Namen verleihen. Dafür wird die Syntax ˹(?<Name>...)˼ oder ˹(?'Name'...)˼ verwendet (sieh Benannte Unterausdrücke). Beide Varianten bewirken genau das Gleiche. Nach meinem Gefühl wird die Schreibweise mit den spitzen Klammern häufiger verwendet, deshalb benutze ich diese Syntax.

Für Rückwärtsreferenzen innerhalb der Regex auf solche Klammern wird das Konstrukt ˹\k<Name oder ˹\k'Name verwendet.

Nach dem Matching (wenn ein Match-Objekt erzeugt worden ist; das Objekt-Modell von .NET wird unter Das Objekt-Modell in .NET –- Überblick behandelt) sind die von den benannten Klammern eingefangenen Textstücke über die Eigenschaft Groups(Name) des Match-Objekts zugänglich. (Bei C# ist dies Groups[Name].)

Im Ersatztext einer Substitution (siehe den Kasten Spezielle »Dollar-Sequenzen« im Ersatztext) kann man auf den gleichen Text mit der Sequenz ${Name} zugreifen.

Manchmal ist es aber doch ganz nützlich, auch Klammerausdrücke, die eigentlich einen Namen haben, über Nummern ansprechen zu können. Die benannten Klammern werden nach den normalen Klammern nummeriert:

˹(1\w)1(3?<num>\d+)3(2\s+)2˼

Die von ˹\d+˼ erkannte Ziffernreihe in diesem Beispiel kann man also mit Groups("Num") oder mit Groups(3) ansprechen. Das ist ein und dieselbe Gruppe, sie hat nur zwei Namen.

Eine unglückliche Auswirkung

Es wird davon abgeraten, normale und benannte einfangende Klammern zusammen zu verwenden; aber wenn sich das einmal nicht umgehen lässt, sollte man sich der Konsequenzen bewusst sein. Die Reihenfolge der Nummerierung der Klammern spielt dann eine Rolle, wenn die Regex mit Split verwendet wird (siehe Gebrauch von Split mit einfangenden Klammern) oder wenn ›$+‹ im Ersatztext verwendet wird (siehe den Kasten Spezielle »Dollar-Sequenzen« im Ersatztext).

Bedingte Klammerausdrücke

Der Ausdruck im Bedingungsteil eines ˹(? if then|else)˼ (siehe Bedingte reguläre Ausdrücke) kann jede Art von Lookaround, die Nummer einer einfangenden Klammer oder der Name einer benannten Klammer sein. Normaler Text (oder eine Regex) im Bedingungsteil wird automatisch als positives Lookahead interpretiert, d.h., die Bedingung verhält sich, als ob der Text in ˹(?=...)˼ verpackt wäre. Daraus ergibt sich eine Zweideutigkeit: Wenn in der Regex keine Klammer namens ›Num‹ vorkommt (also kein ˹(?<Num>...)˼), dann wird die Bedingung in ˹...(?(Num)then|else)...˼ als positives Lookahead interpretiert, sie wird also zu ˹(?=Num)˼. Wenn aber eine solche Klammer vorkommt, wird deren Inhalt als Bedingung interpretiert – wenn die Klammer namens ›Num‹ gepasst hat, ist der if-Ausdruck ›wahr‹.

Ich kann nur empfehlen, sich nicht auf dieses automatische Lookahead zu verlassen. Wenn Sie ein explizites Lookahead mit ˹(?=...)˼ benutzen, wird die Absicht dahinter für den Leser klarer, und sollte die Semantik der bedingten Klammern geändert werden, gibt es keine unliebsamen Überraschungen.

Optimierte Ausdrücke mit »Compiled«

In den früheren Kapiteln verwendete ich den Begriff »Komplilierung« für den gesamten Vorgang, die Zeichen-Darstellung der Regex zu prüfen und in eine interne Form zu verwandeln, damit sie nachher angewendet werden kann. In der Terminologie von .NET wird dafür der Begriff »Parsing« verwendet. »Kompilieren« bezieht sich dagegen auf zwei Arten der Optimierung nach der Parsing-Phase.

Die Phasen im Detail, geordnet nach steigender Optimierung:

Parsing

Wenn eine Regex in einem Programm zum ersten Mal auftritt, muss sie auf Richtigkeit geprüft und in eine interne Form gebracht werden, die für die spätere Anwendung geeignet ist. Für diesen Vorgang wird in diesem Buch sonst der Begriff »Kompilierung« verwendet (siehe Kompilierung der Regex).

On-the-Fly-Kompilierung

Wenn beim Aufbau der Regex die Option RegexOptions.Compiled angegeben wird, wird die Regex in der Parsing-Phase nicht nur in die normale interne Form übersetzt, sondern einen Schritt weiter in MSIL (Microsoft Intermediate Language) kompiliert. Dies ist eine auf tieferer Ebene angesiedelte Sprache, die wiederum von einem JIT-Compiler (»Just-In-Time«-Compiler) bei der Anwendung der Regex in Maschinencode übersetzt werden kann. Das benötigt Zeit und Speicherplatz, aber der daraus entstehende Code ist schneller. Ob sich der Aufwand lohnt, wird später in diesem Abschnitt untersucht.

Vorkompilierte reguläre Ausdrücke

Ein Regex-Objekt (oder mehrere) kann in eine Assembly verkapselt und auf der Festplatte in einer DLL gespeichert werden (Dynamically Loaded Library, dynamisch ladbare Bibliothek, auch shared library). Damit wird das Objekt auch für andere Programme allgemein nutzbar. Dieser Vorgang wird »in eine Assembly kompilieren« genannt. Mehr dazu erfahren Sie im Abschnitt »Regex-Assemblies«.

Bei RegexOptions.Compiled muss man Aufwand und Ertrag gegeneinander abwägen:

Größe Ohne RegexOptions.Compiled Mit RegexOptions.Compiled
Zeit zum Starten Schneller Langsamer (60 mal)
Speicherverbrauch Niedrig Hoch (ca. 5-15 KB pro Regex)
Matching-Geschwindigkeit Nicht so schnell Bis zu 10 mal schneller

Das normale Parsing (ohne RegexOptions.Compiled), das ohnehin bei jeder neuen Regex durchgeführt werden muss, ist relativ schnell. Sogar auf meiner alten 550-MHz-NT-Kiste habe ich 1500 Parsings pro Sekunde gemessen, und dies sogar bei relativ komplizierten Ausdrücken. Mit RegexOptions.Compiled waren das nur noch etwa 25 reguläre Ausdrücke pro Sekunde, außerdem werden pro Regex etwa 10 KB Speicher verbraucht – noch dazu Speicher, der während der ganzen Programmlaufzeit benutzt wird und nicht für andere Zwecke freigegeben werden kann.

Bei zeitkritischen Schleifen, wo die Verarbeitungsgeschwindigkeit erste Priorität hat, ist es sicher sinnvoll, RegexOptions.Compiled zu benutzen, ganz besonders, wenn die Regex auf große Suchstrings angewendet wird. Bei einfachen regulären Ausdrücken, die nur selten auf relativ kleine Strings angewendet werden, lohnt sich der Aufwand kaum. Bei den vielen Zwischenformen ist die Situation nicht so eindeutig – man muss Pro und Kontra einzeln abwägen und von Fall zu Fall entscheiden.

In bestimmten Fällen kann es sinnvoll sein, eine kompilierte Regex als eigene DLL, als vorkompiliertes Regex-Objekt abzuspeichern. So kann im endgültigen Programm Speicher eingespart werden (der ganze Regex-Compiler wird nicht benötigt), und abgesehen davon wird die Regex schneller geladen (sie ist ja in der DLL schon bereit zur Anwendung). Mehr als Nebenprodukt kann man damit die Regex auch in anderen Programmen verwenden (siehe dazu den Kasten Eigene Regex-Bibliotheken mit Assemblies erzeugen).

Mustersuche von rechts nach links

Es wurde schon oft vorgeschlagen, »rückwärts« nach Mustern zu suchen. Das größte Problem dabei ist eigentlich, sich darüber klar zu werden, was »rückwärts« oder »von rechts nach links« eigentlich heißen soll. Wird die Regex umgedreht? Wird der Suchtext umgedreht? Oder wird die Regex-Maschine ganz normal an jeder Position angesetzt, mit dem Unterschied, dass das Getriebe beim Ende des Suchstrings beginnt und sich bei jedem »Schaltvorgang« eine Position nach links in Richtung Stringanfang verschiebt?

Ein Beispiel, damit wir uns das ganz konkret vorstellen können: ˹\d+˼ wird »rückwärts« auf den String ›123und456‹ angewandt. Bei einer normalen Mustersuche wird ›123‹ gefunden, und irgendwie ist intuitiv klar, dass bei einer Rechts-nach-links-Suche ›456‹ gefunden werden müsste. Aber wenn sich die Regex-Maschine so verhält wie im letzten Satz des letzten Absatzes, geht das doch nicht. Die Regex-Maschine »schaut« nach wie vor nach rechts, aber sie wird vom Getriebe an anderen Positionen angesetzt, von rechts nach links. Der erste Versuch von ˹\d+˼ startet bei ›...456▵‹ und passt nicht. Den zweiten Versuch startet das Getriebe bei ›...45▵6‹: Die Regex-Maschine »sieht« zur Rechten die ›6‹, die sicherlich die Forderung ˹\d+˼ erfüllt. Diesmal wird ein Treffer gefunden, aber es ist nur die ›6‹.

In .NET gibt es die Option RegexOptions.RightToLeft. Wie funktioniert diese »Von rechts nach links«-Suche genau? Die Antwort lautet: »Gute Frage!« Das Verhalten ist nämlich nicht dokumentiert, und aus meinen Tests werde ich nicht richtig schlau. In vielen Fällen (wie in diesem Beispiel mit ›123und456‹) ist das Resultat das intuitiv erwartete (es wird ›456‹ gefunden), aber manchmal wird überhaupt nichts gefunden und manchmal ein Treffer, der nicht zu den anderen Resultaten zu passen scheint.

Je nach Situation kann es sein, dass RegexOptions.RightToLeft genau das macht, was Sie verlangen. Dennoch tun Sie das auf eigene Verantwortung, weil das Verhalten eben nicht definiert ist.

Zweideutigkeiten mit Backslash und Ziffern

Ein Backslash, auf den eine Zahl folgt, ist entweder ein oktales Escape oder eine Rückwärtsreferenz. Welche dieser beiden Interpretationen zutrifft, hängt beim .NET-Paket von der Option RegexOptions.ECMAScript ab. Wenn Sie sich nicht um diese Spitzfindigkeiten kümmern wollen, können Sie für Rückwärtsreferenzen immer die Syntax ˹\k<Zahl benutzen und bei oktalen Escapes immer eine führende Null angeben (z.B. ˹\08˼). Diese zwei Notationen sind immer eindeutig und unabhängig von RegexOptions.ECMAScript.

Wenn RegexOptions.ECMAScript nicht aktiviert ist, sind Backslash-Sequenzen von ˹\1˼ bis ˹\9˼ immer Rückwärtsreferenzen, und Sequenzen, die mit 0 beginnen, sind immer oktale Escapes (also erkennt ˹\012˼ das ASCII-LF-Zeichen). Wenn keiner dieser beiden Fälle auftritt, wird die Sequenz als Rückwärtsreferenz interpretiert, »wenn dies sinnvoll erscheint«, d.h. wenn es mindestens so viele einfangende Klammerpaare in der Regex gibt. Andere Sequenzen im Bereich von \000 und \377 werden als oktale Escapes betrachtet. Also wird ˹\12˼ als Rückwärtsreferenz interpretiert, wenn die Regex zwölf oder mehr einfangende Klammerpaare aufweist.

Diese Regeln ändern sich, wenn die Option RegexOptions.ECMAScript verwendet wird.

ECMAScript-Modus

ECMAScript ist eine standardisierte Version von JavaScript (Anmerkung: ECMA bedeutet »European Computer Manufacturers Association«, eine 1960 gegründete Normenorganisation im Computerbereich.), die eigene Regeln kennt, wie reguläre Ausdrücke interpretiert und angewandt werden sollen. Bei den regulären Ausdrücken von .NET kann man diese Regeln anwenden, indem man bei der Erzeugung des Regex-Objekts die Option RegexOptions.ECMAScript angibt. Wenn Ihnen der Ausdruck ECMAScript nichts sagt und Sie nicht mit diesen Regeln kompatibel sein müssen, können Sie diesen Abschnitt ignorieren.

Wenn die Option RegexOptions.ECMAScript aktiviert ist, gilt Folgendes:

  • Mit der Option RegexOptions.ECMAScript können lediglich die folgenden Optionen kombiniert werden:

    RegexOptions.IgnoreCase
    RegexOptions.Multiline
    RegexOptions.Compiled

  • \w, \d und \s (und \W, \D und \S) beziehen sich nur noch auf die ASCII-Zeichen.
  • Wenn auf einen Backslash Ziffern folgen, werden diese wenn möglich als Rückwärtsreferenzen interpretiert und nicht als oktale Escapes. In ˹(...)\10˼ beispielsweise wird ˹\10˼ als Rückwärtsreferenz auf das erste Klammerpaar interpretiert, gefolgt von einer literalen ›0‹.

  

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema Reguläre Ausdrücke bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:
   

Copyright der deutschen Ausgabe © 2008 by O’Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "Reguläre Ausdrücke" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O’Reilly Verlag GmbH & Co. KG, Balthasarstr. 81, 50670 Köln