Regular Expressions mit PHP

Bei der Web-Entwicklung hat man es selten mit mathematischen Problemen zu tun. Meistens geht es darum, Texte in der einen oder anderen Weise zu manipulieren. Dabei sind die Regular Expressions ein enorm leistungsfähiges Werkzeug. Diese Seite befasst sich mit diesem - auf den ersten Blick nicht ganz so übersichtlichen - leistungsstarken Werkzeug für das Suchen und Ersetzen von Textteilen.

Regular Expressions (oder zu deutsch "Reguläre Ausdrücke") gehen weit über das hinaus, was man von den Suchmöglichkeiten einiger Editoren und Textverarbeitungsprogrammen kennt. Man kann sehr flexible Suchbedingungen formulieren, so dass man an Regular Expressions bei der regelmässigen Arbeit mit Programmiersprachen nicht vorbeikommt bzw. man sich damit die Arbeit extrem erleichtern kann.

Sonderzeichen

Bei Regular Expressions gibt es einige Sonderzeichen, welche in der folgenden Tabelle kurz aufgeführt werden.

. Beliebiges Zeichen
\d Ziffer (0-9), auch schreibbar als [0-9]
\D Keine Ziffer (alles ausser 0-9), auch schreibbar als [^0-9]
\w Alphanumerische Zeichen inkl. _ und Zahlen aber ohne Umlaute, auch schreibbar als [a-zA-Z0-9_]
\W Nicht-Alphanumerisches Zeichen, auch schreibbar als [^a-zA-Z0-9_]
\s Leerzeichen und andere White-Spaces (\n \r \t \f [FormFeed]), auch schreibbar als [ \t\r\n\f]
\S Kein Leerzeichen, auch schreibbar als [^ \t\r\n\f]
^ oder \A Beginn des Strings
$ oder \Z String-Ende
[] Auswahlmöglichkeiten einzelner Zeichen / ODER
[^] Negative Auswahlmöglichkeiten einzelner Zeichen / NICHT ODER
() Kopieren in "Zwischenablage" (mehr dazu weiter unten in diesem Kapitel)
\. \[ \] \( \) \{ \} \? \+ Die Zeichen . [ ] ( ) { } ? +
\n \r \t haben ihre normale Bedeutung (newline, carriage return, tab)

Zudem gibt es noch spezielle Tags für Mengenangaben, wie oft ein Zeichen hintereinander vorkommen muss.

? Kein- oder einmal, {0,1}
* Keinmal bis beliebig oft {0,} (gierig; später mehr)
+ Ein- oder mehrmals {1,} (gierig; später mehr)
*? Keinmal bis beliebig oft {0,}? (nicht gierig; später mehr)
+? Ein- oder mehrmals {1,}? (nicht gierig; später mehr)
{7} siebenmal
{3,5} Drei- bis fünfmal
{4,} Viermal oder mehr

ab?c bedeutet also, dass im Text ein "a" (einmal), ein "b" (keinmal oder einmal) und ein "c" vorkommen muss, also "abc" oder "ac".

a.*z bedeutet, dass im Text ein "a" (einmal), dann ein beliebiges Zeichen (gar nicht, einmal oder mehrmals) und ein "z" vorkommen muss, also z.B. "az", "abz", "abcdefghijklm...z" (keine vollständige Aufzählung).

In der oberen Tabelle haben wir gesehen, dass Regular Expressions zwischen "gierig" und "nicht gierig" unterscheiden. In der Standard-Einstellung sind sie sog. gierig, d.h. ein * oder + versucht immer so viele Zeichen wie möglich zu nehmen, so dass die Regular Expression noch maximal möglich ist. Hängt man an das + oder * noch ein Fragezeichen an (+? bzw. *?), hat man das umgekehrte Ergebnis: der Ausdruck nimmt nun nur so viele Zeichen wie absolut notwendig, um die Regular Expression noch gültig zu machen. gierig/nicht gierig ist in erster Linie im Zusammenhand mit der Zwischenablage (Klammern) notwendig, um zu definieren, wie viel Text in die entsprechende Zwischenablage kopiert werden soll. Die Beispiele am Ende der Seite zeigen die Verwendung anhand eines konkreten Beispiels.

