cgicorner.ch

Informationen rund um Perl/CGI

Sie sind hier: Home > CGI Hilfe > Tutorial > Kapitel 11 - Regular Expressions

Tutorial - Kapitel 11: Regular Expressions

Voraussetzungen für dieses Kapitel

  • Webserver mit Perl/CGI-Unterstützung

Einleitung

Wenn jemand von Perl spricht, so fällt häufig im gleichen Atemzug auch das Wort "Regular Expression". Regular Expressions sind mitunter ein Grund, warum Perl im Internet weit verbreitet ist (PHP unterstützt übrigens ebenfalls Regular Expressions, jedoch mit einem leicht anderen Syntax). Hier 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 Tool. Dieses Kapitel 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 Perl nicht vorbeikommt.

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 \t \r \f haben ihre normale Bedeutung

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 Perl zwischen "gierig" und "nicht gierig" unterscheidet. Perl ist in der Standard-Einstellung 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: Perl nimmt nun nur soviele 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 des Kapitels 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:

$var =~ m/Suchpattern/Parameter;         # Prüft, ob Suchpattern in $var vorkommt
$var =~ s/Suchtext/Ersatztext/Parameter; # Ersetzt Suchtext durch Ersatztext

=~ leitet eine Regular-Expression ein. / dient als Trennzeichen.

Als Parameter seien hier vor allem drei erwähnt: i ignoriert Gross-/Kleinschreibung, g ersetzt alle Vorkommnisse des Suchmusters (ohne Parameter g jeweils nur das erste Vorkommnis). s führt dazu, dass der "." (beliebiges Zeichen) auch für Zeilenumbrüche "\n" gilt.

Ein weiterer Parameter ist e: dies bedeutet, dass der Ersatz-String zuerst von Perl verarbeitet wird. Dies ermöglicht die Verwendung von Funktionen. Beispiel: $test =~ s/von(.*)bis/&meineFunktion($1)/e;. Dies heisst, dass der Text zwischen "von" und "bis" der Funktion &meineFunktion als Parameter übergeben wird und dieser Text durch den Rückgabewert der Funktion ersetzt wird.

Ebenfalls kann man eine Regular Expression mit !~ einleiten. Dann liefert sie True zurück, wenn der Ausdruck nicht zutrifft (NOT).

Suchen

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

$variable =~ m/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/;

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.

$variable =~ m/^[abc]/i;
Dieses Suchpattern ist wahr, wenn $variable mit a, b, c, A, B oder C anfängt.

$variable =~ m/(foo|bar)/;

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

$variable =~ m/\.jpe?g$/;

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

$variable =~ m/^[a-z0-9\-_\.]+\@[a-z0-9\-_\.]+\.[a-z]{2,}$/i;

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:

my $variable="Dies ist ein Test";
if ($variable =~ m/test/i) {
  print "enthält TEST"; # wird ausgegeben
} else {
  print "enthält nicht TEST";
}

Zwischenablage / gierig

Perl bietet 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 in $1, $2, $3 etc. 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:

my $variable="Dies ist ein Text zur Demonstration";
$variable=~ m/\s(....)\s/;
print "Erstes Wort mit 4 Buchstaben zwischen zwei Leerzeichen: $1\n";  # Text

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 $1 ausgegeben werden.

Versuchen wir es nochmals:

my $variable="Dies ist nur ein Text, welchen wir zum testen verwenden wollen...";
$variable=~ m/^(.*),/;
print "Text bis zum Komma: $1\n"; # Dies ist nur ein Text

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 $1 zur Verfügung haben. Das Ergebnis in $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:

my $variable="Dies ist nur ein Text, welchen wir zum testen verwenden wollen,
mal sehen, ob die Theorie funktioniert...";
$variable=~ m/^(.*),(.*)$/;

print "Text bis zum Komma (gierig): $1\n";
# Dies ist nur ein Text, welchen wir zum testen verwenden wollen, mal sehen

print "... und der Schluss: $2\n"; #  ob die Theorie funktioniert....
$variable=~ m/^(.*?),(.*)$/;
print "Text bis zum Komma (nicht gierig): $1\n"; # Dies ist nur ein Text
print "... und der Schluss: $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. $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 $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. $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 $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 Subroutine.

($land, $plz, $ort)=&splitLand("CH-8523 Hagenbuch ZH");
print "Land: $land / PLZ: $plz / Ort: $ort\n";
# Land: CH / PLZ: 8523 / Ort: Hagenbuch ZH

($land, $plz, $ort)=&splitLand("DE-10673 Berlin");
print "Land: $land / PLZ: $plz / Ort: $ort\n";
# Land: DE / PLZ: 10673 / Ort: Berlin

($land, $plz, $ort)=&splitLand("9999 Mustershausen");
print "Land: $land / PLZ: $plz / Ort: $ort\n";
# Land: / PLZ: 9999 / Ort: Mustershausen

($land, $plz, $ort)=&splitLand("DE-Berlin");
print "Land: $land / PLZ: $plz / Ort: $ort\n";
# Land: DE / PLZ: / Ort: Berlin

