Die Webseiten der Fachschaft Informatik am ERG Saalfeld


Reguläre Ausdrücke

Es soll hier Schritt für Schritt die Nutzung von regulären Ausdrücken in Perl erklärt werden. Zum Nacharbeiten bzw. Wiederholen reicht es sicherlich, in der Zusammenfassung die benötigten Dinge zu suchen.

  1. einfache Zeichensuche
  2. Suche nach Sonderzeichen
  3. Zeichenklassen
  4. Besondere Zeichenklassen (z.B. \w)
  5. Quantoren
  6. Alternativen
  7. Ankerpunkte (hier nur Zeilenanfang und -ende)
  8. Optionen
  9. Teilausdrücke merken
  10. Capturing vs. Clustering
  11. Teilausdrücke rückwärts referenzieren
  12. Gier

 

Bei der Verwendung von regulären Ausdrücken geht es darum, dass eine Regex auf einen String passt oder eben nicht. Man sagt dann dazu, die Regex "matcht" mit dem String. Als Operator für das Binden an die Regex wird =~ verwendet. Die Regex wird in 2 Slashs geschrieben. z.B. so: $string =~ m/^.*$/, das "m" vor den Slash bedeutet "matchen". Wenn die Regex durch 2 Slashs begrenzt wird, kann das "m" weggelassen werden.
 

einfache Zeichensuche

Bsp.: es sollen mehrere Strings daraufhin untersucht werden, ob sie die Buchstabenfolge 'erg' enthalten. Die Regex können wir so schreiben:

$string =~ /erg/;


Wenn die Regex passt (matcht), dann soll ausgegeben werden 'erg gefunden', sonst 'erg nicht gefunden'. Die if-Anweisung sieht dazu so aus:

if ($string =~ /erg/) {
  print 'erg gefunden', "\n";
}
else {
  print 'erg nicht gefunden', "\n";
};


Das Programm sieht dann z.B. so aus:

#!/usr/bin/perl
use strict;
use warnings;

my @liste = qw(Hilfe Werg mergen ERG-Slf erg-slf);

foreach my $string (@liste) {
  if ($string =~ /erg/) {
    print $string, "\t - ", 'erg gefunden', "\n";
  }
  else {
    print $string, "\t - ", 'erg nicht gefunden', "\n";
  };
}


Der Aufruf sah dann bei mir so aus:

 

Suche nach Sonderzeichen

Nach Zeichen, die in einer Regex eine besondere Bedeutung haben, kann nur dann gesucht werden, wenn man diese maskiert. Dazu wird vor diese Zeichen ein Backslash "\" gestellt. Solche Sonderzeichen sind: . ? * + ^ $ | \ / ( ) [ {

Ein Beispiel (Suche nach zwei Slashs):

$string =~ /\/\//;


Auch hier ein Beispielprogramm dazu:

#!/usr/bin/perl
use strict;
use warnings;

my @liste = ('www.saalfeld.de', 'http://www.saalfeld.de');

foreach my $string (@liste) {
  if ($string =~ /\/\//) {
    print $string, "\t  ", '// gefunden', "\n";
  }
  else {
    print $string, "\t\t  ", '// nicht gefunden', "\n";
  };
}


Der Aufruf sah dann bei mir so aus:

 

Zeichenklassen

Wir wollen aus einer Liste von Zeichen alle Zeichen herausfinden, die für römische Zahlen verwendet werden. Das sind M (für 1000), D (für 500), C (für 100), L (für 50), X (für 10), V (für 5) und I (für 1). Das bedeutet, wir suchen nach den Großbuchstaben M, D, C, L, X, V und I. Das schreibt man als Zeichenklasse so: [MDCLXVI]. Die Regex:

$char =~ /[MDCLXVI]/;

bedeutet, dass das Zeichen auf eines dieser Buchstaben passt oder eben nicht.

Wir wollen jetzt aus einer Liste von Strings alle zweistelligen Zahlen heraussuchen, die einen Tag angeben können. Da der Tag zweistellig ist, bedeutet das, er kann am Anfang eine 1 oder eine 2 oder eine 3 haben (der Januar hat ja z.B. 31 Tage). Die zweite Ziffer kann von 0 bis 9 gehen. Die Regex dazu kann man so schreiben:

$tag =~ /[123][0123456789]/;

Das lässt sich auch so (kürzer) schreiben:

$tag =~ /[1-3][0-9]/;

