Unterabschnitte


Reguläre Ausdrücke

Was sind denn überhaupt reguläre Ausdrücke[*]? Es gibt Leute, die finden reguläre Ausdrücke fast so genial wie die Erfindung des geschnittenen Brotes: Im Prinzip sind reguläre Ausdrücke Suchmuster, die sich auf Strings (Zeichenketten) anwenden lassen. Auf den ersten Blick sehen sie etwas kryptisch aus, was sie durchaus auch sein können. Was sie aber so sinnvoll macht ist ihre Fähigkeit, komplexe Suchen durchzuführen - mehr dazu im folgenden.

Anfängern empfehle ich, dieses Kapitel erstmal zu überspringen. Alles, was man mit regulären Ausdrücken realisieren kann, kann auch mit normalen Befehlen programmiert werden, ist dann aber natürlich etwas länger. Mit regulären Ausdrücken ist es einfach eleganter.

In Unix-Programmen, vor allem in Perl[*], werden die regulären Ausdrücke sehr gerne benutzt, wenn normale Suchmuster nicht mehr ausreichen. Auch wenn einfache Suchmuster noch relativ einfach zu erstellen sind, kann das beliebig kompliziert werden. Nicht umsonst gibt es dicke Bücher, die sich nur mit diesem Thema beschäftigen.

Wann sollen denn nun die regulären Ausdrücke eingesetzt werden? Die Antwort ist ganz einfach: Wenn man nach Mustern suchen will, bei denen die normalen Funktionen nicht mehr ausreichen. Wenn hingegen die normalen String-Funktionen, wie z.B. str_replace() oder strpos(), ausreichen, sollten diese auch benutzt werden, weil sie schneller abgearbeitet werden.

PHP kennt zwei verschiedene Funktionsgruppen von regulären Ausdrücken, die ereg*- und die preg*-Funktionen. Erstere sind von Anfang an dabei, halten sich an die POSIX-Definition, sind aber langsamer und nicht so leistungsfähig wie die letzteren, die sich an Perl orientieren. Von daher sollten, wenn möglich, die preg*-Funktionen verwendet werden. In Konsequenz werde auch ich hier die Perl-kompatiblen Funktionen verwenden; diese werden auch häufig als PCRE[*]-Funktionen bezeichnet.

Mit regulären Ausdrücken ist es wie mit Programmen (und Dokus ;-)): Es ist eine Kunst, gut, verständlich, fehlerfrei und effizient zu schreiben, und mit der Zeit perfektioniert man sein Können. Von daher: Nicht verzweifeln, wenn es am Anfang nicht funktioniert oder man Minuten braucht, bis dieser 20 Zeichen lange Ausdruck steht - üben, probieren und nochmals üben. Irgendwann geht es.

einfache Suchmuster

Das Suchmuster muß bei den preg*-Funktionen von Begrenzungszeichen (sog. Delimiter) eingeschlossen werden. Häufig werden der Schrägstrich (Slash) / oder das Gleichheitszeichen = verwendet. Im Prinzip kann jedes Zeichen benutzt werden, solange es nachher nicht im Suchmuster verwendet wird, bzw. immer mit einem Backslash \ escaped wird.

Z.B. sucht das Muster /<title>/ nach dem HTML-Tag <title>. Wenn man jetzt aber Header-Tags (<h1>,<h2>,...,<h6>) finden will, funktioniert das so nicht mehr. Hier braucht man einen Platzhalter. Das, was auf der Kommandozeile der Stern * und in SQL bei LIKE das Prozentzeichen % ist, ist hier der Punkt .. Das Muster /<h.>/ würde auf alle HTML-Tags zutreffen, bei denen auf das h ein beliebiges Zeichen folgt. Wir wollten aber nur die Zahlen 1-6. Mit \d steht ein Platzhalter für eine beliebige Zahl zur Verfügung. /<h\d>/ trifft nur noch die HTML-Tags <h0>,<h1>,<h2>,...,<h9>. Das ist zwar schon besser, aber auch noch nicht perfekt. Man bräuchte genau die Menge der Zahlen 1,2,3,4,5,6. Dies ist selbstverständlich auch möglich: Der Ausdruck /<h[123456]>/ bewirkt das Gewünschte, er läßt sich aber noch verschönern zu /<h[1-6]>/.

Wie wir gesehen haben, kann mit Hilfe der eckigen Klammern [] eine Menge von Zeichen definiert werden, indem man entweder alle aufzählt oder den Bereich angibt. Wenn man das Dach ^ als erstes Zeichen angibt, wird die Auswahl negiert.


