01.01.04 – Optionen, konditionales Regex, Assertion

  1. Home
  2. /
  3. 01 – Reguläre Ausdrücke
  4. /
  5. 01.01 – Regex allgemein
  6. /
  7. 01.01.04 – Optionen, konditionales...

Teil 5

Hattet ihr die Befürchtung, dass das folgende Kapitel etwas schwieriger als der vorangegangene wird? Ich will euch beruhigen. Nein, es behandelt ein paar wenige Punkte, die etwas komplexer sind, aber im Ganzen sind viele Elemente recht einfach und zudem nur sehr selten zu verwenden. Wer die vorangegangenen Kapitel verstanden hat, kann schon recht effektive und mächtige Regexe bauen.

Einige Begriffe sind in englisch: leider fehlen mir vernünftige deutsche Übersetzungen. Ich hoffe, dass die Darstellung aber dennoch verständlich geblieben ist.

5.1. Assertion

Was soll eine Assertion sein? Die Übersetzung aus dem Englischen ergibt so etwas wie „Behauptung“ oder „Aussage“. In der Computersprache wird es mit „Aussage“ noch am besten übersetzt. Sucht man dann diesen Begriff im Friedl, hat man Pech: Fehlanzeige! Dort wird diese Art der Regexe mit „Lookahead“ bezeichnet. Na ja, wollen wir uns mal anschauen, was sich dahinter verbirgt.

Mit einer Assertion kann getestet werden, ob eine bestimmte Zeichenfolge der gefundenen Stelle vorangeht oder folgt. Na, das ist ja wenig erquicklich, denn das konnten wir auch schon zuvor. Aber: mit der Assertion wird geprüft, ohne dass die geprüfte Zeichenkette in den gefundenen String übernommen wird; sie „frisst“ die Zeichen nicht.

Am besten sieht man sich dazu ein Beispiel an:

Wir wollen nur die Buchstabenfolge ‚foo‘ finden, die von der Zeichenfolge ‚bar‘ gefolgt wird, ohne aber das ‚bar‘ in unserem Suchergebnis wiederzufinden. Normalerweise hätten wir einfach „foobar“ als Regex angegeben. Dann aber findet das Regex ‚foobar‘.

