1. Was ist ein Regulärer Ausdruck?
Ein Regulärer Ausdruck (kurz Regex) ist ein String der angibt wie ein anderer String aussehen soll. Wenn ein String zu einem Regex passt spricht man auch von einem match, daher auch der Funktionsname von preg_match.
2. Aufbau von einem Regex
Unabhängig was ein Regex prüft haben alle Reguläre Ausdrücke der PCRE-Engine (die Engine die für die Regex-Auswertung zuständig ist) den selben Aufbau.
-
Delimiter - Dieses erste Zeichen im String dient dazu den eigentlichen Regex von einer Liste globaler Modifikatoren (auch modifier genannt) zu trennen. So ein Zeichen ist nötig da alles in ein einzelnen String kodiert werden muss. Jedoch besteht ein PCRE-Regex Grundsätzlich aus eben den eigentlichen Regex und den modifiern. Daher gibt das erste Zeichen im String das Trennzeichen an. Üblicherweise werden Zeichen wie
/
oder~
verwendet. -
Regex - In diesem Teil steckt die eigentliche Logik drin. Er beschreibt wie der zu prüfende String aussehen soll.
-
Delimiter - Nun kommt der vorher gewählte Delimiter um die folgende modifier vom Regex zu trennen.
-
Modifier - Dieser Teil beschreibt Modifikatoren für den ganzen Regex. Die Liste der Modifier kann dabei leer sein wenn keine benötigt werden.
Da in einem Regex oft das Zeichen \
verwendet wird, jedoch \
auch das PHP-Escapezeichen für Strings ist werden für Regex üblicherweise
single-quoted-Strings verwendet.
3. Finden von Strings
Die einfachste Variante von einem Regex ist der leere Regex //
(mit /
als Delimiter). Er matched jeden String da eine leere Zeichenkette
gesucht wird und überall eine leere Zeichenkette zu finden ist.
re> // data> foobar 0: data> 0: data> eiovrjerv 0: data> 32 e4234234 324 2 0:
Das Testprogramm pcretest
dient dazu ein Regex zu testen. Hier wurde der Regex
//
eingegeben. Anhand von Beispieleingaben sieht man dass jeweils etwas
gefunden wurde was eine Nummer 0
hat. Dieser Wert liefert immer den gefundenen
Text zurück. Da nach einem leeren String gesucht wurde wurde auch stehts
ein leerer String gefunden. In PHP können wir dann später auch auf das Ergebnis
0
zugreifen.
Als nächstes Beispiel suchen wir nun einen normalen String. Solange er keine
Metazeichen enthält
kann man den String den man suchen möchte als solches hinschreiben. Falls man doch
Metazeichen hat werden sie wie in PHP mit \
escaped.
re> /bla/ data> foobar No match data> blabli 0: bla data> BLABLI No match data> xyzblaBLI 0: bla
Bei der zweiten Eingabe sieht man dass der gesuchte Text auch gefunden wurde. In der
dritten Eingabe wurde der Text jedoch nicht gefunden da reguläre Ausdrücke casesensitiv
sind und entsprechend Groß- und Kleinschreibung. Falls man möchte das ein
regulärer Ausdruck caseinsensitiv arbeitet kann man den modifier i
verwenden.
re> /bla/i data> foobar No match data> blabli 0: bla data> BLABLI 0: BLA data> xyzblaBLI 0: bla
Nun wurde auch im dritten Teststring das Suchmuster gefunden.
4. Unterschiedliche Strings finden
Manchmal möchte man nicht nach einen Konkreten String suchen sondern
nach einer Auswahl von mehrere Strings. Diese Strings werden in einem
Regex mit dem |
-Zeichen getrennt. Es können beliebig viele
Strings mit |
getrennt werden.
re> /foo|bar/ data> blabarfoo 0: bar data> foobar 0: foo data> fobarfoarofboaoar 0: bar data> fobabfoaoofoa No match data> Foobar 0: bar
Mit unserem Regex suchen wir nach einem String foo
oder
bar
. Im ersten Beispiel wurde bar
gefunden, obwohl
beide Strings vorhanden sind. Der Regex sucht dabei von Vorne und findet
entsprechend zuerst das bar
. Im zweiten Beispiel ist es
genau umgekehrt, da findet er das foo
zuerst und im
dritten Beispiel findet er das bar
an der Stelle 3-5.
Im vierten Beispiel ist keiner der beiden Strings vorhanden. Im
fünften Beispiel findet er bar
da wir nicht den
i
-modifier verwendet haben.
5. Erfassen von Teilergebnissen
Manchmal will man mit einem Regex nicht nach einem String suchen
sondern nach mehreren Teilstring. Im Moment kriegen wir nur durch
diese 0:
-Angabe das Suchergebniss des ganzen Strings.
Es ist jedoch möglich gleichzeitig neben den kompletten Suchergebnis
auch Teilergebnisse abzufragen. Für jedes Teilergebnis welches
man abfragen möchte verwendet man die Klammern (
und )
. Dadurch entsteht ein neuer Eintrag in der
Ergebnisliste.
re> /News vom (Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)/ data> <p>News vom Montag<br />Am gestrigen Sonntag ist ein Sack Reis umgefallen</p> 0: News vom Montag 1: Montag data> <p class="news Dienstag">News vom Freitag<br />Wieder ein Sack umgefallen.</p> 0: News vom Freitag 1: Freitag data> Weder am Mittwoch noch am Donnerstag ist was passiert No match
Hier wird erstmal allgemein nach dem Text News vom $WOCHENTAG
gesucht.
Jedoch wird hier der Wochentag selbst noch abgefragt. Im ersten Fall finden
wir somit den Text News vom Montag
als Ergebnis 0
und darin speziell den Montag
als Ergebnis 1
.
Im zweiten Beispiel finden wir den Newsheader mit Freitag obwohl
vorher der String Dienstag
vorhanden ist. Den haben wir
jedoch nicht gesucht, gesucht haben wir nach News vom $WOCHENTAG
.
Dies wird im dritten Beispiel deutlich wo wir zwar die Wochentage finden
aber nicht nach unserem gesuchten News vom $WOCHENTAG
-Aufbau.
6. Zeichenklassen
Wenn man z.B. nach den Ziffern 0, 2, 4, 6 oder 8 sucht würde man ggf. den
Regex /0|2|4|6|8/
verwenden. Dies kann vereinfacht werden
wenn man eine Zeichenklasse verwendet. Dabei verwendet man die eckigen
Klammern [
und ]
, die eine Zeichenklasse einleiten
bzw. beenden. Innerhalb der Zeichenklasse schreibt man einfach
die Zeichen rein die dazugehören sollen. Der Regex passt dann auf ein
Zeichen in dieser Liste.
re> /Es gab [23] Spiele heute/ data> Es gab 2 Spiele heute 0: Es gab 2 Spiele heute data> Es gab 5 Spiele heute No match data> Es gab 22 Spiele heute No match
Im ersten Beispiel wird unser Teststring gefunden, im zweiten und dritten
Beispiel jedoch nicht da weder die 5
in der Zeichenklasse
ist, noch der String Es gab 22 Spiele heute
irgendwie passt.
Anstatt jedes einzelne Zeichen aufzulisten können auch Bereiche mit einem Minuszeichen angegeben werden.
re> /[0-5]/ data> Ich suche eine 4 im Text 0: 4 data> ich suche nun eine 8 No match data> und nun eine 99 No match data> und zu letzt eine 49 0: 4
Im letzten Beispiel sieht man das die 4 gefunden wurde und nicht die Zahl 49,
da es sich bei [...]
um Zeichenklassen handeln und nichts anderes.
Somit wird man mit einem Regex /[0-255].[0-255].[0-255].[0-255]/
nicht nach IP-Adressen suchen können da die Zeichenklasse [0-255]
nur auf die Zeichen 0
, 1
, 2
und
5
passt (die zweite 5 ist sogar überflüssig).
Für den Fall dass man das Minuszeichen in die Zeichenklasse aufnehmen will muss man es am Anfang oder Ende der Zeichenklasse plazieren damit es nicht als Bereich von Irgendwas interpretiert werden kann.
Wenn das erste Zeichen in der Zeichenklasse das ^
-Zeichen wird
die Zeichenklasse negiert, dann matched sie auf alle Zeichen die nicht
in der Zeichenklasse stehen.
re> /[0-5]/ data> Ich suche eine 4 im Text 0: 4 re> /[^0-5]/ data> nun suche ich eine 8 0: n
Im zweiten Beispiel wird das n
von nun
gefunden,
das es das erste Zeichen ist das keine Ziffer von 0 bis 5 ist.
7. Vordefinierte Zeichenklassen
Es gibt Suchstrings die häufig vorkommen wie z.B. eine Ziffer oder ein Buchstabe. Anstatt nun alle Ziffern bzw. alle Buchstaben aufzulisten gibt es in PCRE vordefinierte Zeichenklassen.
-
\d
- Diese Zeichenklasse matched auf eine Ziffer, dasd
kommt dabei von digit. Es ist äquivalent zum Ausdruck[0-9]
. -
\D
- Diese Zeichenklasse matched auf alles was keine Ziffer ist. Man beachte dass dasD
großgeschrieben ist. Es ist äquivalent zum Ausdruck[^0-9]
. -
\w
- Passt auf jedes Zeichen was laut Perl einem Word entspricht. Grundsätzlich sind das Ziffern, Buchstaben und der Unterstrich (_
), kann jedoch auch mehr sein. Je nach eingestelltem Locale können mehr Zeichen damit erfasst werden. Wenn das Locale aufde_DE
gestellt ist (möglich mit der Funktion setlocale) matched das\w
auch auf deutsch-spezifische Zeichen wie Umlaute. -
\W
- Negation von\w
und matched entsprechend auf alles was kein Word ist. -
\s
- Matched auf alles was kein sichtbares Zeichen ist, wie Leerzeichen oder Zeilenumbrüche. Dass
kommt dabei von Whitespace. -
\S
- Negation von\s
und matched entsprechend auf alle sichtbaren Zeichen. -
\b
- Diese Zeichenklasse ist eigentlich gar keine denn es matched nicht auf ein Zeichen sondern zwischen zwei Zeichen. In In diesem Fall matched es auf eine Wortgrenze (word boundary). Dies ist dann wichtig wenn man z.B. nach dem Wortarsch
sucht aber nichtMarschkapelle
treffen will.re> /\barsch\b/ data> du bist ein arsch, weiß du das? 0: arsch data> du bist sogar ein arschloch No match data> Hast du dir die Marschkapelle angeguckt? No match
Im ersten Beispiel wird zwar unser Wort gefunden, in den anderen Beispielen nicht, da die Wortgrenzen nicht passen.
-
.
- Diese Zeichen (der Punkt) matched auf alle Zeichen bis auf ein Zeilenumbruch (und mit dems
-modifier sogar auch auf den Zeilenumbruch). Dies wird später wichtig wenn man nicht auf ein einzelnes Zeichen matched sondern auf beliebig viele. -
\A
- Diese Zeichenklasse matched auf den Beginn des zu prüfenden Strings. Damit wird sichergestellt dass ein String mit dem Regex anfangen muss und nicht nur irgendwo beginnen braucht.re> /\Afoobar/ data> foobarbla 0: foobar data> blafoobar No match
Zuerst wurde der String
foobar
gefunden da er am Anfang steht. Beim zweiten Beispiel wurde der String nicht gefunden, obwohl er vorhanden ist, weil davor noch der Stringbla
steht. -
\z
- Diese Zeichenklasse matched auf den Ende des zu prüfenden Strings. In Kombination mit\A
kann man somit sicherstellen das ein kompletter String auf ein Regex matchen muss und nicht nur ein Teilstring irgendwo im String.re> /\Ablabli\z/ data> blabli 0: blabli data> fooblabli No match data> blablibli No match
Nur im ersten Beispiel wird der String gefunden da in den anderen Beispiel davor bzw. danach Text steht der nicht dort stehen darf.
8. Wiederholungen von Teilstrings
In der Regel sind die zu suchenden Strings länger als ein einzelnes Zeichen. Es kann sogar sein das der Text der gesucht wird unterschiedlich lang sein kann. Daher existiert in Regex die Möglichkeit nach Wiederholungen zu suchen, wie z.B. 20 Buchstaben oder beliebig viele Ziffern.
Um die Wiederholung von einem String festzulegen werden die
geschweiften Klammern {
und }
verwendet.
Innerhalb der Klammern geben ein oder zwei Zahlen die Anzahl der Wiederholungen
an. Somit bedeutet {5}
5 Wiederholungen und
{3,7}
3 bis 7 Wiederholungen. Die Angabe {0}
ist Möglich bewirkt jedoch meistens nicht das was gewollt ist denn
wie ganz am Anfang des Kapitels kann überall ein leerer String gefunden werden.
Wenn ein Bereich angegeben wird, jedoch eine Zahl weggelassen wird entspricht
dies einer offenen Grenze. So entspricht {5,}
mindestens 5 Wiederholungen
und {,8}
maximal 8 Wiederholungen.
re> /\d{2}-\d{2}-\d{2,4}/ data> 12-03-2008 0: 12-03-2008 data> Ich habe am 1-1-1970 Geburtstag No match data> Und ich am 45-23-301. 0: 45-23-301
Hier wurde nach einem Datum-typischen String gesucht. Man sieht auch das nicht alles was wie ein Datum aussieht auch wirklich ein Datum ist. Daher muss man ggf. die Daten die man ausgelesen hat später noch mit PHP prüfen.
Einige Wiederholungen kommen so oft vor dass es für diese sogar Abkürzungen gibt.
-
*
- Dies ist die Abkürzung für{0,}
und bedeutet entsprechend beliebige Wiederholungen von einem Suchmuster. -
+
- Dies ist die Abkürzung für{1,}
und bedeutet dass das Suchmuster beliebig oft vorkommen darf, jedoch mindestens ein mal. -
?
- Dies ist die Abkürzunge für{0,1}
und bedeutet dass ein Suchmuster vorkommen darf, aber nicht muss.
Diese Abkürzungen werden oft mit den Abkürzungen der Zeichenklassen verwendet.
So findet man in Regex oft solche Suchmuster wie .*
oder \d+
.
9. Gierige Suchmuster
Wenn ein Suchmuster unterschiedlich oft wiederholt werden kann versucht die Regex-Engine
den größtmöglichen Text zu finden, auch wenn ein kleinerere Text reichen würde.
Dieses Verhalten wird greedy genannt. Es kann jedoch sein dass
man genau dies nicht möchte bzw. fehl am Platz ist. Als Beispiel wollen wir HTML-Elemente
finden und würde da vielleicht den Regex /<.*>/
verwenden.
re> /<.*>/ data> foo<br />bar 0: <br /> data> Ein Text mit einem <em>hervorgehobenen</em> Wort. 0: <em>hervorgehobenen</em>
Im ersten Beispiel wurde zwar das HTML-Element, im zweiten Beispiel
jedoch auch Text der nicht erwünscht ist. Eine Möglichkeit wäre es nicht
die das .
-Zeichen zu verwenden sondern eine Zeichenklasse
die kein >
-Zeichen erlaubt. Eine weitere Möglichkeit wäre
die Regex-Engine von greedy auf ungreedy
zu stellen. Dies wird mit dem Modifier U
gesetzt.
re> /<.*>/U data> foo<br />bar 0: <br /> data> Ein Text mit einem <em>hervorgehobenen</em> Wort. 0: <em>
Die Greedy-Eigenschaft lässt sich bei *
und +
auch umdrehen wenn hinter den Abkürzungen ein weiteres ?
folgt.
re> /<.*?>/ data> foo<br />bar 0: <br /> data> Ein Text mit einem <em>hervorgehobenen</em> Wort. 0: <em>
Dies ist nötig wenn man mehrere solche Wiederholungen hat, jedoch nur einige greedy und andere ungreedy sein sollen.
10. Regex in PHP
Mit dem ganzen Vorwissen können wir nun Regex in PHP verwenden. Dabei werden meistens die Funktionen preg_match und preg_replace verwendet. Die Funktion preg_match wird verwendet wenn man gucken möchte ob ein Regex auf ein String passt, preg_replace wird verwendet wenn der gefundene String dann durch einen anderen String ersetzt werden soll.
Der erste Parameter der preg_match ist der Regex, der
zweite Parameter ist der Eingabestring der getestet werden soll. Da
der Regex in der Regel Backslashes enthällt (\
) sollte man
hier stehts single-quoted-Strings verwenden, das
spart ne Menge escaperei. Die Funktion liefert dann 0
zurück wenn der Regex nicht passt und 1
wenn der Regex
passt. Daher kann preg_match gut in einer
if-Abfrage verwenden.
<?php
if (!preg_match('~\A\d{2}-\d{2}-\d{4}\z~', '40-50-0001')) { // hier mal '~' als Delimiter verwendet
die('Bitte geben sie etwas ein was wie ein Datum aussieht.');
}
?>
Falls man Suchmuster einklammert um zu Gruppen zu erfassen kann
man als dritten Parameter eine Variable übergeben. Wenn der Regex
zum String passt wird in der Variable ein Array gespeichert, welches
die erfassten Gruppen erfasst, die wir oben mit 0
und 1
kennengelernt haben. Die Schlüssel entsprechen
dann den gefundenen Gruppen.
<?php
if (!preg_match('~Foo(.*)Bar~i', 'blablifoooooooblabaRblibar', $matches)) {
echo "nicht gefunden";
} else {
var_dump($matches);
}
?>
Dies gibt folgendes aus.
array(2) { [0]=> string(20) "foooooooblabaRblibar" [1]=> string(14) "oooooblabaRbli" }
Hier sieht man die Gruppen 0
und 1
wie sie auch
pcretest
finden würde.
Mit der Funktion preg_replace geht man noch ein Schritt weiter und ersetzt jeden gefundenen Teilstring durch einen anderen String. Insbesondere kann gefundener Text beim ersetzen beibehalten werden.
<?php
$Text = 'Ein sehr Langer Text mit "besonderen" Markierungen, die dann "besonders" behandelt werden.';
$Regex = '~"(.*)"~U';
$Ersetzen = '<em>\1</em>';
$Text = preg_replace($Regex, $Ersetzen, $Text);
echo $Text;
// Gibt folgendes aus
// Ein sehr Langer Text mit <em>besonderen</em> Markierungen, die dann <em>besonders</em> behandelt werden.
?>
Auf die gefundenen Strings kann man mit \$Nummer
zugreifen, wobei
dann entsprechend die Nummern die Gruppen sind, die man gefunden hat. Beachtet das
für einfache Textersetzungen man kein preg_replace braucht. Wenn
man z.B. alle e
's durch E
's ersetzen will reicht es
Dicke wenn man die str_replace-Funktion verwendet und nicht
die Regex-Engine anwerfen muss.
11. Die Welt der Regex
Dieses Kapitel ist nur ein Grundwissen über Regex. Im Handbuch zu Regular Expressions (Perl-Compatible) sowie auf diversen Internetseiten wie in der Wikipedia zu Regular expression finden sich detailiertere Informationen wie man mit Regex umgeht und was alles noch möglich ist.