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.