Einführung in RegEx - Regular Expressions



Regular Expressions, meistens kurz als RegEx oder auf Deutsch auch als "reguläre Ausdrücke" bezeichnet, verfolgen nicht nur bemitleidenswerte Informatikstudenten in die trocken-theoretischen Abschnitte ihres Studiums, sondern sind im Alltag der raschen und effizienten Informationsverarbeitung für viele - zu Recht - nicht wegzudenken. Ob man nun ein Logfile nach relevanten Einträgen durchsuchen, eine E-Mail-Addresse auf Gültigkeit prüfen oder einen ausgewachsenen Parser für ein Dateiformat schreiben möchte - all das ist mit RegEx mit mehr oder minder vertretbarem Aufwand realisierbar.

Konventionen



Um diesen Text so verstehen zu können, wie er eigentlich gemeint ist, wollen wir einige Vereinbarungen treffen, die möglichen Fehlinterpretationen vorbeugen sollen. Folgenden Zeichen soll daher bei der Lektüre dieses Howtos besondere Beachtung geschenkt werden:

Was sind RegEx?



Wir wollen uns hier aber nicht mit den esoterisch-theoretischen Details und Fakten aufhalten, sondern in den wichtigsten Dialekten - denn auch die gibt es bei RegEx leider - soweit sattelfest werden, dass wir am Ende Konstrukten wie ^\([[:blank:]]\+\)\(.*\) nicht mehr hilflos mit fragendem Blick gegenübersitzen.

RegEx dienen dazu, Muster zu definieren - Muster, die Klassen von Zeichenketten beschreiben. Regular Expressions kennen zum Beispiel ein Konstrukt für "das Zeichen X beliebig oft wiederholt, direkt gefolgt vom Zeichen Y, welches aber nur genau zwei bis vier Mal vorkommen darf". Zum Lösen einer solchen Aufgabe sieht man sich vor eine Reihe nicht ganz trivialer Problem gestellt: Man hat, möchte man alle Zeichen eines Alphabets durch RegEx matchen können, in dem auch sämtliche Zeichen des zugehörigen RegEx-Alphabets vorkommen, eigentlich zu wenige Zeichen zur Verfügung. Im Alltag möchte man natürlich alle möglichen Zeichen, die in Texten vorkommen können, durch RegEx matchen können - unnötige Limitationen wollen und brauchen wir nicht.

Erste Probleme - und ihre Lösungen



Was nun aber, wenn wir vereinbaren, dass das Zeichen * für "beliebig viele Wiederholungen des direkt links davon notierten Zeichens" steht, und dann vor das Problem gestellt werden, das Zeichen "*" zu matchen?
In diesem Moment kommt uns so genanntes "Escapen", das aus vielen Anwendungen in der Informatik vertraut ist, ins Spiel: einem gewissen Zeichen, meist \ (dem "Backslash"), wird eine besondere Bedeutung verliehen:
Wird "\" vor ein Zeichen aus dem RegEx-Alphabet gestellt, verliert dieses Zeichen seine Sonderbedeutung - und wird zu einem "Literal", quasi einem Zeichen mit "wörtlicher Bedeutung". Das gilt übrigens auch für "\" selbst; will man einen Backslash matchen, muss man also \\ notieren.
Steht "\" aber direkt vor einem Zeichen aus dem literalen Alphabet, so bekommt dieses - falls zutreffend - eine besondere Bedeutung verliehen. Die Programmiersprache "Perl" mit ihrem (extrem mächtigen) RegEx-Dialekt "PCRE" ("Perl-compatible regular expressions") macht extensiven Gebrauch dieses Kniffs.

Wir können also sagen, dass uns an RegEx (und ihrem Alphabet) zwei Klassen von Zeichen interessieren: Literale einerseits, und Steuerzeichen andererseits.

`grep` - unser täglich Brot