Setzt man dagegen eine Assertion „(?=“ ein, so sieht das Regex wie folgt aus:

„foo(?=bar)“. Das Ergebnis lautet nun ‚foo‘.

Natürlich kann man negative Lookaheads oder Assertions definieren. Dazu verwendet man „(?!“.

Wieder auf unser wenig sinnreiches Beispiel angewendet, ergibt dies bei dem Regex „foo(?!bar)“, dass nur ‚foo‘ gefunden wird, wenn kein ‚bar‘ folgt. Außerdem wird im gefundenen String nur das ‚foo‘ abgelegt. Wird also das Regex auf den Text ‚foolish‘ verwendet, dann lautet das Ergebnis ‚foo‘.

Aber Achtung, da gibt es eine Falle. Man könnte bei dem Regex „(?!foo)bar“ meinen, es würde die Zeichenkette ‚bar‘ nur dann finden, wenn dem nicht ‚foo‘ vorangeht. Falsch, sie findet jedes Vorkommen der Buchstaben ‚bar‘, egal was davor steht! Denn, da diese Assertion eine Lookahead, also nach vorn schauende ist, sucht sie ab der Fundstelle des ‚bar‘ nach vorn, ob dort ‚foo‘ steht. Nun, das steht dort nie, denn da steht ‚bar‘. Logisch, oder?

Was wir brauchen, ist eine Lookbehind-Assertion! Klar gibt es das: „(?<=“ für die positive Variante und „(?<!“ für die negative Version.

Beispiel:

„(?<!foo)bar“ findet das ‚bar‘ nun nur dann, wenn kein ‚foo‘ davor steht.

„(?<=foo)bar“ dagegen findet ‚bar‘ nur, wenn ‚foo‘ davor erscheint.

Selbstverständlich kann man auch Alternativen in den Assertions verwenden. Als Beispiel: „(?<=Einig|Möglich)keit“ findet die Zeichenkette ‚keit‘, wenn entweder ‚Einig‘ oder ‚Möglich‘ davor steht. (‚keit‘ wird auch gefunden, wenn dort UnMöglich‘ vor steht. Ich nehme an, dass dies aber allen klar ist, oder?)

Eine Bedingung ist an den Suchbegriffen im Assertion allerdings zu stellen: ihre Länge muss definiert sein, das heißt, es können keine Quantifizierer verwendet werden! Die Assertion „(?<=\d+,)\d\d“ führt zu einem Laufzeitfehler.

Bei Alternativen, die man im Assertion prüfen kann, dürfen die einzelnen alternativen Elemente sehr wohl unterschiedlich sein, aber sie müssen eine bekannte Länge haben.

Die definierte Länge bezieht sich auf die oberste Ebene der Prüfung im Assertion. Ja, ich weiß: „Was ist die oberste Ebene?“ werdet ihr fragen. Ich versuche es erst gar nicht, ich zeige gleich ein Beispiel:

„(?<=ab(c|de))“ ist verboten, „(?<=abc|abde)“ dagegen nicht. Durch Öffnen einer weiteren Klammerebene im ersten Assertion verlasse ich die oberste Ebene; damit wird die Assertion für die Regex-Maschine unberechenbar.

Schließlich sollte zumindest bekannt sein, dass man mehrere Assertions hintereinander verwenden darf, aber auch ineinander verschachteln kann.

Beispiel dafür: „(?<=\d{2},\d{2})(?<!00,00)\s+Ausgabe“ findet ‚ Ausgabe‘ nur dann, wenn davor jede beliebige Summe steht, die nicht auf ‚00,00‘ lautet.

Oder: „(?<=(?<!un)möglich)keit“ findet das Wort ‚keit‘ nur, wenn davor ‚möglich‘ steht, aber nicht ‚unmöglich‘.

5.2. Backreference

Wieder mal ein englisches Wort. „Rückbezug“ ist ein passender Begriff dafür, denn was wir im Regex damit machen wollen, ist ein solcher Rückbezug. Aber beginnen wir erst einmal vorn!

In einer der letzten Abschnitte hatten wir etwas von Subpatterns gelesen. Das waren gefundene Zeichenketten, die vom Regex in Variablen gespeichert werden. Ich schrieb da „… werden in eine temporäre Variable für spätere Zwecke zwischengespeichert…“. Nun, einen dieser Zwecke haben wir jetzt vor uns.

Betrachten wir einfach gleich das Regex: „(Rede|Kehrt) und \1wendung)“ Das Regex soll entweder ‚Rede‘ oder ‚Kehrt‘ finden. Dieser Wert wird in das erste Subpattern (wir erinnern uns: die erste öffnende Klammer) gelegt. Danach soll das Wort ‚und‘ folgen. Nun steht im Regex „\1“. Dies heißt nichts anderes als: verwende den Inhalt des ersten Subpatterns zum Suchen! Da es von ‚wendung ‚ gefolgt wird, soll also entweder ‚Redewendung‘ oder ‚Kehrtwendung‘ an dieser Stelle gefunden werden, je nachdem, welches der beiden Wörter weiter vorn gefunden wurden. Das Regex findet also nur ‚Rede und Redewendung‘ oder ‚Kehrt und Kehrtwendung‘, aber niemals ‚Rede und Kehrtwendung‘.

Der Rückbezug darf nicht in der Klammer, dem Subpattern stehen, auf den es sich bezieht. Dies führt zu keinem Ergebnis: „(a\1)“ führt nie zu einem positiven Ergebnis. Allerdings kann diese wieder verwendet werden, wenn sich ein Quantifizierer dahinter befindet: „(da|de\1)+“ Das findet ‚dadadada‘ oder ‚dadeda‘ oder auch ‚dadedadadada‘. Ganz schön verwirrend, oder?

5.3. Konditionale Reguläre Ausdrücke

Selten genutzt, aber dennoch interessant sind konditionale Regexe. Sie folgen dem Prinzip „Wenn Muster A gefunden, dann suche nach Muster B; wenn nicht, dann nach Muster C“.

Die Syntax ist eigentlich recht einfach: „(?(Bedingung)Ja-Muster|Nein-Muster)“

oder „(?(Bedingung)Ja-Muster)“

Eine Bedingung gilt es aber zu erfüllen: wenn die Bedingung nicht eine Folge von Ziffern ist, dann muss es sich um eine Assertion handeln.

In welchen Situationen lässt sich denn ein solches konditionales Regex verwenden? Nehmen wir mal an, wir brauchen aus einem Text Tag, Monat und Jahr, wissen aber aus unerfindlichen Gründen nicht, ob das Datum im deutschen TT.MM.JJJJ oder englischen TT, MMM JJJJ Format geschrieben ist. Was wir wissen ist, dass am Zeilenanfang ‚Date‘ gefolgt vom englischen Format oder ‚Datum‘ gefolgt vom deutschen Format dort steht und die Information mit der Zeile endet.

Nun müssen wir also nur das Regex veranlassen, das englische Muster zu suchen, wenn davor ‚Date‘ steht, ansonsten soll es das deutsche Muster suchen.

„(?(?=^Date)Date:\s(\d+),\s([A-Za-z]{3})\s(\d{4})$
|Datum:\s(\d{2}\.)(\d{2}\.)(\d{4})$)“

[Anm.: Das Regex wird aus Layout-Gründen umgebrochen. Tatsächlich muss sich alles in einer Zeile befinden!]

Ich hätte die Suche nach dem Doppelpunkt und dem folgenden Leerzeichen auch in die Assertion nehmen können, war mir aber nicht sicher, ob das nicht ein Problem bei der Speicherung der Teilinformationen in die Subpattern hervorruft.

Eingangs hatte ich schon angedeutet, dass als Bedingung eine Assertion stehen muss oder eine Ziffer. Die Ziffer ist aber in diesem Fall kein Zeichen, das literal gesucht wird. Vielmehr muss es ein Rückbezug sein, ein Backreference auf ein zuvor gefundenes Muster, das in einem Subpattern abgelegt wurde. Wie haben wir das zu verstehen?

Beispiel: in einem Text erscheint in der ersten Zeile ein Name nach dem Eintrag ‚Name‘. Der Name kann je nach Text variieren. Im weiteren Verlauf des Textes erscheint der Name erneut, aber mit einem Attribut, das wir benötigen, sagen wir einfach mal die Schuhgröße.

„Name:\s*(.*)?$“ sollte den Namen finden. Danach kommt etwas, was uns nicht stört, aber irgendwo da drin gibt es dann wieder den Namen, gefolgt von einem Doppelpunkt und folgend die Schuhgröße:

„.*?(?(1):\s*(\d+))“

Insgesamt also:

„Name:\s*(.*)?$.*?(?(1):\s*(\d+))“

Versucht es mal mit der folgenden Zeichenkette:

‚Name: Lieschen Mueller

Bladibla

Lieschen Mueller: 43′

5.4. Optionen, Modifikatoren

Nachdem wir nun eine Vielzahl von „Vokabeln“ für das Regexische lernen mussten, kommen wir zu den Modifikatoren. Wie der Name vermuten lässt, modifizieren sie was. Und was? Nun, die Regex-Zeichen werden bei Gebrauch der Modifikatoren „modifiziert“. Ich höre euch aufstöhnen: kaum hat man ein paar von den vielen Zeichen gelernt und kann wenigstens rudimentäre Regexe bauen, da wird deren Bedeutung durcheinander gewirbelt. Aber keine Sorge, so schlimm wird es nicht. Wir werden uns auf ein paar wenige beschränken. Betrachten wir also folgende Auswahl:

i für Caseless
Damit wird die Regex-Maschine veranlasst, Groß- und Kleinschreibung zu ignorieren. Egal, wie es im Regex steht, es wird in jeglicher Schreibweise gefunden.

m für Multiline
Standardmäßig betrachtet die Regex-Maschine die zu untersuchende Zeichenfolge als eine einzige Zeile, gleichgültig, ob im Text Newline-Character stehen (\n). Das ^-Zeichen passt wirklich nur am Anfang und das $-Zeichen am Ende der Zeichenfolge oder aber an einem abschließenden Newline-Character. Probiert es mal im Regex-Tester aus: schaltet die Option Multiline aus. Dann gebt folgenden Text mit Zeilenumbrüchen ein:

‚Dies ist ein Test, der

als Test

am Ende steht‘

und als Regex „Test$“. Es wird nichts gefunden. Schaltet die Option wieder ein und das Wort ‚Test‘ wird gefunden.

Mit eingeschalteter Option werden tatsächlich alle Newline-Character beachtet; die Zeichenfolge wird aus mehreren Zeilen bestehend angesehen. Wichtig, wenn wir mal einen ganzen Mailtext mit einem Regex prüfen.

s für DotAll
Der Punkt steht, wie wir zuvor gesehen haben für alle beliebigen Zeichen, außer dem Zeilenumbruch, also dem Newline. Wird die Option eingeschaltet, so findet der Punkt auch dieses Zeichen. Allerdings ist das auch nicht die ganze Wahrheit: das Newline wird auch von so genannten negierten Zeichenklassen gefunden. Lautet also ein Regex „[^x]“, so werden alle Zeichen, auch Newlines gefunden, die kein ‚x‘ sind!

x für Extended
Wenn diese Option gesetzt ist, werden alle Whitespace-Zeichen ignoriert, es sei denn, sie werden maskiert. Dies geschieht, wie wir im ersten Teil gelernt haben mit einem vorangestellten Backslash „\ „. Oder aber man sucht das Whitespace mit „\s“.

Weiterhin werden Whitespaces oder Leerzeichen auch dann noch gefunden, wenn sie innerhalb einer Charakterklasse definiert werden: „[ ]“ Diese Option ermöglicht das Einfügen von Kommentaren in das Regex, indem die Kommentare hinter nicht maskierte „#“-Zeichen gesetzt werden.

A für Anchored
Wenn dieser Modifikator gesetzt wird, wird das Regex gezwungen, ab dem Anfang der Zeichenkette zu suchen. Dies lässt sich natürlich auch durch das korrekte Verwenden des Metazeichens „\A“ erzielen. In Perl gibt es den Modifizierer A nicht, daher ist das Metazeichen „\A“ dort die einzige Möglichkeit.

D für Dollar_Endonly
Mit diesem Modifizierer findet das Metazeichen „$“ nur noch das Ende der Zeichenkette, unabhängig davon, ob zuvor noch weitere Zeilenumbrüche („\n“) existieren. Ohne diesen Modifizierer matcht „$“ unmittelbar vor dem letzten Zeichen, wenn dieses ein Newline („\n“) ist. Er wird allerdings ignoriert, wenn zusätzlich der Modifizierer „m“ eingeschaltet wird. In Perl gibt es den Modizierer nicht.

U für Ungreedy
Hiermit wird die Gierigkeit von Quantifizierern umgekehrt: wenn er gesetzt ist, sind alle Quantifizierer im Regex standardmäßig nicht gierig. Sie werden nur durch ein nachfolgendes „?“ gierig. Auch hierfür gibt es bei Perl kein Äquivalent.

u für UTF8
Der Modifizierer ist ebenfalls inkompatibel zu Perl: ist er eingeschaltet, dann wird der Suchstring als UTF-8 behandelt.

X für Extra
Noch ein Modifizierer, der in Perl keine Verwendung findet. Jedes Zeichen, das keine besondere Metazeichenbedeutung hat, aber mit einem Backslash maskiert ist, führt zu einem Fehler. Normalerweise wird jedes Nicht-Metazeichen, das einen vorhergehenden Backslash hat, literal gesucht. Ich habe es sogar im Text als unschädlich bezeichnet, ein Backslash zuviel zu setzen, da das Zeichen dennoch gesucht wird. Wird X als Modifizierer gesetzt, führt dies zu einem Fehler.

e für Evaluate
Eigentlich habe ich erfunden, dass dieser Modifizierer für „Evaluate“ steht; es gibt kein PCRE-Synonym dafür. Die Modifizierer steht, soweit ich weiß, nur den PHP-Usern zur Verfügung. Er kann nur in der Funktion preg_replace verwendet werden. Ist er eingeschaltet, so werden Substitutionen und Backreferences ganz normal durchgeführt, allerdings dann als PHP-Code interpretiert und das Ergebnis dann zum Ersetzen verwendet. Schwierig zu verstehen? Ok, hier ein Beispiel: Das Regex sei „(foo)(.*?)(bar)“. Im Subpattern 2 sind also nicht bekannte Zeichen. Diese wollen wir in Großbuchstaben wandeln und verwenden dazu als Beispielzeichenkette „foosibar“:

<?php
$ergebnis=preg_replace(„/(foo)(.*?)(bar)/e“, „‚\1‘.strtoupper(‚\2‘).’\3′“, „foosibar“);
echo $ergebnis;
?>

Das Ergebnis lautet: „fooSIbar“ (Ja, ich weiß schon: kein sehr sinnvolles Beispiel *g* Ich hoffe, man versteht trotzdem, was „e“ macht).

S für Speed
Schon wieder eine Erfindung von mir: es gibt kein PCRE_SPEED. Und wieder freuen sich nur die PHP-User, soweit ich das überblicke. Wenn ein Suchpattern mehrfach verwendet werden soll, so macht es für die Regex-Maschine Sinn, es etwas länger zu analysieren und so die Match-Geschwindigkeit (Speed) zu erhöhen. Dieser Modifizierer veranlasst die Extra-Analyse. Sinn macht das natürlich nur, wenn im Suchpattern nicht schon Verankerungen wie Zeilen- oder Zeichenkettenanfang eingetragen sind.

Wer in den Regex-Tester schaut und dort Optionen aufruft, wird noch ein paar weitere Optionen bzw. Modifikatoren finden. Ich werde sie hier nicht weiter erläutern. Es handelt sich um spezielle Einstellungen, deren Auswirkungen auch in geeigneter Literatur nachlesbar ist und zum Grundverständnis von Regexen nur bedingt beitragen. Die hier aufgeführten werden uns in aller Regel reichen.

Wie aber schaltet man diese Optionen ein? Nichts einfacher als das: man trägt den Buchstaben zwischen „(?“ und „)“ ein. Also: „(?i)“ schaltet die „Caseless“ ein. Man kann die Optionen auch kombinieren: „(?im)“ heißt ‚Caseless, Multiline‘. Und noch mehr: man kann die Optionen ein- und ausschalten. „(?im-sx)“ schaltet Caseless und Multiline ein, aber DotAll und Extended aus. Erscheint ein Buchstabe sowohl vor dem „-„-Zeichen als auch dahinter, dann wird die Option ausgeschaltet.

Es ist im Grunde auch egal, wo die Option geschaltet wird: es kann am Anfang geschehen, aber auch mitten im Regex.

„(?i)Test“ ist gleichbedeutend mit „Te(?i)st“. Wird die gleiche Option mehrfach auf der obersten Suchebene gesetzt, so gewinnt die am weitesten rechts stehende Einstellung. Aber es ist so nicht sehr übersichtlich und daher soll man diese Optionen an den Anfang des Regex stellen.

Ich schrieb „…im Grunde…“. Hmmm, das impliziert ja schon wieder eine Ausnahme! Und so ist es auch: erscheint eine Option innerhalb eines Subpatterns, so gilt es nur für das Subpattern. „(a(?i)b)c“ findet ‚abc‘ aber auch ‚aBc‘.

Mal was zum Rätseln:

„(a(?i)b|c)“ sei das Regex. Findet das nun ein ‚C‘ oder nur ‚c‘? Unstrittig ist sicherlich, dass ‚ab‘ oder ‚aB‘ gefunden werden.

Das Setzen der Modifikatoren kann bei den Programmiersprachen unterschiedlich erfolgen. Lest bitte die entsprechenden Infos dort.

5.5. Besonderes

Kommen wir zu ein paar Besonderheiten, die uns nur selten begegnen werden. Ich werde auch nicht im Detail auf alles eingehen; dieses Kapitel soll mehr als Erinnerungshilfe dienen, damit man weiß, wo man nachschauen muss, wenn sich mal ein Regex eigentümlich verhält. 😉

Meta-Zeichen
Gleich im ersten Kapitel erwähnte ich die Metazeichen. Dabei zählte ich auch die Zeichen ] und } auf. Tatsächlich sind dies gar keine Metazeichen. Wird nach ihnen literal gesucht, müsste man sie nicht maskieren. Ich dagegen mache dies aber, um Überblick über mein Regex zu haben. Ich vermeide dabei auch ein Risiko für einen Fehler:

Innerhalb einer Charakterklasse, deren Definition mit „[“ beginnt, gibt es eine abweichende Definition für Metazeichen. Nur die nachfolgenden Zeichen sind dort Metazeichen:

\ zum Maskieren
^ zum Negieren der Charakterklasse, aber nur, wenn es das erste Zeichen ist
– zum Kennzeichnen eines Bereichs
] zum Beenden der Charakterklasse

Nun betrachten wir mal folgendes, wenig geistreiches Regex:

„[Y-]345]“ Dieses Regex soll nach meinem Wunsch eigentlich eine Charakterklasse definieren, bestehend aus allen Zeichen von Y bis zum Zeichen ] sowie den Ziffern 3,4 und 5. Was kommt aber heraus? Findet das Regex den String ‚Z34‘ oder Teile davon? Nein! Stattdessen gebt mal ‚Y345]‘ oder ‚-345]‘ als zu untersuchenden Text ein. Und siehe da, es wird was gefunden. Aber irgendwie was anderes, als wir eigentlich von dem Regex wollten.