Aufbau von Regular-Expressions

Nun setzen wir uns einmal mit dem groben Aufbau einer Regular-Expression auseinander. Dazu ein kleines Schema:

preg_match("/Suchpattern/Parameter", $var); // prüft, ob Suchpattern in $var vorkommt
$var = preg_replace("/Suchtext/Parameter", "Ersatztext", $var); // ersetzt Suchtext durch Ersatztext

Als Parameter seien hier vor allem zwei erwähnt: i ignoriert Gross-/Kleinschreibung und s führt dazu, dass der "." (beliebiges Zeichen) auch für Zeilenumbrüche "\n" gilt.

Das Suchpattern wird mit einem Zeichen eingeleitet und vor den Parameter mit dem identischen Zeichen abgeschlossen. Viele Programmiersprachen verwenden hier fix ein /, weshalb sich das Zeichen als Standard durchgesetzt hat, bei PHP kann es theoretisch ein beliebiges nicht alphanumerisches Zeichen sein. Wird es im Pattern verwendet, muss es als \/ geschrieben werden.

preg_match("/e/i", $variable)
preg_match("%e%i", $variable)
preg_match("=e=i", $variable)

preg_match("/a\/b/i", $variable)
preg_match("%a/b%i", $variable)

Die ersten drei Zeilen finden also ein "e" in $variable, die letzten zwei ein a/b. Im oberen der zwei Beispiele ist das Trennzeichen ein /, weshalb das / im Suchpattern als \/ geschrieben werden muss. Im unteren Beispiel ist das Trennzeichen ein %, somit kann das / im Suchpattern 1:1 geschrieben werden. Alle folgenden Beispiele auf dieser Seite verwenden das / als Trennzeichen.

Suchen

Genug Theorie. Wollen wir das mal an einem Beispiel anschauen:

preg_match("/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/", $variable);

Dieses Beispiel überprüft, ob $variable vier Zahlenblöcke mit ein bis drei Stellen enthält, getrennt durch einen Punkt (z.B. IP-Adresse). Hier sieht man: ein Punkt muss als \. geschrieben, da der Punkt alleine ein beliebiges Zeichen darstellt.

preg_match("/^[abc]/i", $variable);

Dieses Suchpattern ist wahr, wenn $variable mit a, b, c, A, B oder C anfängt.

preg_match("/(foo|bar)/", $variable);

In diesem Beispiel bezieht sich das Suchpattern nicht nur auf einzelne Zeichen (wie im oberen Beispiel mit [abc]) sondern auf ganze Wörter: die Bedingung ist in diesem Fall wahr, wenn $variable das Wort "foo" und/oder "bar" enthält (an beliebiger Stelle).

preg_match("/\.jpe?g$/", $variable);

Hier wird überprüft, ob $variable mit dem Text ".jpg" oder ".jpeg" endet (jedoch NICHT ".JPG"!).

preg_match("/^[a-z0-9\-_\.]+\@[a-z0-9\-_\.]+\.[a-z]{2,}$/i", $variable);

Hierbei handelt es sich um einen primitiven Check, ob $variable eine gültige Mailadresse enthält. In der Praxis müsste dies noch etwas erweitert werden (z.B. Domainnames mit Umlauten).

Die Regular Expression kann natürlich in eine if-Abfrage eingebettet werden, um anhand des Ergebnisses (gefunden/nicht gefunden) eine Aktion auszuführen:

$variable="Dies ist ein Test";
if (preg_match("/test/i", $variable)) {
  echo "enthält TEST";
} else {
  echo "enthält nicht TEST";
}

Zwischenablage / gierig