Das Programm, mit dessen Hilfe wir unsere ersten RegEx formulieren werden, ist `grep`. Der Name "grep" ist, wie so oft in der UNIXoiden Welt, ein Akronym - in diesem Fall für "global regular expression print". Die Syntax von `grep` wie sie uns momentan interessiert ist trivial: `grep [muster] [datei]`. Verzichtet man auf die Angabe einer Datei, liest `grep` von stdin, also dem Standardinput einer Shell (in den meisten Fällen ist das schlicht die Tastatur oder eine Pipe).
`grep` arbeitet, wie die meisten RegEx verstehenden Programme, zeilenorientiert. Wird das geforderte Muster in einer Zeile der angegebenen Datei gefunden, wo wird diese auf stdout (meist also dem Monitor) ausgegeben, andernfalls verschweigt `grep` ihre Existenz ganz einfach.
RegEx - und damit auch `grep` - sind im übrigen "case-sensitive", sie unterscheiden zwischen Groß- und Kleinschreibung.

Zur Veranschaulichung folgt ein Beispiel:

Eine Datei "demo1.txt" beinhaltet die folgenden Zeilen:
$ cat demo1.txt ENTER
ersteZeile
zweiteZeile
drittezeile
vierteZeilE
FünfteZEiLE
sechste Zeile

Führen wir nun `grep e demo1.txt` aus, fordern wir `grep` dazu auf, alle Zeilen des Files "demo1.txt" auszugeben, welche das Muster e (was in diesem ersten Beispiel ein Literal, also tatsächlich ein "e" höchstselbst, ist) enthalten. Ein kurzer Blick auf den Inhalt der Datei verrät uns das wenig überraschende Ergebnis eigentlich auch ohne `grep`: alle Zeilen matchen unser Kriterium, und werden folglich ausgegeben. So weit, so gut.

Als nächstes ändern wir unser Muster leicht ab; wir wollen nur noch Zeilen zu sehen kriegen, welche das Literal eZ enthalten. Hier verrichtet `grep` erstmals für uns sichtbare Arbeit, die Ausgabe beschränkt sich nämlich auf:
$ grep "eZ" demo1.txt ENTER
ersteZeile
zweiteZeile
vierteZeilE
FünfteZEiLE

Wenig überraschend - aber doch auch beruhigend - die Tatsache, dass die dritte und sechste Zeile weichen mussten - was nur gerecht ist, beinhaltet doch keine die notwendige Zeichenfolge "eZ".

Variable Bausteine - tatsächliche RegEx



Bislang haben wir uns mit wenig spannenden Literalen sehr genügsam gegeben. Wirklich interessant und vielseitig werden RegEx freilich erst dann, wenn man ein wenig mit Steuerzeichen würzt - und derer gibt es viele, nüztliche. Das erste solche, das wir in unsere Dienste stellen wollen, ist . - der Punkt. In den meisten RegEx-Dialekten, so wie auch dem von `grep`, steht . für genau ein beliebiges Zeichen (außer "newline", dem Zeilenumbruch).

Um das zu veranschaulichen erweitern wir unser Muster von weiter oben so, dass es sich als e.Z liest - und wierderholen das altbekannte Spiel. In diesem Fall unterschlägt uns `grep` gleich einiges an Text:
$ grep "e.Z" demo1.txt ENTER
sechste Zeile

Nur noch eine einsame Zeile verbleibt, die unsere Kriterien erfüllt - ein kleines "e", gefolgt von einem beliebigen Zeichen, worauf ein "Z" in Uppercase folgt. Eigentlich einfach, nicht?