Na gut, versuchen wir das mal zu erklären: die erste schließende eckige Klammer wird unmittelbar als Abschluss einer Charakterklasse verstanden. Das Regex findet also Zeichenketten, die mit ‚Y‘ oder ‚-‚ beginnen und danach die Zeichen ‚345]‘ haben. Aber, nichts einfacher als das. Der Kursleiter hat ja gesagt, es ist unschädlich, das ‚]‘ zu maskieren, damit es literal gesucht wird. Und? Ausprobiert?

Jawohl, „[Y-\]345]“ ist genau die Lösung.

Eckige Klammern
Bleiben wir mal bei den eckigen Klammern und schauen uns ein paar Sonderfälle an. Nehmen wir an, wir haben die Option Caseless gewählt, dann wird mit „[aeiou]“ ein ‚A‘ und auch ein ‚a‘ gefunden. Das Regex „[^aeiou]“ findet dagegen ein ‚A‘ nur, wenn Caseless nicht eingeschaltet ist.

Ziffern und Zahlen
Außer Dezimalzahlen, die wir bisher einfach mit „\d“ gesucht haben, kann man selbstverständlich auch hexadezimale oder oktale Zahlen finden. Ein Regex wie „\x09“ findet das Zeichen mit dem hexadezimalen Code 09.