Regular Expresions bieten eine Zwischenablage, d.h. bestimmte Teile der Regular Expressions (bzw. deren Ergebnisse) können in Klammern gesetzt werden. Der Teil des Strings, welcher auf den Teil in Klammern zutrifft steht dann einem Array zur Verfügung. Klammern können auch geschachtelt werden, die Reihenfolge der Variablen wird dann anhand der öffnenden Klammern definiert. Ziemlich komplex, oder? Schauen wir das anhand eines Beispiels an:

$variable="Dies ist ein Text zur Demonstration";
preg_match("/\s(....)\s/", $variable, $matches);
echo "Erstes Wort mit 4 Buchstaben zwischen zwei Leerzeichen: ".$matches[1]."\n";

Was macht diese Regular Expression? Lassen wir mal die Klammern weg: gesucht wird also nach "\s....\s", d.h. einem Leerzeichen (bzw. etwas genauer gesagt "Whitespace", also auch Tabulator, Zeilenumbruch), dann nach 4 Zeichen (könnte auch mit ".{4}" geschrieben werden) und dann wieder einem Whitespace. In unserem Beispieltext trifft dies auf das Wort "Text" zu ("Dies" hätte zwar auch 4 Buchstaben, aber kein Leerzeichen am Anfang). Da die 4 Punkte in Klammern gesetzt wurden, wird das Wort, welches hier steht in die Zwischenablage kopiert und kann danach mit dem Array $matches (drittes Argument, Name des Arrays frei definierbar) ausgegeben werden.

Versuchen wir es nochmals:

$variable="Dies ist nur ein Text, welchen wir zum testen verwenden wollen...";
preg_match("/^(.*),/", $variable, $matches);
echo "Text bis zum Komma: ".$matches[1]."\n";

Auch diesen regulären Ausdruck wollen wir in Ruhe analysieren: wie beginnen am Anfang des Strings (^) und kopieren beliebige Zeichen (.*), bis wir ein Komma erhalten. Die beliebigen Zeichen (nicht aber das Komma) haben wir in Klammern gesetzt, so dass wir sie unter $matches[1] zur Verfügung haben. Das Ergebnis in $matches[1] ist somit auch "Dies ist ein Text".

Dieses Beispiel bringt uns aber gleich zum 2. Thema dieses Abschnitts: was passiert, wenn der Text mehrere Kommas hat? Hier kommt das Fragezeichen hinter dem + oder * zum tragen: fehlt dieses, wird möglichst viel verwendet, ist es vorhanden möglichst wenig. Schauen wir das an oberem Beispiel an:

$variable="Dies ist nur ein Text, welchen wir zum testen verwenden wollen, mal sehen, ob die Theorie funktioniert...";
preg_match("/^(.*),(.*)$/", $variable, $matches);
echo "Text bis zum Komma (gierig): ".$matches[1]."\n";
// Dies ist nur ein Text, welchen wir zum testen verwenden wollen, mal sehen
echo "... und der Schluss: ".$matches[2]."\n";
// ob die Theorie funktioniert....
preg_match("/^(.*?),(.*)$/", $variable, $matches);
echo "Text bis zum Komma (nicht gierig): ".$matches[1]."\n";
// Dies ist nur ein Text
echo "... und der Schluss: ".$matches[2]."\n";
// welchen wir zum testen verwenden wollen, mal sehen, ob die Theorie funktioniert....

Im ersten Fall wurde also die erste Klammer gierig belassen, d.h. $matches[1] enthält soviel Text wie nur irgendwie möglich, um die Regular Expression trotzdem zu erfüllen. Dies ist der Text bis zum letzten Komma. Obwohl auch der Ausdruck nach dem Komma gierig belassen wurde, bleibt in $matches[2] dann nur noch der Text nach dem letzten Komma (ohne das Komma selber, wohl aber mit dem folgenden Leerzeichen) bis zum Schluss.

Im zweiten Fall hingegen wurde durch ein einzelnes ? (also .*? statt .*) der Ausdruck als nicht gierig deklariert. $matches[1] enthält deshalb nur den Text bis zum ersten Komma. Den kompletten folgenden Text bis zum Ende hat er dann der zweiten Klammer und somit $matches[2] überlassen.