Dadurch, dass der Bindestrich in einer Zeichenklasse eine Sonderbedeutung hat, muss ein Bindestrich, falls er zu den Zeichen einer Zeichenklasse gehören soll, entweder am Anfang oder am Ende geschrieben werden. z.B. so

$char =~ /[a-z-]/;

Diese Regex matcht, wenn es ein Kleinbuchstabe (von a bis z) oder ein Bindestrich ist.

Die folgende Regex (mit Zirkumflex)

$char =~ /[^a-z-]/;

bedeutet, dass nach allen Zeichen gesucht wird, die nicht auf einen Kleinbuchstaben oder einen Bindestrich passen. Das bedeutet, mit dieser Regex werden Ziffern, Großbuchstaben, Leerzeichen, Punkt, Semikolon u.a.m. gefunden.

 

Besondere Zeichenklassen

Für häufig verwendete Zeichenklassen gibt es folgende Abkürzungen.

    Zeichen           Entsprechung           Bedeutung  
     \d  [0-9]  Ziffer
     \D  [^0-9]  Gegenstück zu \d
     \w  [a-zA-Z_0-9]  Buchstabe, Unterstrich oder Ziffer
     \W  [^a-zA-Z_0-9]  Gegenstück zu \w
     \s  [ \t\n\f\r]  Whitespace
     \S  [^ \t\n\f\r]  Gegenstück zu \s

 

Quantoren

Es wird in den seltensten Fällen nach einzelnen Zeichen gesucht, sondern nach Mustern aus mehreren/vielen Zeichen. Die Anzahl der Zeichen kann so angegeben werden:

    Quantor     Bedeutung       Beispiel     matcht mit
     ? Zeichen tritt einmal auf oder tritt nicht auf   /https?:/   https:  oder  http:
     * Zeichen tritt keinmal, einmal oder mehrmals auf   /shop\s*center/   shopcenter  oder  shop center  oder  shop     center
     + Zeichen tritt mindestens einmal auf (einmal oder mehrmals)   /0,\d+/   0,7  oder  0,314  oder  0,00301


Klammerschreibweise: Es gibt durchaus Fälle, wo die Verwendung von ?, * und + nicht präzise genug ist. Hier steht dann die Klammerschreibweise für eine Bereichsangabe zur Verfügung. Ein Beispiel: \d{2,5} bedeutet, mindestens 2 Ziffer, höchstens 5 Ziffern. Damit ergeben sich z.B. auch für ?, * und + andere Schreibweisen.

    Abkürzung     Entsprechung           Bedeutung            
     {n}   {n,n} Zeichen tritt genau n-mal auf
     {n,}   Zeichen tritt mindestens n-mal auf
     {,n}   Zeichen tritt höchstens n-mal auf
     ?   {0,1} Zeichen tritt einmal auf oder tritt nicht auf
     +   {1,} Zeichen tritt mindestens einmal auf
     *   {0,} Zeichen tritt beliebig oft auf

 

Alternativen

Eine Zeile des Logfiles vom Webserver Apache im "common"-Format sieht z.B. so aus:

    127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326

D.h. der Zeitpunkt wird so angegeben: 10/Oct/2000:13:55:36. Dabei ist "Oct" die Abkürzung für Oktober. Die Abkürzungen der anderen Monate sind: Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec. Wenn man nun die Zeilen heraussuchen will, die von den Monaten September bis November sind, dann sieht die Regex so aus:

$zeile =~ /Sep|Oct|Nov/;

Allerdings wird die Regex nicht nur aus der Alternative bestehen, sodass man üblicherweise Klammern setzt.

$zeile =~ /\d\d\/(Sep|Oct|Nov)\/20../;

 

Ankerpunkte

Wenn ein Muster in einer Zeile gesucht wird, kostet das deutlich mehr Zeit, als wenn die Regex "weiss", wo sie beginnen muss. Wenn also die Regex mit dem Beginn der Zeile bzw. des Strings matcht und das in der Regex auch angegeben wird, dann erhöht das die Performance beträchtlich. Um am Anfang der Zeile des Logfiles vom Apache (siehe letzter Punkt "Alternative") nach der IP am Anfang zu suchen, schreibt man das so:

$zeile =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/


Genauso sinnvoll ist es, wenn die Regex auf das Ende des Strings bzw. der Zeile passt. Dann schreibt man das so:

$zeile =~ /\d+$/

Das bedeutet im Fall der Zeile des Logfiles, dass diese darauf geprüft wird, dass am Ende eine Zahl aus mindestens einer Ziffer steht.

 