Beliebige Zeichen zu matchen ist zwar nett, aber nicht immer das, was wir auch wollen - in den meisten Fällen wird es wohl eher so sein, dass wir eine gewisse Menge von Zeichen erlauben, alle anderen aber ausschließen wollen. Dafür gibt es in allen RegEx-Dialekten die Möglichkeit, so genannte "bracket expressions" zu formulieren - eine Liste von Zeichen, die an einer gewissen Stelle in unserem Muster stehen dürfen bzw. sollen. Eine bracket expression, die die Literale a, b, x, Y und 9 repräsentiert, sieht so aus: [abxyZ9] - wobei die Reihenfolge der Listenelemente keine Rolle spielt. Damit das ganze etwas komfortabler wird, gibt es noch einige Erweiterungen hierzu anzumerken. Einerseits ist es möglich, innerhalb einer bracket expression ganze "ranges", also "Bereiche" gültiger Zeichen anzugeben. Will man zum Beispiel alle Ziffern matchen, ist das innerhalb einer bracket expression auf mehrere Arten möglich, unter anderen den folgenden: [0123456789], oder kürzer mittels [0-9]. Man kann auch mehrere dieser "intelligenten" Ranges kombinieren; alle Kleinbuchstaben, sowie die Ziffern von "4" bis "9", matcht zum Beispiel der Ausdruck [a-z4-9].
Doch auch damit ist noch nicht alles zu Ranges gesagt. Es gibt nämlich auch die Möglichkeit, den Match einer bracket expression zu negieren, indem man einfach ^ direkt nach der öffnenden eckigen Klammer notiert. [^c-k] matcht also alle Zeichen, außer die Buchstaben von "c" bis "k".

Probleme? Keine!



Besonders aufmerksamen Lesern werden, bracket expressions betreffend, vielleicht zwei kleine Ungereimtheiten aufgefallen sein: Wie zum Beispiel ist es nun möglich, die Zeichen "^" oder "-" innerhalb einer solchen zu matchen?
Ganz einfach: will man "^" innerhalb einer bracket expression matchen, stellt man sie einfach NICHT an den Anfang der Liste. Möchte man "-" matchen, stellt man es schlicht an den Anfang, also direkt nach die öffnende eckige Klammer, oder das Ende.
Einen ganz ähnlichen Cornercase, nämlich das Matchen von "[" und "]", lässt sich auf durch Kennenlernen dieses "Tricks" bereits vertraute Weise lösen. Ein Muster, das alle unsere eben kennengelernten "Problemzeichen" matcht, ist das folgende: []^[-]. Alle anderen RegEx-Zeichen, bis auf den escape character, verlieren innerhalb von [ und ] ihre Sonderbedeutungen, und bereiten uns deshalb keine weiteren Schwierigkeiten.

Weil "^" uns im vorigen Absatz schon so lange beschäftigt hat, wollen wir auch gleich noch eine zweite Sonderbedeutung klären. Mit RegEx its es nämlich auch möglich, ein Muster notwendigerweise am Anfang oder am Ende einer ganzen Zeile zu matchen. Dies erledigen das mittlerweile schon hinlänglich strapazierte ^ für den Anfang, und das noch nicht erwähnte $ für das Ende dieser Zeile. Ausgehend von unserem ersten File wollen wir uns ansehen, wie wir alle Zeilen von `grep` ausgeben lassen, die mit einem Großbuchstaben beginnen: Durch `grep "^[A-Z]" demo1.txt` erreichen wir die Ausgabe von:
$ grep "^[A-Z]" demo1.txt ENTER
FünfteZEiLE

Die übrigen fünf Zeilen fallen durch unser Raster, und werden stillschweigend verschluckt.

RegEx - einfach logisch



Eine weitere unverzichtbare Funktionalität, die RegEx bietet, ist das logische (boolsche) "ODER" bzw. "OR". Dieses wird, wie in den allermeisten computerlinguistischen Implementationen, durch "|", die "pipe", repräsentiert. Die Anwendung unterscheidet sich nicht von der anderer Zeichen aus dem RegEx-Alphabet: "A|Z" matcht alle Zeichen, die entweder "A" oder "Z" beinhalten. (Konkatenierte Zeichenketten werden ohnehin implizit mit "AND" bzw. "UND" verknüpft, weshalb es hierfür auch keinen Operator gibt.)
`grep "erste\|zweite" demo1.txt` zeigt, was durch Anwendung dieses Konzepts auf unser Beispiel zur Folge hat:
$ grep "erste\|zweite" demo1.txt ENTER
ersteZeile
zweiteZeile

1001 Zeichen. Aber nicht händisch.