Nun widmen wir uns aber gleich mal einem konkreten und etwas komplizierterem Beispiel (auch wenn dieses Beispiel nicht mal ein Prozent der Möglichkeiten von Regular Expressions darstellt). Ziel des folgenden Beispiels soll es ein, einen String mit Landeskennzeichen, PLZ und/oder Ort (wobei alles freiwillig ist) in drei Variablen aufzuteilen. Dabei stecken wir die Regular-Expression in eine Funktion.

list($land, $plz, $ort)=splitLand("CH-8523 Hagenbuch ZH");
echo "Land: ".$land." / PLZ: ".$plz." / Ort: ".$ort,"\n";
// Land: CH / PLZ: 8523 / Ort: Hagenbuch ZH

list($land, $plz, $ort)=splitLand("DE-10673 Berlin");
echo "Land: ".$land." / PLZ: ".$plz." / Ort: ".$ort,"\n";
// Land: DE / PLZ: 10673 / Ort: Berlin

list($land, $plz, $ort)=splitLand("9999 Mustershausen");
echo "Land: ".$land." / PLZ: ".$plz." / Ort: ".$ort,"\n";
// Land: / PLZ: 9999 / Ort: Mustershausen

list($land, $plz, $ort)=splitLand("DE-Berlin");
echo "Land: ".$land." / PLZ: ".$plz." / Ort: ".$ort,"\n";
// Land: DE / PLZ: / Ort: Berlin

function splitLand($text) {
  if (preg_match("/^[A-Z]+-/", $text)) {  // Landeskennzeichen vorhanden
    preg_match("/^([A-Z]+)-(\d*)\s*(.*)$/", $text, $matches);
    return array($matches[1], $matches[2], $matches[3]);
  } else {  // kein Landeskennzeichen
    preg_match("/^(\d*)\s*(.*)$/", $text, $matches);
    return array('', $matches[1], $matches[2]);
  }
}

list($land, $plz, $ort)=splitLand("9999 Mustershausen"); übergibt der Funktion "splitLand" einen String, welcher innerhalb der Funktion dann unter $text zur Verfügung steht. Die Rückgabewerte der Funktion (return "Land", "PLZ", "Ort";) stehen dann in den drei Variablen $land, $plz, $ort (in der Praxis eher ein Array) zur Verfügung und werden in der darauf folgenden Zeile ausgegeben.

