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.
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.
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.
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.
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. |
|
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/
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.
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