Optionen

Optionen dienen dazu das Verhalten unserer Regex zu verändern. Die Optionen werden hierbei hinter dem regulären Ausdruck angehängt. Es ist auch möglich mehrere Optionen zu benutzen, die Reihenfolge der Optionen spielt hierbei keine Rolle.

  • Option: /i  -  "ignore-case"
$zeile =~ /Sep|Oct|Nov/i;

Es wird ein Treffer erzielt, wenn in der Zeile 'Sep' oder 'Oct' oder 'Nov' gefunden wird, wobei aber die Groß- und Kleinschreibung ignoriert wird. D.h. es wird auch 'sep' oder 'SEP' oder 'oCT' oder 'nov' oder 'noV' gefunden usw.

Es soll jetzt nur eine Übersicht der Optionen angegeben werden (Kopie von wikibooks):

  • "i", case-insensitive: Groß- und Kleinschreibung wird ignoriert
  • "g", global: Es werden alle Stellen gesucht, die passen. Die Funktion ist eher für das Ersetzen mit regulären Ausdrücken interessant.
  • "m", multi-line: Verändert die Bedeutung des $-Zeichen (siehe Sonderzeichen), damit es an der Position vor einem "\n" passt.
  • "s", single-line: Verändert die Bedeutung des .-Zeichen (siehe Sonderzeichen), damit es auch auf einem "\n" passt.
  • "x", extended: Alle Whitespace-Zeichen innerhalb des Regulären Ausdrucks verlieren ihre Bedeutung. Dadurch kann man Reguläre Ausdrücke optisch besser aufbereiten und über mehrere Zeilen schreiben. Außerdem kann man die Ausdrücke kommentieren, wie im Beispiel des nächsten Abschnitts zu sehen ist.
  • "e", evaluate: Das Ersetzmuster wird als Perl-Code interpretiert und ausgeführt.

Ein Beispiel für die Option /x ist im nächsten Punkt zu finden.

 

Teilausdrücke merken

Bis jetzt wurde immer das Muster als Ganzes betrachtet, entweder es hat gepasst (gematcht) oder eben nicht. Die wahre Leistung entfalten die regulären Ausdrücke aber dann, wenn man auf beliebige Teile des Musters zugreifen kann. Dazu werden runde Klammern verwendet. Auf den Teilausdruck in der ersten Klammer kann mit $1, auf den Teilausdruck in der zweiten Klammer kann mit $2, auf den Teilausdruck in der dritten Klammer kann mit $3, usw. zugegriffen werden.

Es soll im Folgenden aus den Zeilen des Logfiles vom Webserver Apache die IP, das Login, das Datum und die Zeit herausgezogen werden. Dazu wird als erstes die Zeile des Logfiles mit Hilfe einer Regex beschrieben. Nochmal die Beispiel-Zeile des Logfiles:

    127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326

Man kann den Anfang dieser Zeile so beschreiben:

    1.   die IP-Adresse des Client \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} 11.   ein Leerzeichen \s
    2.   ein oder mehrere Leerzeichen \s+ 12.   eine Zahl, eventuell mit Minus -?\d+
    3.   ein Minus "-" - 13.   eine schließende Klammer "]" \]
    4.   ein Leerzeichen \s 14.   ein Leerzeichen \s
    5.   das Login \w+ 15.   ein Anführungszeichen \"
    6.   ein Leerzeichen \s        
    7.   die öffnende Klammer "[" \[     mehr wird nicht gebraucht  
    8.   das Datum \d\d\/.{3}\/20\d\d        
    9.   ein Doppelpunkt ":" \:        
    10.   die Zeit ..:..:..        


Damit ergibt sich als Regex zur Beschreibung für den Anfang einer solchen Zeile des Logfiles:

^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\s+-\s\w+\s\[\d\d\/.{3}\/20\d\d\:..:..:..\s-?\d+\]\s\"


Um jetzt die IP, das Login, das Datum und die Zeit heraus zu holen, werden um die Teilausdrücke (runde) Klammern gesetzt.

^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+-\s(\w+)\s\[(\d\d\/.{3}\/20\d\d\):(..:..:..)\s-?\d+\]\s\"


Man kann diese Ausdrücke z.B. so in eigene Variablen stecken:

 if ($zeile =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+-\s(\w+)\s\[(\d\d\/.{3}\/20\d\d\):(..:..:..)\s-?\d+\]\s\"/){
   $ip = $1;
   $login = $2;
   $datum = $3;
   $zeit = $4;
 }


Oder unter Verwendung einer Regex mit dem Schalter /x (extended Regex) sähe das dann so aus:

 if ($zeile =~ /
   ^                                                # die Regex muss auf den Anfang der Zeile passen
     \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} # die IP-Adresse des Client
     \s+                                            # ein oder mehrere Leerzeichen
     -                                               # ein Minus "-"
     \s                                              # ein Leerzeichen
     \w+                                           # das Login
     \s                                              # ein Leerzeichen
     \[                                              # die öffnende Klammer "["
     \d\d\/.{3}\/20\d\d                       # das Datum
     \:                                              # ein Doppelpunkt ":"
     ..:..:..                                        # die Zeit
     \s                                              # ein Leerzeichen
     -?\d+                                         # eine Zahl, eventuell mit Minus
     \]                                              # eine schließende Klammer "]"
     \s                                              # ein Leerzeichen
     \"                                              # ein Anführungszeichen
   /x) {
           $ip = $1;
           $login = $2;
           $datum = $3;
           $zeit = $4;
 }

 