Tabelle 12.1: Zeichenmengen
. ein beliebiges Zeichen (bis auf Newline \n)
[] die angegebenen Zeichen bzw. Bereiche
z.B.  
[1-6] die Zahlen 1, 2, 3, 4, 5 und 6
[^] als erstes Zeichen wird die Auswahl negiert
z.B.  
[^1-6] alle Zeichen bis auf die Zahlen 1-6
\d Dezimalziffer
\D alle Zeichen bis auf die Dezimalziffern
\s Whitespace (Leerzeichen, Tabulator etc.)
\S alle Zeichen außer Whitespace
\w alle ``Wort``-Zeichen (Buchstaben, Ziffern, Unterstrich)
\W alle ``Nicht-Wort``-Zeichen



Tabelle 12.2: Sonderzeichen bei den PCRE
\n Zeilenumbruch (LF)
\r Wagenrücklauf (CR)
\\ Ein \
\t Tabulator


Das Dach ^ hat auch im Muster eine besondere Bedeutung: es markiert den Stringanfang. Der Ausdruck /^Hallo/ paßt auf alle Strings, bei denen das Wort ,Hallo` am Anfang steht, nicht jedoch in der Mitte. Neben dem Anfang ist natürlich auch das Ende interessant, das mit einem Dollar $ markiert wird. Das Suchmuster /Welt!$/ paßt demnach auf alle Strings, die mit dem Wort ,,Welt!`` enden. Das läßt sich natürlich auch kombinieren: Mit /^Hallo Welt!$/ findet man genau die Strings ,,Hallo Welt!`` .

Zur Vertiefung und Übung habe ich versucht, ein paar Beispiele zu erfinden. Diese befinden sich bei den Übungen am Ende des Kapitels.

Quantifizierer

Mit dem bisher Gesagten kann man zwar schon schöne Ausdrücke schreiben, aber irgendwie fehlt noch etwas. Wie die letzte Übung gezeigt hat, ist es doch relativ umständlich, mehrere aufeinander folgende Zeichen anzugeben. Selbstverständlich ist auch das möglich: hier hilft der Quantifizierer (auch Quantor genannt). Man unterscheidet die folgenden Typen:

Da wäre als erstes der altbekannte Stern *, der für ,,keinmal`` oder ,,beliebig oft`` steht. Das Suchmuster /^\d*$/ würde z.B. alle Strings, die aus keiner oder beliebig vielen Ziffern bestehen, finden. Wie man an dem Beispiel sieht, muß der Quantifizierer immer hinter dem Zeichen bzw. der Zeichenmenge stehen. Neben dem Stern *, Allquantor genannt, gibt es noch das Fragezeichen ?, das für keinmal oder einmal steht und Existenzquantor genannt wird, sowie das Pluszeichen + (mind. einmal).


Tabelle 12.3: Quantifizierer
? kein-/einmal
+ mind. einmal
* keinmal oder beliebig oft
{n} genau n-mal
{min,} mind. min-mal
{,max} keinmal bis höchsten max-mal
{min,max} mind. min, aber höchstens max-mal


Wenn man aber genau fünf Zahlen oder zwischen drei und sieben kleine Buchstaben haben will, reichen die bisherigen Quantifizierer dann doch noch nicht. Deshalb gibt es noch die geschweiften Klammern {}, in die man die gewünschten Zahlen eintragen kann. Das Suchmuster für genau fünf Zahlen sieht z.B. so aus: /\d{5}/; das für die drei bis sieben kleinen Buchstaben so: /[a-z]{3,7}/.

Auch hier gibt es zur Vertiefung Übungen am Ende des Kapitels.

Gruppierungen

Die runden Klammern ( ) haben auch eine besondere Bedeutung: Der Hauptzweck ist, den eingeklammerten Bereich zur späteren Verwendung zu markieren. Die Klammern kommen auch beim wiederholten Vorkommen eines Teilaudrucks hintereinander zum Einsatz. Z.B. würde ein Ausdruck, der auf drei zweistellige Zahlen, die mit einem Doppelpunkt anfangen, paßt, ohne Klammern so aussehen /:\d{2}:\ d{2}:\d{2}/ . Mit Klammer wird das ganze übersichtlicher: /(:\d{2}){3}/ .

Die markierten Teilbereiche eines Ausdruckes werden auch Backreferences genannt. Sie werden von links nach rechts durchnumeriert. Innerhalb eines Ausdruckes kann man sich mit \1 auf den Text, der von der ersten Klammer eingeschlossen wird, beziehen, mit \2 auf den zweiten usw. Der Ausdruck /(\w)\s\1/ paßt immer dann, wenn zweimal dasselbe Wort nur durch ein Leerzeichen getrennt vorkommt.

Zur allgemeinen Verwirrung gibt es auch hier am Ende des Kapitels Übungen.

Optionen

Die ganzen Ausdrücke können sich je nach angegebener Option auch noch anders verhalten. Die Optionen werden hinter dem letzten Delimiter angegeben. So bewirkt z.B. ein i, daß nicht auf Groß-/Kleinschreibung geachtet wird. Der Ausdruck /hallo/i paßt auf ,,Hallo``, ,,hallo``, ,,HALLO`` oder jede andere Kombination von Groß-/Kleinbuchstaben. Eine Übersicht zu den Optionen gibt die Tabelle 12.4.


Tabelle 12.4: Optionen für reguläre Ausdrücke
i Es wird nicht auf Groß-/Kleinschreibung bei Buchstaben geachtet.
m Normalerweise betrachtet PHP bei den PCRE, wie auch Perl, den String als eine Zeile, das heißt das Dach ^ paßt nur auf den Anfang des Strings und das Dollar $ auf das Ende des Strings.
  Wenn diese Option gesetzt ist, paßt das ^ auf jeden Zeilenanfang, wie auch auf den String-Anfang. Beim $ gilt das Ganze analog für das Ende.
  Wenn es keine Zeilenumbrüche in dem Text gibt oder im Ausdruck kein ^ bzw. $ verwendet wird, hat diese Option logischerweise keine Funktion.
s Ohne diese Option paßt der Punkt . auf alle Zeichen, außer den Zeilenumbruch \n. Mit gesetzter Option paßt der Punkt auf alle Zeichen, inklusive Zeilenumbruch.
  In negierten Mengen, wie z.B. [^a], ist der Zeilenumbruch immer enthalten, unabhängig von der Option.
e Mit dieser Option wird der zu ersetzende Text bei preg_replace() als PHP-Code aufgefaßt und entsprechend ausgewertet.
A Bei Setzen dieser Option, wird der Ausdruck auf den Anfang des Strings angewandt. Dieses Verhalten kann auch durch entsprechende Konstrukte im Ausdruck erreicht werden.
U Normalerweise sind die Quantifizierer greedy (gierig), das heißt, sie versuchen immer den größtmöglichen Text zu treffen. Mit Hilfe dieser Option wird auf ungreedy umgeschaltet, das heißt, es wird immer der kürzestmögliche Text genommen.


Übungen

einfache Suchmuster

Passen die folgenden Suchmuster auf die beiden Texte oder nicht? Text1=,,Hallo Welt!``, Text2=,,PHP ist einfach genial und die Regex sind noch genialer``. Die Lösung befindet sich in Anhang B.6.1.

.

/hallo/

.

/[^0-9A-Z]$/

.

/^[HP][Ha][lP]/

.

/\w\w\w\w\w/

.

/\w\w\w\w\w\w/

Quantifizierer

Hier gilt dieselbe Aufgabenstellung wie in der vorigen Aufgabe

.

/^\w* \w*$/

.

/^\w* \w*!$/

.

/^\w* [\w!]*$/

.

/\w{4} \w+/

.

/\w{4} \w+$/

.

Diesmal darfst du selbst nachdenken: Schreibe einen Ausdruck, der überprüft, ob eine Log-Zeile dem Standard-Log Format des Apache-Servers entspricht. Eine Zeile kann z.B. so aussehen: 192.168.1.1 - - [01/Apr/2001:08:33:48 +0200] "GET /test.php4 HTTP/1.0" 200 3286 Die Bedeutung der Felder ist hier nicht wichtig, kann aber trotzdem interessant sein: Als erstes steht die IP-Adresse bzw. der Rechnername, von dem die Anfrage kam. Die nächsten beiden Felder sind der Username (vom identd bzw. von auth), beide Felder müssen aber nicht existieren. In den eckigen Klammern steht das genaue Datum mit Zeit und Zeitzone. In den Anführungszeichen steht die angeforderte Datei mit der Methode (kann auch POST sein) und dem Protokoll (kann auch HTTP/1.1 sein). Als vorletztes wird der Status angegeben (200 ist OK) und das letzte Feld sagt, wie viel Daten bei diesem Aufruf übertragen wurden.

Gruppierungen

.

Schreibe einen Ausdruck, der überprüft, ob bei einer Preisangabe der DM-Betrag gleich dem Pfennig-Betrag ist. Die Preisangabe sieht vom Format her so aus: 99,99DM.

Christoph Reeg