Regexes
Regexes sind Muster, die Text beschreiben. Wenn man sie auf einen String "anwendet", überprüft man, ob der String diesem Muster folgt.
if "Mein Name ist Hase" ~~ m/Hase/ { say "In dem Text kam 'Hase' vor"; }
In diesem Beispiel wird ein Text darauf überprüft, ob der String
"Hase" darin vorkommt. Dabei ist "Hase" die regex, und wird mit
m/.../ begrenzt.
Die Regex wird mit dem Smart Match Operator
~~ auf den String angewandt, oder gematched wie man
sagt.
Aufbau von Regexes
Im einfachsten Fall bestehen Regexes einfach auch Text, ein Buchstabe nach dem anderen.
Dabei sollte man beachten, dass Leerzeichen keinerlei Bedeutung
haben, d.h. m/Hase/ und m/Ha se/
sind genau die gleiche Regex.
Zusätlich gibt es Zeichen, die eine Sonderfunktion haben, zum Beispiel
der Punkt . steht für ein beliebiges Zeichen:
my $str = "Hosenträger"; if $str ~~ m/H.se/ { # passt auch. } if "Haase" ~~ m/H.se/ { # passt nicht, der Punkt steht für genau ein Zeichen }
Es gibt auch Zeichenkombinationen, die etwas besonderes bedeuten,
z.B. \d steht für eine Ziffer (Englisch digit =
Ziffer).
my $zeit = "6H30"; if $zeit ~~ m/\d H \d\d/ { say "gültige Weckzeit"; } else { say "Damit kann ich nichts anfangen"; }
Die folgende Tabelle fasst die wichtigsten dieser Zeichenkombinationen zusammen. Dabei gilt, dass jeweils genau ein Zeichen gematched wird.
Die Negation ist die Verneinung, d.h. wenn \d auf eine
Ziffer passt, passt \D auf ein beliebiges Zeichen, das keine
Ziffer ist.
| Zeichen | Bedeutung | Beispiel | Herkunft | Negation |
|---|---|---|---|---|
| . | beliebiges Zeichen | / | ... | |
| \d | Ziffer | 7 | digit = Ziffer | \D |
| \t | Tabulator | tab = Tabulator | \T | |
| \n | Zeilenumbruch | newline = neue Zeile | \N | |
| \w | Buchstabe, Ziffer oder Unterstrich | H | word = Wort | \W |
| \s | (Unicode)-Leerzeichen | space = Leerzeichen | \S |
Alternativen
Alternativen sind oder-Verknüpfungen, das heißt die Regex matcht, wenn eine von mehreren Alternativen passt:
if $str ~~ m/der|die|das/ { say "Bestimmten Artikel gefunden"; }
Wenn sich die Alternative nicht über die gesamte Regex erstrecken soll, kann man ihren Gültigkeitsbereich mit eckigen Klammern einschränken:
m/ [ der | die | das ] \s Artikel / # passt auf "der Artikel", "die Artikel" oder "das Artikel"
Nur zur Erinnerung: Leerzeichen in Regexes werden ignoriert, wenn
man Leerzeichen matchen will, muss man das zum Beispiel mit
\s angeben.
Quantoren, oder: "Wie oft darf's denn sein?"
Mit sogenannten Quantoren (Englisch quantifier) gibt man an, wie häufig ein bestimmtes Zeichen oder Teilmuster vorkommen muss.
Dabei steht ein Fragezeichen für "0 mal oder einmal":
m/abc?d/ # passt auf abcd oder abd m/a[bc]?d/ # passt auf abcd oder ad
Ein + steht für "Ein mal oder beliebig oft":
m/abc+d/ # passt auf abcd, abccd, abcccd, ... m/a[bc]+d/ # passt auf abcd, abcbcd, abcbcbcd, ... if $str ~~ m/\d+/ { say "in $str kommt eine Zahl vor"; }
Ein * passt auf keine, eine oder beliebig
viele Wiederholungen:
m/abc*d/ # passt auf abd, abcd, abccd, abcccd, ... m/a[bc]*d/ # passt auf ab, abcd, abcbcd, abcbcbcd, ...
Wenn man z.B. eine Wiederholung genau vier mal haben will, kann man
das mit der **4-Notation angeben:
m/a b**4 c/ # passt auf abbbbc m/a b**2..4 c/ # passt auf abbc oder abbbc oder abbbbc m/a b**2..* c/ # passt auf abbc, abbbc, abbbbc, abbbbbc, ...
Regexes sinnvoll einsetzen
Bisher haben Sie gesehen, wie man mit Regexes überprüft, ob ein Muster in einem String vorkommt oder nicht.
Natürlich kann man auch abfragen, welcher Teil eines Strings auf eine Regex passt:
my $str = "Du schuldest mir 122 Euro"; if $str ~~ m/\d+/ { say "$/ Euro, verstanden?"; }
Das Ergebnis des letzten Matches wird in der speziellen Variable
$/ gespeichert, und im String-Kontext wird daraus automatisch der Teil des
Strings, auf den die Regex gepasst hat.
Man kann das Ergebnis auch explizit einer Variable zuweisen, und auf weitere Eigenschaften zugreifen:
my $match = "foobar" ~~ m/o+/; say "Anfangsposition des Matches: ", $match.from; say "Endposition des Matches: ", $match.to;
Mit den Funktionen comb und split kann
man Strings in Arrays zerlegen.
comb liefert dabei alle Treffer einer Regex
zurück:
my $text = "20 mal 2 ist 40"; my @zahlen = $text.comb(m/\d+/); # Ergebnis: @zahlen = 20, 2, 40;
(Der Name kommt von to comb, was auch "etwas durchkämmen" im Sinne von "systematisch Durchsuchen" heißt.)
split trennt einen String mithilfe einer Regex, wobei
der String überall dort getrennt wird, wo die Regex passt. Der
passende Teil wird nicht zurückgeliefert:
my $text = "20 mal 2 ist 40"; my @worte = $text.split(m/\s+/); # Ergebnis: @worte = "20", "mal", "2", "ist", "40";
Wenn man in einer Regex einen Schrägstrich /
benutzt, muss man ihn mit einem Rückstrich escapen: \/.
Eine andere Möglichkeit ist, ein anderes Zeichen als Begrenzung der
Regex zu verwenden:
my $cmd = "/usr/bin/perl -w"; if $cmd ~~ m{^(/.*/)perl} { say "Perl liegt im Verzeichnis $/[0]"; }
Man kann fast beliebige Sonderzeichen zur Begrenzung verwenden, wenn man Klammern verwendet wird die Regex mit der entsprechenden schliessenden Klammer beendet.
Gierige und bescheidene Quantoren
Angenommen, man lässt die Regex m/a.*b/ auf den String
afffbffb los. Die meisten Anfänger erwarten, dass die
Regex auf "afffb" passt, aber wenn man sich $/
ausgeben lässt, erlebt man eine Überrasschung: Der ganze String passt
auf die Regex.
Der Grund dafür ist, dass die Quantoren +,
*, ? und **{...}
gierig, englisch greedy sind, d.h. sie
passen immer auf einen möglichst langen String.
Wenn der Programmteil von Perl, der die Regexes verarbeitet, auf
eine Konstruktion wie m/a.*c/ trifft, sucht er nach dem
ersten "a", geht dann ans Ende des Strings, und sucht dann rückwärts
nach dem ersten "c", wenn er das gefunden hat, ist er fertig.
Um das zu verhindern, und dafür zu sorgen, dass ein möglichst
kurzer String gefunden wird, kann man hinter jeden Quantoren ein
Fragezeichen ? stellen:
$m = "affbeeb" ~~ m/a .* b/; say $m; # Ausgabe: affbeeb $m = "affbeeb" ~~ m/a .*? b/; say $m; # Ausgabe: affb
Genauso kann man aus + mit der Schreibweise
+? "einen oder mehr, aber möglichst wenig davon" machen,
aus **{2...6} mit **{2..6}? "zwei bis sechs
Wiederholungen, aber möglichst wenig davon" usw.
Man nennt diese Quantoren dann non-greedy, also nicht gierig.
Anker
Die bisherigen Regexes haben immer nur überprüft, ob ein bestimmtes Must in einem String vorkommt, aber nicht, ob der gesamte String zu dem Muster passt.
Das geht, indem man die Regex verankert. Dazu gibt es die
Sonderzeichen ^ und $, die auf Anfang bzw.
Ende des Strings passen. Wenn man also überprüfen will, ob ein String
mit einer Ziffer anfängt, geht das m/^\d/, und wenn man
überprüfen will, ob ein String eine positive, ganze Zahl ist, geht das
mit m/^\d+$.
Dabei sollte man beachten, dass ^ und $
keine Zeichen "aufbrauchen", d.h. ^a$ passt genau auf den
String "a".
Wenn man sich nicht für Anfang und Ende eines Strings interessiert,
sondern für Zeilenanfang und -Ende, so kann man ^^ und
$$ als Anker verwenden.
my $s = "He shouted\nRUN\nAnd I ran"; if $s ~~ m/ ^^ \w+ \n $$ / { # passt auf eine Zeile, die aus nur einem Wort # und einem newline besteht, matched also say $/; # Ausgabe: RUN } if $s ~~ m/ ^ \w+ $ / { # passt nicht, da der String nicht nur # aus einem Wort besteht. }
Dabei gelten Anfang und Ende eines Strings auch immer als logischer Zeilenanfang bzw. -Ende.
Captures
Mit den bisher vorgestellten Elementen von Regexes ist es nicht möglich, nach einem Muster wie Ein Wort, gefolgt von einem Leerzeichen, gefolgt von dem selben Wort zu suchen.
Das ändert sich mit sogenannten captures (auf Deutsch auch manchmal fangende Klammern genannt), die mit runden Klammern eingerichtet werden:
m/ (\w+) \s $0 /
Dabei wird der Match des in den runden Klammern stehenden Teils der
Regex in der Variable $0 gespeichert.
Der String "foo foo" würde z.B. zu der Regex passen,
nicht aber "foo bar".
Wenn es mehrere Capturing Groups gibt, wird das Ergebnis
der ersten in $0 abgespeichert, der zweiten in
$1 usw.
Außerdem kann man das Match-Objekt, das beim Anwenden einer Regex entsteht, als Array verwenden, das als Elemente die verschiedenen Capturing Groups hat.
my @a = "der Artikel, das Adverb", "das Schiff, das Monster", "der Berg, das Schaf"; for @a -> $s { if $s ~~ m/(\w+) \s (\w+), \s $0 \s (\w+)/ { say "$/[1] und $/[2] haben den gleichen Artikel: $/[0]"; } } # Ausgabe: # Schiff und Monster haben den gleichen Artikel: das
Dieses Muster sucht nach einem Wort \w+ und speichert
das Ergebnis in der Variablen $0, sucht weiter nach einem
Leerzeichen \s, nach noch einem Wort, das in $1
gespeichert wird, einem Komma, einem Leerzeichen, dem zuerst gefunden
Wort, und dann noch einem Wort, das in $2 gespeichert wird.
Außerhalb der Regex kann auf $0 mit $/[0]
oder mit $0 zugegriffen werden, auf $1 auch mit
$/[1] usw.
Ab dem 11. Capture mit Nummer 10 kann nur noch mit der
$/[$index]-Notation darauf zugegriffen werden.
Wenn man z.B. eine Datei einliest, kann man mit der folgenden Regex
nach Strings suchen, die entweder mit " oder
' begrenzt sind:
m/ ('|") .*? $0 /
Das sucht nach einem einfach oder doppelten Anführungszeichen, dann beliebig viele (aber möglichst wenige) Zeichen, und dann noch einmal dem Anfangszeichen.
Modifier
Mit sogenannten Modifiern kann man das Verhalten von Regexes auf verschiedene Arten verändern.
Groß- und Kleinschreibung ignorieren - :ignorecase
Mit dem :i oder :ignorecase-Modifier wird
Groß- und Kleinschreibung ignoriert. Das gilt nicht nur für latinische
Zeichen, sondern für beliebige Schriften, die das Konzept von Groß- und
Kleinschreibung kennen.
my $str = "Ab"; $str ~~ m:i/ab/; # matched regex foo { :ignorecase # kann auch innerhalb einer Regex stehen ab # passt auf ab, Ab, aB und AB }
Akzente ignorieren - :basechar
Mit :basechar oder :b werden alle Arten von
Akzente von der Regex ignoriert.
m:b/a/ matched also nicht nur "a", sondern auch "ä",
"à", "á", "â", "ã", "ä" oder "å".
Alle Treffer finden - :global
Mit dem :g oder :global-Modifier kann man
nach allen, nichtüberlappenden Treffern einer Regex suchen:
my $str = "3 = (4 - 2) + 1"; my @zahlen = $str ~~m:g/\d+/; say @zahlen.join(", "); # 3, 4, 2, 1
Überlappende Treffer finden - :overlap
Mit dem :overlap oder :ov-Modifier findet
man auch überlappende Treffer, aber pro Anfangsposition nur eines:
my @treffer = 'abababa' ~~ m:ov/a.+a/; # Ergebnis: 'abababa', 'ababa', 'aba'
Wirklich alle Treffer finden - :exhaustive
Mit dem :exhaustive oder :ex-Modifier kann
man jedem möglichen Treffer suchen lassen:
$str = "abracadabra"; for $str ~~ m:exhaustive/ a (.*?) a / -> $match { say $match[0]; # br brac bracad bracadabr c cad cadabr d dabr br }
:sigspace
Mit dem :sigspace oder :s-Modifier werden
alle Leerzeichen so interpreteirt, als stünde an ihrere Stelle die Regex
<.ws>.
Die matcht, wenn sie zwischen zwei Wörtern steht, \s+, und
sonst \s*.
(Der Punkt am Anfang bewirkt, dass keine Named Capture erstellt wird.)
m:sigspace/ \d+ [\+ | \*] \d+ / # matcht: '2+3', '2 + 3', ' 2 + 3 ', ...
Backtracking verhindern -
:ratchet
Bisher haben wir so getan, also ob die Regex-Engine auf magische Weise wüßte, ob eine Regex zu einem String passt oder nicht.
Das ist natürlich nicht der Fall, sie muss in dem String suchen.
Dabei gibt es häufig anfangs mehrere Möglichkeiten, wie die Regex-Engine einen String matchen könnte.
Ein Beispiel: Wenn die Regex \w+\d+ in dem String
"ab123" das erste Mal auf eine Ziffer trifft, könnte das
immer noch zu \w+ gehören, oder
aber schon zu \d+. Da die Engine das noch nicht entscheiden
kann, ohne den Rest des Strings zu kennen, merkt sie sich, dass es an
dieser Stelle noch eine andere Möglichkeit gegeben hätte, und fährt damit
fort, \w+ zu matchen.
Bei der nächsten Ziffer, 2 passiert genau das Gleiche,
ebenso bei der dritten.
Dann ist die Engine am Ende des Strings angelangt, die Regex ist aber
noch nicht zuende. Also geht die Regex Engine zu dem letzten Punkt zurück,
an dem es mehrere Möglichkeiten gab, das war beim letzten Zeichen, der
3.
Dort wird versucht, die verbleibende Möglichkeit, \d+, zu
matchen. Das ist erfolgreich, also matcht die ganze Regex.
Dieser Vorgang des Speicherns von mehrdeutigen Stellen und wieder darauf zurückzugehen, wenn auf keine andere Art gematcht werden kann, nennt sich Backtracking.
Manchmal möchte man dieses Verhalten jedoch abschalten, meistens um das Matchen einer Regex zu beschleunigen.
Das geht mit dem :ratchet-Modifier.
Die Idee dahinter ist, dass man häufig genauer als die Regex-Engine weiß, auf was eine Regex matchen soll und auf was nicht.
Wenn man z.B. eine Datei der Form
name1 = wert1 name2 = wert2
parsen will, nimmt eine Regex der Form ^^(\w+) \s* \= \s*
(\N+)\n$$ - und man weiss genau, dass wenn das = nicht
matcht, muss die Engine gar nicht erst versuchen, den vorherigen String zu
kürzen (der vorher gematchte String waren null oder mehr Leerzeichen, das
kann niemals ein = sein).
Also sagt man der Engine, dass sie es gar nicht erst versuchen soll:
m:ratchet:global/ ^^(\w+) \s* \= \s* (\N+)\n$$/
Regexes können beschreibbar sein
my $str = "12+3"; token op { \+ }; $str ~~ m:rw/(\d+) <op> (\+d)/; $<op> = '(irgendwas)'; # $str ist jetzt "12(irgendwas)3"
Mit dem :rw-Modifier kann man die Captures einer Regex
beschreibbar machen, wie oben demonstriert.
Dabei ändern sich automatisch auch die anderen Captures,
insbesondere .from und .to der Captures:
my $str = "ab"; $str ~~ m:rw/(a)(b)/; say $/[1].from; # 1 $/[0] = 'aa'; say $/[1].from; # 2 say ~$/; # aab
Eigene Zeichenklassen, Grammatiken
Wenn einem die vorgefertigten Zeichenklassen wie \d
und \w nicht ausreichen, kann man sich selbst welche
erzeugen, allerdings mit anderer Notation:
m/ <[a..yB..J_]> /
Damit werden Zeichen aus dem Bereich "a" bis "y" oder "B" bis "J" oder der Unterstrich "_" gefunden.
Man kann auch Zeichenklassen mit einem Minuszeichen negieren:
m/<-[a..z]>/; # alle Zeichen ausser Kleinbuchstaben m/<[a..z]-[ij]>/; # alle Kleinbuchstaben außer i und j
Man kann Zeichklassen und Regexes auch Namen geben:
token meine_zeichen { <[a..zA-U]> }; regex identifier { <meine_zeichen> \w* };
Dabei werden Regexes wie Funktionen definiert, und werden mit dem
Schlüsselwort token, rule oder regex
eingeleitet.
Innerhalb anderer Regexes kann man dann mit der Notation
<name> auf die Regex zugreifen (dabei sind keine
Leerzeichen vor oder nach dem Namen erlaubt).
Dabei ist token ein regex mit einem
impliziten :ratchet-Modifier (d.h. es findet kein
Backtracking innerhalb des Tokens statt), eine rule ist eine
regex mit implizitem :ratchet :sigspace.
Named Captures
Einer Regex einen Namen zu geben und in einer andere Regex zu verwenden hat noch einen weiteren Effekt:
token integer { \d+ }; my $str = "zahl: 25"; my $match = $str ~~ m/ ^zahl: \s <integer> /; if $match { say "Die Zahl war: $match<integer>"; } # Ausgabe: Die Zahl war 25
Da die subrule, hier integer, einen Namen
hat, wird eine sogenannte named capture angelegt. Wenn man
das Match-Objekt als Hash verwendet, kann man unter dem Namen der
subrule auf den Teil des Strings zugreifen, auf den die subrule
gepasst hat.
Der Inhalt einer named capture ist ebenfalls ein Match-Objekt:
token integer { \d+ }; my $str = "zahl: 25"; my $match = $str ~~ m/ ^zahl: \s <integer> /; if $match { my $z = $match{"zahl"}; say $z.from, " bis ", $z.to; say $z.WHAT; } # Ausgabe: # 7 bis 8 # Match
Wenn eine subrule mehrfach verwendet, wird unter dem Namen ein Array von Matches erstellt:
token integer { \d+ }; token add_op { \+ | - }; if my $match = "23+42" ~~ m/ <integer> <add_op> <integer>/ { say $match<integer>.elems; # Ausgabe: 2 say $match<integer>[0]; # Ausgabe: 23 }
Wenn man kein Freund von Arrays ist, kann man auch einen anderen Namen für den Match angeben:
token integer { \d+ }; token add_op { \+ | - }; if my $match = "23+42" ~~ m/ $<lhs>:=<integer> <add_op> $<rhs>:=<integer>/ { say $match<lhs>; # Ausgabe: 23 say $match<rhs>; # Ausgabe: 42 }
Named Captures auslassen
Angenommen, man möchte obige Regex so erweitern, dass auch Leerzeichen zugelassen werden (aber optional sind), könnte man das so machen:
token integer { \d+ }; token add_op { \+ | - }; if "3 + b" ~~ m/<integer> <ws> <add_op> <ws> <integer>/ { ... }
<ws> ist eine "builtin" regex, und steht für ein
(kontextsensitiv) optionales Leerzeichen (oder mehrere davon).
Dabei wird im Match-Objekt eine Liste von Leerzeichen angelegt - wenn man aber nur an dem verarbeiteten mathematischen Ausdruck interessiert ist, interessieren die Leerzeichen aber nicht. Wenn man sie nicht im Match-Objekt haben möchte, kann man vor den Namen ein Fragezeichen stellen:
token integer { \d+ }; token add_op { \+ | - }; if "3 + b" ~~ m/<integer> <.ws> <add_op> <.ws> <integer>/ { # $/ hat hier keinen <ws>-Eintrag }
Grammatiken
Durch das verschachteln von regexes und tokens kann man ganze Grammatiken aufstellen und wie eine regex benutzen. In diesem Fall spricht man von rules, also Regeln:
token integer { \d+ }; token add_op { \+ | - }; token mul_op { \* | \/ }; rule factor { <integer> [ <mul_op> <integer> ]* } rule expression { <factor> [ <add_op> <factor> ]* }
Damit kann man Mathematische Ausdrücke wie 2 + 3*5 -
4/23 erkennen (und entsprechend verarbeiten), während z.B.
2 3 (also zwei Zahlen, die nicht von einem Operator
getrennt werden) korrekterweise nicht erkannt werden.
Weitere Elemente von Regexes
Konjunktionen
Regexes unter Perl 6 können Konjunktionen enthalten, d.h. zwei (oder mehr) Regexes, die gemeinsam mit dem selben Start- und Endpunkt matchen müssen:
if "1 + 3 * 4" ~~ m/^[<expression> & ^<-[4]>+4 ]$/ { ... }
Dieses Beispiel passt auf mathematische Ausdrücke, die mit 1 anfangen, auf 4 enden und dazwischen keine weitere 4 haben.
Code in Regexes ausführen
Mit der Konstruktion <{...}> kann man Perl-code
ausführen, dessen Rückgabewert als Regex ausgewertet wird.
Zum Beispiel kann man damit eine ganze Zahl, gefolgt von ihrer Wurzel, abgerundet auf die nächste ganze Zahl, gematched werden:
m/ (\d+) <ws> <{ return $0.sqrt.int; }> /
Lookahead- und Lookbehind-Assertions
Mit <before ...> und
<after ...>
kann man vor und hinter eine Regex schauen, der Rest in den spitzen
Klammern wird als Regex interpretiert.
m/ <before foo> \d+ <after bar> / # ist fast das gleiche wie m /foo \d+ bar/
Der Unterschied ist, dass bei der zweiten Regex foo
und bar zum Match gehören, bei der ersten nicht.
Man kann alle Konstruktionen in spitzen Klammern auch mit einem Ausrufezeichen negieren:
m/ \d+ <!after <sp> Euro>/ # passt auf "3 Dollar", aber nicht auf "3 Euro"
Mit Rules parsen
Mit Rules kann man sehr leicht Parser schreiben, also Programme, die einen Text in eine interne Struktur verarbeiten.
Als Beispiel sollen einfache Konfigurationsdateien gelesen werden:
# Das ist ein Kommentar # Strings werden mit doppelten Anführungszeichen ip = "127.0.0.1"; timeout = 120; # Zahlen können einfach so darstehen # Variablen können ohne Anführungszeichen zugewiesen werden: other_timeout = timeout;
Das ist zwar noch leicht ohne Rules zu parsen, aber des pädagogischen Wertes wegen soll es mit Rules geparsed werden.
Ein erster naiver, aber gar nicht so schlechter Ansatz sieht so aus:
grammar config_file { rule config { <config_line>* }; # Kommentare müssen nicht gespeichert werden: rule confg_line { <?comment> | <assignment> }; token comment { '#' \N* \n }; rule assigment { <ident> '=' <expression> ';' }; rule expression { <number> | <quoted_string> | <ident> }; rule quoted_string { \" <-["]>* \" }; }
Das parsed eine Konfigurationsdatei, hat aber einen großen Nachteil: Wenn die Datei einen Syntaxfehler hat, dann matched die Regex nicht - aber es gibt keine Fehlermeldung, was schief gelaufen ist.
Alternative schafft z.B. folgende Konstruktion, hier nur an zwei Beispielen gezeigt:
rule config_line {
| <?comment>
| <assignment>
| <fail: "Expected comment or assignment">
};
rule assigment {
<ident>
[ '=' | <fail: "Expected '='"> ]
<expression>
[ ';' | <fail: "Expected ';'"> ]
};