Capturing vs. Clustering

Wie im letzten Abschnitt "Teilausdrücke merken" angegeben, werden Klammern verwendet, um Teilstrings festzuhalten (Capturing). Im Abschnitt "Alternativen" wurde angegeben, dass man bei Alternativen üblicherweise Klammern verwendet, um diese zu gruppieren (Clustering).   Im "Kamel-Buch" (Programmieren mit Perl) steht das so (siehe Weblinks):

Capturing
Um einen Substring für die spätere Verwendung festzuhalten (capture), schließen Sie das entsprechende Submuster in runden Klammern ein. Das erste Klammerpaar legt den entsprechenden Substring in $1 ab, das zweite Paar in $2 und so weiter. Sie können so viele Klammern verwenden, wie Sie wollen; Perl sorgt dafür, daß ausreichend numerierte Variablen für alle festgehaltenen Strings verfügbar sind.
Clustering
Reine Klammern dienen sowohl zur Gruppierung als auch zum Festhalten. Manchmal will man das aber nicht. Manchmal will man einfach einen Teil eines Musters gruppieren, ohne Rückwärtsreferenzen zu erzeugen. Sie können die erweiterte Form der Klammerung verwenden, um das Capturing zu unterbinden: die Notation (?:MUSTER) bildet Cluster, ohne etwas festzuhalten.
 

Ein Beispiel:

Wir suchen nach einem Substantiv und wissen, dass davor ein Artikel steht. Der Artikel interessiert uns nicht, sondern nur das Substantiv. Oder anders gesagt, das Substantiv wollen wir uns merken, den Artikel nicht. Das läßt sich so schreiben:

my $string = 'Heute ist das Wetter schön.';
if ($string =~ /(?:der|die|das)\s(\w+)/i) { print $1, "\n"; }


Der Aufruf sah bei mir so aus:

 

Teilausdrücke rückwärts referenzieren

"Möchte man sicherstellen, dass ein gefundener Teilausdruck genau so nocheinmal im durchsuchten Text zu finden ist, kann man Rückreferenzen benutzen." (aus einer Mail der Mailingliste der Frankfurter Perl-Mongers von Renée Bäcker)

Um das 1 : 1 in ein Programm zu gießen, sei ein Satz (String) gegeben und in diesem sollen Wörter gefunden werden, die mehrfach vorkommen. Häufig kommt folgender Tippfehler vor, dass ein Wort unabsichtlich doppelt geschrieben wird. Der Einfachheit halber wird hier davon ausgegangen, dass das Wort nur aus Buchstaben, dem Unterstrich und Ziffern besteht. Dann läßt sich das Wort so als Regex schreiben: (\w+) . Damit auch bestimmt ein ganzes Wort genommen wird, rahmen wir diesen kleinen regulären Ausdruck durch den Wortbegrenzer \b ein. Damit sieht die Regex für ein Wort jetzt so aus: \b(\w+)\b . Danach sollen bzw. können beliebige Wörter und Zeichen kommen, also einfach  .*  und dann soll das stehen, was durch den ersten Ausdruck, also die Klammer, gefunden wurde. Dafür steht die \1 für den ersten gefundenen Ausdruck. Allerdings würde sich dann auch ein Treffer ergeben, wenn dieses Wort nur ein Teil des zweiten Wortes wäre. Es geht aber hier darum, dass das Wort zweimal (gleich) geschrieben wurde. Also sieht der Suchausdruck bei mir so aus: \b(\w+)\b.*\b\1\b   (Dieses Beispiel wurde mir aus der Mailingliste der Frankfurter Perl-Mongers von Wieland Pusch angegeben.)