All das mag schön und gut sein, aber ein weiteres Basiskonzept fehlt uns noch, um etwas mehr Komfort zu erreichen, und RegEx in noch mehr Situationen tatsächlich effektiv anwenden zu können: Quantoren.
Quantoren sind Zeichen aus dem RegEx-Alphabet, die ihnen vorangegangene Muster um das Kriterium "Anzahl von Wiederholungen" erweitern. Die zwei meistgenutzten Quantoren, die wir als erstes kennenlernen wollen, sind * und +. Der "*" oder "Asterisk" ist der genügsamste aller Quantoren, er bezeichnet das Kriterium "muss 0 bis n Mal vorkommen", was ziemlich einfach zu erfüllen sein sollte. Anspruchsvoller gibt schon sich "+", "Plus", das "muss mindestens 1 Mal vorkommen" formuliert.
Um das kurz zu demonstrieren, erstellen wir ein neues File "demo2.txt" mit dem Inhalt aus dem Listing unten:
$ cat demo2.txt ENTER
den
denn
dennnnn

Führen wir nun `grep "denn*" demo2.txt` aus, ergibt sich folgendes Bild:
$ grep "denn*" demo2.txt ENTER
den
denn
dennnnn

Wie erwartet. Wollen wir nun aber statt "*" dessen Verwandten "+" als Quantor verwenden, müssen wir diesen - wie zuvor bereits kurz angeschnitten - escapen. Da "+" als Literal erfahrungsgemäß relativ häufig gesucht wird, als RegEx-Quantor aber weniger oft Gebrauch findet, hat er ohne Schutz durch einen Backslash eben Literalbedeutung (Was insofern auch sinnvoll ist, da man "n+" - mit "+" in seiner Bedeutung als Quantor - äquivalent als "nn*" formulieren kann!).

Wir führen also `grep "denn\+" demo2.txt` aus, und freuen uns wie ein Schneekönig über die folgende Ausgabe:
$ grep "denn\+" demo2.txt ENTER
denn
dennnnn

Sehr gut. Weil das so schön ist, befassen wir uns noch ein wenig länger mit dieser Facette regulärer Ausdrücke. Man kann nämlich auch ein Kriterium für "muss 0 oder 1 mal vorkommen" brauchen, welches uns das Zeichen ?, das natürlichsprachlich oftgebrauchte Fragezeichen, liefert. Um zu zeigen, was es kann, führen wir gleich `grep "denn?" demo1.txt` aus - und siehe da:
$ grep "denn?" demo2.txt ENTER
den
denn

Das ohnehin orthographisch fragwürdige "dennnnn" will unserer RegEx mit dem ?-Quantor nicht genügen, diese Zeile fällt also weg. Manchmal ist aber das noch nicht genug; etwas wie "muss zwischen 2 und 7 Mal vorkommen" ist gefragt, und man hätte dafür gerne ein halbwegs nachvollziehbares und vor allem menschenlesbares Konstrukt. Dafür bietet sich { an, die "opening curly bracket". Ihr folgt eine natürliche Zahl, die angibt, wie oft das voranstehende Atom - eine unteilbare Einheit aus Literalen und RegEx - hintereinander notiert sein muss, um zu matchen. "en{5}" matcht also auf "ennnnn", nicht aber z. B. auf "ennn" oder "en".
Damit noch nicht genug: mittels "{" ist es auch möglich, einen gewissen Bereich gültiger Wiederholungen zu formulieren, und zwar über {n,m} - wobei n die Unter-, und m die Obergrenze dieser Anzahl beschreiben.
Dabei muss man beachten, dass die geschwungenen Klammern in POSIX Regular Expressions ganz gewöhnliche Terminalzeichen sind - wenn man sie nicht richtig escapet, haben sie also keine besondere Bedeutung.
Angewandt auf unser Beispiel von vorhin gibt `grep "n\{2,6\}" demo2.txt` folgenden Output:
$ grep "n\{2,6\}" demo2.txt ENTER
denn
dennnnn
wird fortgesetzt...

Herzlichen Dank für Korrekturen, Kommentare und Ergänzungen an:

Zuletzt geändert: Friday, 14-May-2021 10:08:26 UTC
Copyright ©2006 Johannes Truschnigg. Lizenziert unter den Bedinungen der GNU Free Documentation License.
Bitte besuchen Sie auch meine Website!