sub splitLand {
  if ($_[0] =~ m/^[A-Z]+-/) {	# Landeskennzeichen vorhanden
    $_[0] =~ m/^([A-Z]+)-(\d*)\s*(.*)$/;
    return $1, $2, $3;
  } else {	# kein Landeskennzeichen
    $_[0] =~ m/^(\d*)\s*(.*)$/;
    return "", $1, $2;
  }
}

Gehen wir mal ganz der Reihe nach: zuerst repetieren wir den Aufruf der Funktion und deren Rückgabewerte (siehe Kapitel 7: Subroutinen).

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

Nun aber zu den Regular Expressions:

  • if ($_[0] =~ m/^[A-Z]+-/)
    prüft, ob der String mit einem oder mehreren Grossbuchstaben, gefolgt von einem Bindestrich beginnt, also ein Landeskennzeichen enthält.
  • $_[0] =~ m/^([A-Z]+)-(\d*)\s*(.*)$/;
    ist etwas komplizierter: Die Grossbuchstaben am Anfang bis zum ersten Bindestrich (jedoch ohne denselben) werden in $1 kopiert. Danach folgt eine beliebige Anzahl Zahlen (evtl. auch keine), welche in $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 $3 kopiert wird.
  • return $1, $2, $3;
    Diese Zeile ist dann selbsterklärend: $1 enthält das Landeskennzeichen, $2 die PLZ (kann auch leer sein, da \d* auch keine Zahl sein darf), $3 dann den Rest des Strings (also den Ort).
  • $_[0] =~ m/^(\d*)\s*(.*)$/;
    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 $1 kopiert, dann folgen kein, ein oder mehrere Leerzeichen und der Rest des Strings wird nach $2 kopiert.
  • return "", $1, $2;
    Diese Zeile ist ebenfalls leicht verständlich: Landeskennzeichen gibt es keines, also "", $1 enthält die PLZ, $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. Nochmals zum Syntax:

$variable =~ s/Suchstring/Ersatzstring/Parameter  # Ersetzt Suchstring durch Ersatzstring

Als Parameter haben wir "i" (Gross-/Kleinschreibung ignorieren) und "g" (mehrmals ersetzen, falls möglich).

Auch hier zuerst ein ganz einfaches Beispiel:

my $variable="Demo-Text zum testen";
$variable=~ s/e/i/;
print "Nach Ersetzung: $variable\n"; # Dimo-Text zum testen

my $variable="Demo-Text zum testen";
$variable=~ s/e/i/g;
print "Nach Ersetzung: $variable\n"; # Dimo-Tixt zum tistin

my $variable="Demo-Text zum testen";
$variable=~ s/t/x/;
print "Nach Ersetzung: $variable\n"; # Demo-Texx zum testen

my $variable="Demo-Text zum testen";
$variable=~ s/t/x/g;
print "Nach Ersetzung: $variable\n"; # Demo-Texx zum xesxen

my $variable="Demo-Text zum testen";
$variable=~ s/t/x/ig;
print "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. s/t/x/g; ersetzt also "t" und "T" durch "x" und NICHT "t" durch "x" und "T" durch "X"!

Nun aber doch noch ein paar originellere Beispiele:

my $variable="Dies ist ein <b>Text</b> mit <i>HTML</i>-Kennzeichen";
$variable=~ s/<.*?>//g;
print "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:

my $variable="Dies ist ein <b>Text</b> mit <i>HTML</i>-Kennzeichen";
$variable=~ s/<.*>//g;
print "Nach Ersetzung: $variable\n"; # Dies ist ein -Kennzeichen

Was ein Fragezeichen alles bewirken kann...

my $variable="Dies ist ein <b>Text</b> mit <i>HTML</i>-Kennzeichen";
$variable=~ s/^.*?>(.*?)<.*$/$1/g;
print "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 von $1.

An dieser Stelle auch noch meine ganz persönlich Lieblings-Regex: diese lässt sich wunderbar verwenden, um ein Template zu parsen und dann auszugeben. Da das Script relativ gross ist, habe ich es auf einer eigenen Seite abgebildet.

Bemerkung zu diesem Kapitel

Über Regular Expressions könnte man eigene 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. Ein weiteres Tutorial zu diesem Thema findet man beispielsweise auch unter: http://de.selfhtml.org/perl/sprache/regexpr.htm. Weitere geniale Seiten zum testen von Perl Regular Expressions sind http://www.regexpal.com und http://erik.eae.net/playground/regexp/regexp.html: hier kann man seine eigenen Regular-Expressions an einem eigenen Demo-Text testen, ideal für die ersten Versuche.

Wie geht's weiter?

Im Kapitel 12 dreht sich alles rund im Perl und MySQL. Dabei lernen wir zuerst die wichtigsten SQL-Befehle kennen und widmen uns dann der Anbindung von Perl an MySQL.

Im vorherigen Kapitel (10) beschäftigten wir mit Mails.

Sie können aber auch zurück zum Inhaltsverzeichnis und dort ein beliebiges anderes Kapitel auswählen.