Bei den oktalen wird es spannend: die Syntax ist recht einfach „\ddd“, wobei d für eine Ziffer steht, sucht nach einem Zeichen mit dem oktalen Wert ‚ddd‘. Oder, und nun wird es unübersichtlich, nach einem Backreference, einem Rückbezug. Und zwar interpretiert die Regex-Maschine jede Ziffer, die kleiner als 10 ist sofort als Rückbezug, wenn es sich außerhalb einer Charakterklasse befindet. Innerhalb von Charakterklassen oder wenn es nicht genügend öffnende Klammern gibt, wird der Teil als oktales Suchmuster verstanden. Alles Klarheiten beseitigt? Ok, dann doch besser mit ein paar Beispielen:

\040 ist oktal (Leerzeichen)
\40 ist auch oktal, es sei denn, es gibt genügend öffnende Klammern und somit Subpattern
\6 ist immer Rückbezug
\11 kann Rückbezug sein, ansonsten ‚Tabulator‘
\011 ist immer ‚Tabulator‘
\113 ist immer oktal, weil es nie mehr als 99 Rückbezüge geben darf

Was heißt eigentlich „\0113“?

Einschränkungen
Nur zur Information. Im Regelfall werden wir einfache User das gar nicht bemerken, aber ein Regex darf maximal 65535 Bytes lang sein. Alle Werte, die mit einem Quantifizierer gefunden werden, dürfen 65535 Bytes nicht überschreiten. Es darf nicht mehr als 99 Subpatterns geben. Das Maximum aller geklammerten Ausdrücke, also Subpatterns, Optionen, Assertions, konditionalen Mustern darf 200 nicht überschreiten. Tja, und die Textgröße ist ebenso eingeschränkt, aber im Regelfall irrelevant. Sie darf nur so groß werden wie die größte positive Integervariable sein darf. Da aber für Subpatterns und Quantifizierer mit undefinierter Größe rekursiv von der Regex-Maschine vorgegangen wird, könnte der Speicherplatz geringer ausfallen.

