Perl 6

Die Zukunft des Programmierens

Reguläre Ausdrücke (1)

Wie in einem vorhigen Kapitel schon erwähnt, ist die Verarbeitung von Text eine der Stärken von Perl.

Das liegt zu großen Teilen an Perls Regulären Ausdrücken, auf Englisch regular expressions oder kurz regex genannt.

Reguläre Ausdrücke sind Muster, und auf Wunsch überprüft Perl, ob ein bestimmter Text zu diesem Muster passt, beziehungsweise welcher Teil eines Strings zu dem Muster passt.

Ein Beispiel: Preise extrahieren

Angenommen, Sie müssen ein Programm schreiben, das Text darauf untersucht, ob darin Preisangaben stehen.

Der Text könnte so aussehen: "Für 400 Euro würde ich den Flug buchen, 450 Euro wären mir schon zu viel".

Darin tauchen zwei Preisangaben auf, beide in dem Format Zahl Lehrzeichen 'Euro'.

Also suchen wir danach:

my $text = "Für 400 Euro würde ich ...";

if $text ~~ m/ <digit>+ <space> 'Euro' / {
    say "Der Text enthält eine Preisangabe";
};

Was geht hier vor? Neu ist diese Zeile:

if $text ~~ m/ <digit>+ <space> 'Euro' / {

Neu ist zuerst einmal der Operator, der aus zwei Tilden ~~ besteht und der Smart Match Operator heisst, und der Dinge vergleicht - hier einen String $text mit einer Regex.

Die Regex selbst sieht so aus: m/ <digit>+ <space> 'Euro' /. Sie beginnt mit einem m, das für Match steht, und einen Schrägstrich. Sie geht bis zum nächsten Schrägstrich weiter.

<digit> steht für eine beliebige Ziffer, also 0, 1, 2, usw. Das Plus + bedeutet, dass sie beliebig oft wiederholt werden darf, aber mindestens ein mal vorkommen muss.

<space> steht für ein Leerzeichen, und 'Euro' steht für den Text Euro.

Die Zeile könnte man also so ins Deutsche übersetzen: Suche in dem String $text nach einer Zeichenkette, die aus einer oder mehr Ziffern besteht, gefolgt von einem Leerzeichen, gefolgt von dem Text Euro. Wenn der String so etwas enthält, führe die folgende Zeile aus.

Man gibt nicht an, wie nach diesem Muster gesucht werden soll - diese Arbeit nimmt einem der Perlinterpreter ab.

"Capturing Groups" - Informationen extrahieren

In dem vorherigen Beispiel ist man sicher nicht nur daran interessiert, ob eine Preisangabe in dem Text vorkommt, sondern auch wie groß der Betrag ist.

Dafür gibt es einfache Lösung:

my $text = "Für 400 Euro würde ich ...";

if $text ~~ m/ (<digit>+) <space> 'Euro' / {
    say "Der Text enthält eine Preisangabe von $/[0] Euro";
};

Die Regex wurde nur ein wenig verändert: aus <digit>+ wurde (<digit>+). Die Klammern bedeuten so viel wie "Speichere den Text, auf den Teil der Regex in den Klammern passt".

Beim Anwenden einer Regex auf einen String, also beim Matchen, wird automatisch die Variable $/ erzeugt, das unter anderem wie ein Array verwendet werden kann. Das erste Element dieses Arrays ist der Text, der auf <digit>+ passt - also in dem oberen Beispiel 400.

Alternativen

Mit Regexes kann man noch mehr machen. Als Beispiel soll die vorherige Suche auf andere Währungen ausgeweitet werden:

my $text = "It's 2 Dollar for each pancake";

if $text ~~ m/ (<digit>+) <space> (Euro|Dollar|Yen) / {
    say "Ein Preis: $/[0] $/[1]";
};

Neu ist, dass statt 'Euro' jetzt (Euro|Dollar|Yen) steht. Die Funktion der Runden Klammern ist ja schon bekannt: sie speichern den Text, der auf den Teil der Rexes in den Klammern steht.

In den Klammern stehen die Namen von verschiedenen Währungen, getrennt durch senkrechte Striche |. Das bedeutet, dass eine der Alternativen im String vorkommen muss.

Dadurch, dass zwei Paare von Klammern verwendet wurden, steht nicht nur $/[0] zur Verfügung, sondern auch $/[1], das zweite Element in dem Array $/.

Quantifier

Neben dem + gibt es noch weitere sogenannte Quantifier, d.h. Operatoren die kontrollieren, wie häufig ein bestimmtes Konstrukt vorkommt.

Quantifier Bedeutung Beispiel
* Beliebig viele Wiederholungen, auch gar keine m/'ab'*/ passt auf "", "ab", "ababa", "ababab", ...
+ Beliebig viele Wiederholungen, mindestens eine m/'ab'+/ passt auf "ab", "ababa", "ababab", ...
? Null oder eine Wiederholungen m/'ab'?/ passt auf "" oder "ab"
**{1..3} Ein bis drei Wiederholungen m/'ab'**{1..3}/ passt auf "ab", "abab" oder "ababab"

Regexes deklarieren

Wie Funktionen kann man auch Regexes definieren und Namen geben:

token zahl {
    <digit>+
}
token waehrung {
    Euro | Dollar | Yen | Drachmen | Pesos
}
rule preis {
    <zahl> <waehrung>
}
if $text ~~ m/ <preis> / {
    say "Preis $/<zahl> mit Währung $/<waehrung> gefunden";
}

Hier wird mit dem Schlüsselwort token eine Regex mit Namen zahl definiert. Ein token ist eine einfache Regex, hier passt sie auf eine oder mehrere Ziffern.

Dann wird ein token waehrung erzeugt, und dann eine rule preis definiert. Eine rule ist das gleiche wie ein token, nur dass überall, wo Leerzeichen in der Regex sind, auch optionale Leerzeichen erwartet werden.

In der rule preis wird mit spitzen Klammern <...> auf die anderen Regexes zugegriffen.

Später wird mit $/<zahl> auf den Text zugegriffen, auf den die Regex zahl gepasst hat. (Eine kürzere Schreibweise wäre $<zahl>, also ohne den Schrägstrich).

Die runden Klammern haben also Matchergebnisse in $/[0], $/[1] usw. gespeichert, die spitzen Klammern in $/<name> - einmal wird $/ wie ein Array verwendet, das andere mal als Hash.

Beides kann man auch kombinieren:

token zahl {
    <digit>+
}
if $text ~~ m/ <zahl> <space>+ ([Euro | Dollar | Yen | Drachmen | Pesos]) / {
    say "Preis $/<zahl> mit Wähung $0 gefunden";
}

Vielleicht ist Ihnen aufgefallen, dass <zahl> und <space> gleich verwendet werden - konsequenterweise kann man auch auf $/<space> zuweisen. Doch weil darin nur Leerzeichen stehen, interessiert das nicht besonders.

Wenn man Rechenzeit sparen will, kann man dafür sorgen, dass $/<space> nicht gespeichert wird, indem man es als <?space> verwendet:

token zahl {
    <digit>+
}
token waehrung {
    Euro | Dollar | Yen | Drachmen | Pesos
}
if $text ~~ m/ <zahl> <?space>+ <waehrung> / {
    say "Preis $<zahl> mit Wähung $<waehrung> gefunden";
}

Modifier

Mit sogenannten Modifiers kann man das Verhalten von Regexes verändern. Ein einfaches Beispiel ist der :i oder :ignorecase-Modifier, der dafür sorgt, dass Groß- und Kleinschreibung ignoriert wird. Modifier können entweder vor eine Regex geschrieben werden, oder in die Regex hinein:

if $text ~~ m:i/Flugzeug/ {
    # Passt auch auf 'flugzeug', 'FlUgZeug' etc.
}

token waehrung {
    :ignorecase         # gilt ab hier in dieser Regex
    Euro | Dollar | Yen | Drachmen | Pesos
}

Mit dem :b oder :basechar-Modifier werden alle Akzentuierungen ignoriert, d.h. die folgenden Zeichen werden als gleich behandelt: óôõöøo.

Mit dem :g oder :global-Modifier werden alle möglichen, nicht überlappende Matches gefunden:

my $text = "2 + 3 * (52 - 3)";
my @zahlen = $text ~~ m:g/<digit>+/;
say @zahlen.join(", ") # Ausgabe: "2, 3, 52, 3"