my $string = 'Beim Testen ist mir ein ein Detail aufgefallen.';

if ($string =~ /\b(\w+)\b.*\b\1\b/) {  print $1, "\n"; }


Der Aufruf sah dann bei mir so aus:


In der Wikipedia fand ich ein schönes Beispiels für das Rückwärtsreferenzieren (siehe unten Weblinks). Dort wurde angegeben, dass man mit Rückwärtsreferenzen Datumsangaben vom amerikanischen Format einfach ins deutsche Datumsformat umformen kann. Aus der Liste der Frankfurter Perl-Mongers bekam ich auch den Quellcode für dieses Beispiel:

my $datum_im_us_format = "03/30/2019";             # MM/DD/YYYY
my $datum_im_de_format = $datum_im_us_format; # DD.MM.YYYY
# jetzt wird das Datum für das deutsche Format bearbeitet
$datum_im_de_format =~ s|(\d\d)/(\d\d)/(\d\d\d\d)|\2.\1.\3|; # als Begrenzer für die Regex bewußt | gewählt
print 'Datum (us): ', $datum_im_us_format, "\n";
print 'Datum (de): ', $datum_im_de_format, "\n";


Der Aufruf sah bei mir so aus:


Aber: Von Martin Fabiani erhielt ich den Hinweis, dass ich doch die Warnungen einschalten sollte. Damit sieht der Quelltext so aus:

use warnings;
use strict;

my $datum_im_us_format = "03/30/2019";             # MM/DD/YYYY
my $datum_im_de_format = $datum_im_us_format; # DD.MM.YYYY
# jetzt wird das Datum für das deutsche Format bearbeitet
$datum_im_de_format =~ s|(\d\d)/(\d\d)/(\d\d\d\d)|\2.\1.\3|; # als Begrenzer für die Regex bewußt | gewählt
print 'Datum (us): ', $datum_im_us_format, "\n";
print 'Datum (de): ', $datum_im_de_format, "\n";


Der Aufruf sah bei mir nun so aus:


Wenn man die Warnung beherzigt und die Zeile mit dem regulären Ausdruck durch diese ersetzt:

$datum_im_de_format =~ s|(\d\d)/(\d\d)/(\d\d\d\d)|$2.$1.$3|;

dann verschwinden die Warnungen. Bedeutet: das in der Wikipedia angegebene Beispiel ist kein wirkliches Beispiel für Rückwärtsreferenzen (zumindest nicht für Perl).

Merke: Rückwärtsreferenzen sollten nur im Suchteil verwendet werden.

 

Gier

Bei der Verwendung von runden Klammern, um Teilausdrücke zu merken (siehe Punkt 9) oder rückwärts zu referenzieren (siehe Punkt 11) kommt es häufig vor, dass es mehrere Möglichkeiten gibt, die auf die geklammerten Ausdrücke in der Regex passen. Damit ergibt sich die Frage, "welcher Teilausdruck wird dann durch die Regex gefunden?".

Ein Beispiel: in dem Logfile sollen alle HTML-Seiten herausgefiltert werden. HTML-Seiten enden auf 'html' oder 'htm'. Man kann das durch folgende Regex beschreiben:

^[^"]+\"\[A-Z]+\s(.*\.html?)\s

Bedeutet: vom Anfang solange weiter gehen, wie es kein Anführungszeichen ist, dann ein Anführungszeichen, dann ein Wort aus Großbuchstaben, dann ein Leerzeichen, dann der Dateiname, der auf .html oder .htm endet.

Das Programmfragment zum Finden des Dateinamens würde dann so aussehen:

 if ($zeile =~ /^[^"]+\"\[A-Z]+\s(.*\html?)\s/){
    $dateiname = $1;
 }

Für die Zeile

    127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /index.html HTTP/1.0" 200 24005

stellt sich die Frage: steht in dem Klammerausdruck nun /index.html oder /index.htm ?  Die Antwort lautet: /index.html !

Merke: die Regex findet von den passenden Ausdrücken immer den größten! - man sagt auch: die Regex ist gierig.

 

 

Weblinks

 

zurück


© ERG Saalfeld   -   Hans-Dietrich Kirmse   14.05.2019