5.6. Überblick über diesen Abschnitt

Dieser Abschnitt hat uns ein Besonderheiten der Regexe nähergebracht. Wir werden damit genügend über Regexe wissen, um sie in PHP, Perl oder sonstwo einsetzen zu können.

Fassen wir mal dieses Kapitel zusammen:

  • es gibt so genannte Assertion, mit denen geprüft werden kann, ob eine bestimmte Zeichenfolge vor oder hinter unserem Suchmuster erscheint, ohne dass diese Zeichenfolge selbst als Ergebnis ausgegeben wird. Diese dürfen nicht durch Quantifizierer in ihrer Länge unberechenbar werden. Man unterscheidet
    • positive Lookahead-Assertion (?=
    • negative Lookahead-Assertion (?!
    • positive Lookbehind-Assterion (?<=
    • negative Lookbehind-Assertion (?<!
  • Zeichenfolgen, die durch Muster in runden Klammern als Subpattern gefunden wurden, können über so genannte Rückbezüge erneut im Regex aufgerufen werden. Rückbezüge oder auch Backreferences werden durch „\d“ definiert.
  • Mit Assertions oder Rückbezügen lassen sich konditionale Regexe erstellen. Die Syntax lautet „(?(Bedingungsmuster)Ja-Suchmuster|Nein-Suchmuster)“
  • Das Regex-Vokabular kann durch Optionen oder Modifikatoren in seiner Interpretation verändert werden. Sie können dem Regex vorangestellt werden: „(?Modifikator)“. Wir haben hier nur einen Ausschnitt der möglichen kennen gelernt:
    • i für Caseless
    • m für Multiline
    • s für DotAll
    • x für Extended
  • Wir lernten ein paar Besonderheiten kennen, die sich auf bestimmte Zeichen beziehen: innerhalb von Charakterklassen können bestimmte Zeichen zu Metazeichen mit anderer Bedeutung werden.
  • Mit einem Regex lassen sich auch hexadezimale oder oktale Werte der Zeichen suchen. Dabei kann es zu Kollisionen mit der Bezifferung von Rückbezügen kommen.
  • Regexe dürfen nicht beliebig groß werden, auch ihr Ergebnis ist beschränkt. Sogar die zu untersuchende Textlänge ist nach oben eingeschränkt. Aber keine dieser Grenzen wird für im Regelfall für uns relevant sein.

Aufgaben

1. Erstellt doch bitte ein Regex, das verdoppelte Wörter erkennt (im Stile von ‚der der‘). Dabei sollte beachtet werden, dass die Wörter nach einem Zeilenumbruch aufeinander folgen dürfen, sie groß oder klein geschrieben sein können und nur als ganze Wörter gefunden werden sollen (also nicht ‚Das Dasein‘).

2. Versuchen wir uns mal an einer ganz einfachen Version eines Subject-Bereinigungsregexes, also einem Regex, das den eigentlichen Betreff aus der Betreffzeile herausoperiert. Beispiel für ein solches Betreff ist zum Beispiel: „Re [2]: Regenechsen in freier Wildbahn (was: Irgendein anderer Thread)“. Das ‚Re‘ kann von allem möglichen und einem Doppelpunkt gefolgt werden. Dann kommt der eigentliche Betreff „Regenechsen in freier Wildbahn“und nach einem Leerzeichen könnte eine Klammer folgen, mit ‚was:‘ oder auch ‚war:‘ würde darin der ehemalige Betreff eingeleitet. Wir wollen aber nur den eigentlichen Betreff! Aber Achtung: es handelt sich wirklich nur um eine ganz stark abgespeckte Version.

3. Wir erhalten Mails mit Bestellsummen. Aus diesen wollen wir mit einem Regex nur den ganzzahligen Wert auslesen (also alle Ziffern vor dem Dezimaltrennzeichen). Dummerweise kommen die Bestellsummen entweder in $ oder in EUR. Na, das wär ja halb so wild, wenn nicht die $-Beträge mit typisch amerikanischen Dezimal- und Tausendertrennzeichen geliefert würden: #,###.##$ und die EUR-Beträge eben halt im deutschen Format #.###,##EUR geschrieben würden. In jedem Fall soll das Regex entscheiden, mit welchem Muster es welche Summe liest.

Zu 1:
Wichtig war hier der Hinweis auf den Zeilenumbruch und die Groß-/Kleinschreibung. Dies sind Optionen, die wir im Regex erst einmal einschalten sollten: „(?im)“. Worte zu finden, sollte uns leicht von der Hand gehen: „[a-z]+“ Wir betrachten wirklich nur Worte ohne Ziffern. Da wir die Option „i“ eingeschaltet haben, müssen wir auch in der Zeichenklasse keine Großbuchstaben mehr definieren. Allerdings wollen wir ganze Wörter betrachten: also sollte vor dem Wort eine Wortgrenze sein. Danach lassen wir einfach ein Leerzeichen (oder viele) folgen. Eine Wortgrenze am Ende zu fragen, könnte uns in die Irre leiten. Schließlich könnte ein Satz mit einem Wort enden und mit genau dem gleichen könnte der Folgesatz beginnen. Das liefert uns also bis jetzt: „(?im)\b[a-z]+\s+“

Das ist natürlich noch falsch, denn das gefunden Wort soll ja noch mal gefunden werden: wir müssen also den Wert in ein Subpattern geben, um es mit einem Rückbezug aufrufen zu können. Also noch mal:

„(?im)(\b[a-z]+)\s+\1“

Leider liefert uns das nun genau den Fall ‚das Dasein‘, den wir gar nicht wollten. Nichts leichter als das: auf das verdoppelte Wort darf halt nichts mehr als eine Wortgrenze folgen und damit hätten wir es:

„(?im)(\b[a-z]+)\s+\1\b“

Zu 2: Noch mal: es ist nur eine sehr vereinfachte Variante eines Regexes. Als Übung sollte es aber gereicht haben.

Das Betreff hätte also laut Vorgaben so aussehen können: ‚Re[2]: richtiger Text, der gefunden werden soll 😉 (was: irgendetwas Unwichtiges)‘

Den Anfang holen wir uns wie folgt: „^Re(.*?):“ Also am Zeilenanfang soll das ‚Re‘ stehen. Alles mögliche, was danach kommen kann, soll genommen werden; ggf. aber auch nichts, wenn nämlich gar kein Zähler dort steht. So etwas können wir natürlich mit „.*“ fangen, aber dummerweise ist das gierig und würde gleich den ganzen anderen Rest des Betreff mitnehmen, wenn noch irgendwo ein Doppelpunkt dort stünde. Daher also das Fragezeichen.

Nach dem Doppelpunkt soll der Betreff folgen, den wir gerne in ein Subpattern hätten. Ok, nur sicherheitshalber fangen wir auch noch evtl. rumlungernde Leerzeichen davor ab: „^Re(.*?):\s*(.*?)“

Im Subpattern 2 sollte sich also der eigentliche Betreff wiederfinden. Wir haben wieder ein Fragezeichen untergebracht, damit nur das gefunden wird, was zwingend nötig ist. Allerdings folgt dahinter noch gar kein Muster, so dass das Regex auch den geklammerten Alt-Betreff mit nimmt.

Das müssen wir auch noch verhindern. Dieser Teil beginnt auf jeden Fall nach einem oder keinem Leerzeichen mit einer Klammer und entweder dem ‚was‘ oder dem ‚war‘.

Letzteres geht einfach „wa(s|r)“ sollte beide Alternativen erledigen. Allerdings könnte dieser Teil auch fehlen! Wir dürfen also im Regex nicht darauf bestehen, dass dieser geklammerte Bereich existiert! Also müsste der Schluss des Regex etwa so aussehen: „\s*(\(wa(s|r):.*\))*$“ oder insgesamt:

„^Re(.*?):\s*(.*?)\s*(\(wa(s|r):.*\))*$“

Das „$“-Zeichen soll sicherstellen, dass auch die gesamte Zeile gelesen wird. Beim Betreff dürfen wir davon ausgehen, dass es in ihm keine Zeilenumbrüche gibt. Wer sich da nicht so sicher ist, sollte dann besser „\Z“ als Kennzeichen für das String-Ende verwenden.

Zu 3: Ok, ein sehr konstruiertes Beispiel. Aber manchmal braucht man die halt (ich erinnere mich an Physikaufgaben im Studium, die von „gewichtslosen Lametta-Fäden“ ausgingen oder „eindimensionalen Kühen“. War auch nicht schlauer ;-)) Sicher könnte man das Einlesen auch anders gestalten, aber ich wollte halt unbedingt ein konditionales Regex haben:

Zuerst brauchen wir eine Assertion, die nach irgendwas gefolgt von zwei Ziffern und einem Dollarzeichen sucht: „(?=.*\d{2}\$)“ Wenn das nämlich existiert, dann soll die Dollarvariante gelesen werden: „([\d,]+)\.\d{2}\$“ Die Vordezimalstellen habe ich einfach als Zeichenklasse definiert und nur Ziffern bzw. Kommata erlaubt (ok, Fehler möglich: eine Zeichenkette aus Kommata, dann Punkt und zwei Ziffern mit Dollar würde akzeptiert. Na, ihr dürft es gern verbessern ;-))

Ist aber diese Dollarvariante nicht da, dann soll das Regex schleunigst nach einer Euro-Variante suchen: „([\d\.]+),\d{2}EUR)“ Auch hier die Zeichenklassendefinition als Vereinfachung.

Zusammen muss das dann so aussehen:

„(?(?=.*\d{2}\$)([\d,]+)\.\d{2}\$|([\d\.]+),\d{2}EUR)“

Ist euch beim Test aufgefallen, dass das EUR-Ergebnis im zweiten Subpattern steht, das Dollarergebnis aber im ersten?

Schlussbemerkung

Ein sehr langer und umfangreicher Kurs. Dabei erklärt er noch nicht mal den Gebrauch von Regexen in den einzelnen Sprachen, werdet ihr denken. Stimmt! Das war auch gar nicht meine Absicht! Vielmehr sollte dieser Workshop eine Schritt-für-Schritt-Anleitung für den Neuling in Regexisch sein. Hat man erst mal verstanden, wie so ein Regex funktioniert, dann ist die spätere Verwendung in PHP, Perl, Javascript oder wo auch immer nur noch eine Frage der sprachspezifischen Syntax. Und dafür gibt es schon Workshops und wird es bestimmt auch weitere geben. Ich hoffe, ich habe euch die Regenechsen näher gebracht. Es ist eine faszinierende Sprache! So faszinierend, dass sie von Programmiersprachen gern als Werkzeug verwendet wird.