Nun aber zu den Regular Expressions:

  • if (preg_match("/^[A-Z]+-/", $text)) {
    prüft, ob der String mit einem oder mehreren Grossbuchstaben, gefolgt von einem Bindestrich beginnt, also ein Landeskennzeichen enthält.
  • preg_match("/^([A-Z]+)-(\d*)\s*(.*)$/", $text, $matches);
    ist etwas komplizierter: Die Grossbuchstaben am Anfang bis zum ersten Bindestrich (jedoch ohne denselben) werden in $matches[1] kopiert. Danach folgt eine beliebige Anzahl Zahlen (evtl. auch keine), welche in $matches[2] kopiert werden ((\d*)). Nach den Zahlen folgt eine beliebige Anzahl (auch wieder keine möglich) Leerzeichen (\s*), bevor dann der Rest des Strings bis zum Ende ($) in die Variable $matches[3] kopiert wird.
  • return array($matches[1], $matches[2], $matches[3]);
    Diese Zeile ist dann selbsterklärend: $matches[1] enthält das Landeskennzeichen, $matches[2] die PLZ (kann auch leer sein, da \d* auch keine Zahl sein darf), $matches[3] dann den Rest des Strings (also den Ort).
  • preg_match("/^(\d*)\s*(.*)$/", $text, $matches);
    Diese Zeile folgt nach dem "else", geht also davon aus, dass kein Landeskennzeichen vorhanden ist. Der Syntax entspricht aber dem oberen Beispiel: vom Beginn des Strings werden alle (0 bis x) Zahlen nach $matches[1] kopiert, dann folgen kein, ein oder mehrere Leerzeichen und der Rest des Strings wird nach $matches[2] kopiert.
  • return array('', $matches[1], $matches[2]);
    Diese Zeile ist ebenfalls leicht verständlich: Landeskennzeichen gibt es keines, also '', $matches[1] enthält die PLZ, $matches[2] den Ort.

Alles in allem ist diese Funktion schon sehr gut geeignet, um Landeskennzeichen, PLZ und Ort zu trennen.

Ersetzen

Nachdem wir das Suchen nun zur genüge behandelt haben, bereitet uns das Ersetzen keine grossen Probleme mehr. Hier wird die Funktion preg_replace anstelle von preg_match verwendet und das zweite Argument ist der Ersetz-String.

Auch hier zuerst ein ganz einfaches Beispiel:

$variable="Demo-Text zum testen";
$variable = preg_replace("/e/", "i", $variable);
echo "Nach Ersetzung: ".$variable."\n"; // Dimo-Tixt zum tistin

$variable="Demo-Text zum testen";
$variable = preg_replace("/t/", "x", $variable);
echo "Nach Ersetzung: ".$variable."\n"; // Demo-Texx zum xesxen

$variable="Demo-Text zum testen";
$variable = preg_replace("/t/i", "x", $variable);
echo "Nach Ersetzung: ".$variable."\n"; // Demo-xexx zum xesxen

Die Ergebnisse dürften wohl kaum überraschen. Erwähnt sei noch, dass "i" zwar die Gross-/Kleinschreibung ignoriert, das Ersetz-Pattern jedoch nicht in der Gross-/Kleinschreibung anpasst, "t" und "T" werden durch "x" ersetzt und NICHT "t" durch "x" und "T" durch "X"!

Nun aber doch noch ein paar originellere Beispiele:

$variable="Dies ist ein <b>Text</b> mit <i>HTML</i>-Kennzeichen";
$variable = preg_replace("/<.*?>/", "", $variable);
echo "Nach Ersetzung: ".$variable."\n";
// Dies ist ein Text mit HTML-Kennzeichen

Dieses Beispiel löscht alle HTML-Kennzeichnungen innerhalb eines Textes. Also von einem "<" eine beliebige Anzahl Zeichen bis zum nächsten ">" durch nichts ersetzen. Wichtig ist hier das *?, also die Definition von nicht gierig, da ansonsten der Text vom ersten "<" bis und mit dem letzten ">" ersetzt werden würde:

$variable="Dies ist ein <b>Text</b> mit <i>HTML</i>-Kennzeichen";
$variable = preg_replace("/<.*>/", "", $variable);
echo "Nach Ersetzung: ".$variable."\n";
// Dies ist ein -Kennzeichen

Was ein Fragezeichen alles bewirken kann...

$variable="Dies ist ein <b>Text</b> mit <i>HTML</i>-Kennzeichen";
$variable = preg_replace("/^.*?>(.*?)<.*$/", "\\1", $variable);
echo "Nach Ersetzung: ".$variable."\n";
// Text

Hier greift unsere Regular-Expression auf den ganzen String (^ bis $), d.h. es wird auch alles ersetzt. Den Text innerhalb des ersten HTML-Tags ("Text") wird dabei in die Zwischenablage kopiert und schlussendlich ersetzt. $variable enthält also nur noch den Wert der ersten Klammer, welcher direkt im Ersetzen in \\1 gespeichert wird.

Über Regular Expressions könnte man ganze Tutorials schreiben (wurde auch schon des öfteren gemacht). Was ich hier aufzeige ist nicht der komplette Umfang von Regular Expressions, jedoch das, was man bei der täglichen Arbeit immer wieder braucht. Wer es ganz genau wissen will, nimmt am besten mal ein echtes Perl-Buch zur Hand oder macht sich im Netz mal auch die Suche. Unter https://www.regexpal.com/ und https://www.debuggex.com/ können Regular Expressions online testet werden. Hier kann man seine eigenen Regular-Expressions an einem eigenen Demo-Text testen, ideal für die ersten Versuche.