Reguläre Ausdrücke

( http://www.perlscripten.de/modules.php?name=News&file=article&sid=169 )

Tutorials: Reguläre Ausdrücke
(TOOX-rudi auf Mittwoch, 03.Juli. @ 18:30:39 CEST)

Werkzeuge, die reguläre Ausdrücke verwenden, basieren auf einer von zwei, streng genommen drei Varianten dieser Maschinen: DFA (deterministischer finiter Automat), NFA (nicht-deterministischer finiter Automat) und POSIX-NFA. Der DFA betrachtet Zeichen für Zeichen und schreitet fort, wenn es auf den Ausdruck passt. Er markiert keine Zeichen, um zum Prüfen von Alternativen zurückkehren zu können. Eine DFA-basierte RegEx-Maschine kann nicht schon während des Suchprozesses einen Treffer speichern, um ihn zum Ersetzen zu verwenden. Das Verlängern von "Hund" zu "Hundekuchen" mittels s/(Hund)/lekuchen/ beherrschen DFAs deshalb nicht.

NFAs basieren auf Backtracking

Reguläre Ausdrücke in Perl basieren auf dem NFA, der anders vorgeht: Er merkt sich die Stellen, an denen mehr als eine Möglichkeit zu kontrollieren ist. Stellt er beim Testen einer Variante fest, dass der Gesamtausdruck nicht mehr zutrifft, geht er zurück zum "Scheideweg" und prüft die Alternative. Erst wenn alle abgehakt sind, entscheidet der NFA, ob der Ausdruck zutrifft oder nicht. Durch dieses "Backtracking" genannte Vorgehen beherrscht Perl nummerierte Rückbezüge wie s/(Ei) (Henne) /\2\1/9. Hier sorgen die Klammern dafür, dass Perl sich jedes "Ei" und jede "Henne" merkt. Im zweiten Teil vertauscht dann \2\1 die beiden miteinander. Perl versucht, den frühesten Treffer zu finden. So matcht es mit / [Ff]isch/ in "Fischers Fritze fischt frische Fische" Fischers - der erste Treffer - auch wenn fischt ebenso richtig wäre. Kommt aber ein Quantifizierer wie * ins Spiel, will der NFA so viel wie möglich finden - er wird gierig ("greedy"). Dabei ist die "Gierigkeit" stärker als die "Links-Bindung". Will man in HTML-Code einen bestimmten Tag erwischen, heißt der erste Versuch vermutlich: /<.*>/. Übersetzt: "Suche beliebig viele (auch gar kein) Zeichen, umschlossen von spitzen Klammern." Was würde Perl nun in der Zeile

<CENTER><H2>Heute frische Fische!</H2></CENTER>


finden? Alles von der ersten spitzen Klammer bis zur letzen hinter </CENTER>. Da ein Quantifizierer dabei ist, gilt nicht mehr "Treffer so weit links wie möglich" (also <CENTER>), sondern "So viel wie möglich" - wegen .* die gesamte Zeile. Dieses Verhalten findet man übrigens genauso im vi oder in grep. Perls Gierigkeit lässt sich jedoch durch ein hinter + oder * gesetztes Fragezeichen beschränken. Benutzt man im obigen Beispiel < . *?>, wird es <CENTER> finden. In solchen Fällen hilft ebenfalls das Motto "think negative": /<[^>]+>/ erledigt das Gewünschte in allen Werkzeugen. Dieser Ausdruck sucht ein <, dann etwas, was kein > ist, davon mindestens eines, schließlich ein >. Ähnlich geht man zum Beispiel vor, um Worte in Anführungszeichen zu finden: /" [^"] +"/ verrichtet diesen Job zuverlässiger als /".*"/.


Vorausschauende Suche

Perls reguläre Ausdrücke können vorausschauen, obern String passen könnte ("lookahead"). Mit

/Festessen(?=Champagner)/

findet der Interpreter "Festessen" - aber nur, wenn "Champagner" folgt. Schließt sich an "Festessen" jedoch "Lebensmittelvergiftung" an, trifft der Ausdruck nicht mehr zu. Das Ganze darf man auch verneinen: " Finde jedes Festessen, aber nur, wenn dem keine Lebensmittelvergiftung folgt" heißt:

/Festessen(?!Lebensmittelvergiftung)/


Der Konjunktiv unter den Ausdrücken funktioniert nur, weil der Perl-NFA Backtracking benutzt und illustriert, was es mit dieser Technik auf sich hat. Bei einem ? zum Beispiel (ein oder kein Zeichen) überprüft Perl zuerst, ob "ein Zeichen" wahr ergeben würde. Wenn nicht, kehrt es zum Scheidepunkt zurück und überprüft, ob "kein Zeichen" den Ausdruck als Ganzes wahr werden ließe. Nach diesem Punkt ist der Ausdruck bereits wahr oder falsch. Die Zeichen sind gefressen. Beim Lookahead geht Perl behutsamer vor: Es rennt nicht gleich um den ganzen Häuserblock, stellt dann fest, "hoppla das passt nicht mehr, also Haus für Haus zurück", sondern lugt erst einmal vorsichtig um die Ecke, ob es sich um das richtige Haus handelt es verbraucht keine Zeichen. Beim Lookahead wird der gesuchte Ausdruck nicht gesichert, obwohl er durch Klammern begrenzt ist. Seit Perl 5.005 gibt es übrigens auch den Blick zurück, "Lookbehind" ist durch (?<) und (?<!) implementiert. Für nummerierte Rückbezüge kennt Perl zwei Schreibweisen: Die Variablen $1, $2, $3 und so weiter enthalten jeweils den Wert des in der ersten, zweiten, dritten ... einfangenden Klammer gefundenen Musters. \1, \2 etc. sind Bestandteil der RegEx-Maschine. In ihnen steht ebenfalls der in der ersten, zweiten ... einfangenden Klammer gefundene Wert. Die Anzahl der Rückbezüge ist in beiden Fällen unbegrenzt. Vorsicht ist jedoch geboten: Aufgrund der Art und Weise, wie die RegExMaschine den Ausdruck interpoliert und kompiliert, gibt es durchaus einen unterschied zwischen $1 und \1. Empfohlen wird, innerhalb des Ausdrucks nur den Rückbezug mittels \1 zu verwenden, etwa

/(der|die|das) \1/


um doppelte Artikel zu finden. Da eine RegEx-Maschine Variablen wie $1 zunächst interpoliert und erst dann beginnt, nach Treffern zu suchen und die Klammern zu füllen, enthält $1 beim ersten Interpretieren eines Ausdrucks noch gar keinen Wert.

/(der|die|das) $1/


passt also auch auf einzelne Artikel (denn $1 ist leer} - vorausgesetzt, es gab vorher keine einfangende Klammer, die $1 mit einem Wert belegt hat. In diesem Fall könnte man den Treffer des vorherigen Ausdrucks wiederfinden, wenn dieser sich im gleichen Block befand. Im Ersetzen-Teil darf $1 vorkommen, denn dort ist die einfangende Klammer bereits gesucht und bewertet. Für "doppeltes Wort Wort finden und löschen" erledigt s/(\w+) \1/$1/ das Gewünschte, s/(\w+)$l/$l/ jedoch in der Regel nicht. \w+ ist übrigens Perl-ish für "mindestens ein Wortzeichen". Dies sind in der Regel die alphanumerischen Zeichen. Einer Zeichenklasse (etwa [a-z] } kann man beliebig Zeichen hinzufügen, die Definition von w steht fest, in Abhängigkeit von den Locale-Einstellungen. Neben $1, $2 etc. belegt Perl bei jeder RegEx-Auswertung einige Spezialvariablen neu: In $& findet sich immer der letzte gültige Treffer, in $' alles, was vor ihm und in $` das, was nach ihm lag. Wer viel mit Klammerung arbeitet, hat vielleicht noch Verwendung für $+: Es enthält den Wert der letzten passenden gruppierenden Klammer. Für den Text "der die das" setzt das Muster

/(der) (die) (Schüttelreim)?/


$+ auf "die". Will man einen Treffer nicht in $1 oder $2 speichern, kann man trotzdem Klammern zur Gruppierung benutzen, indem man (?:Bitte nicht speichern) verwendet. Rückbezüge sind dann nicht mehr möglich, alle anderen Eigenschaften der Gruppierung bleiben jedoch erhalten. Quantifizierer etwa sind auch mit (?:) benutzbar.


Mit oder quotemeta schützen

Reguläre Ausdrücke leben davon, dass man auseinander halten kann, wann welches Zeichen eine Sonderbedeutung hat und wann nicht. Enthält ein Ausdruck viele $, / oder (), die nicht für Zeilenende, Trenner oder Gruppierungen stehen, sondern für sich selbst, müssen sie geschützt (quotiert) werden. In Einzelfällen genügt dafür ein vorangestellter . Bei langen Ausdrücken oder vielen Sonderzeichen erlaubt Perl mehrere Varianten des Quotens, beispielsweise die Funktion quotemeta():

$string = quotemeta("Schützen Sie den \ oder den /,". "indern Sie einen \ vor den / oder den \ stellen. " "Benutzen Sie $ für $skalar.")


Sonst müsste man vor $ , / , \ und den . einen Backslash als Schutz setzen. Innerhalb eines Ausdrucks erreicht man das Gleiche mit

Q<-Beginn des zu quoten den Strings -
Ende des zu quoten den Strings->E.


Die () und die <> verlieren ihre Sonderbedeutung, und Perl behandelt sie als normale Zeichen. Einige Zeichen ändern ihre Bedeutung allerdings je nach Zusammenhang, zum Beispiel das Leerzeichen. Normalerweise ist es nur es selbst - es sei denn, man verwendet den Modifizierer x am Ende des Ausdrucks. Dadurch sind Leerzeichen und Kommentare erlaubt. Das verbessert vor allem bei komplizierten Ausdrücken die Lesbarkeit:

/"# öffnendes Anführungszeichen
[^"]+ # gefolgt von irgendwas != "
"/x # gefolgt von schließendem "


Wer hier ein Leerzeichen benötigt, das nicht ignoriert werden darf, muss es mit einem Backslash schützen. Perls s bietet sich nur dann als Ersatz an, wenn es auf die Art des Leerraums nicht ankommt, denn diese Abkürzung steht für jede Art von Leerraum - auch Tabulatoren und Newlines.


Beliebige Begrenzer statt /

Nicht nur Leerzeichen verändern ihre Bedeutung, sondern auch die Begrenzer des Ausdrucks. Oft kombiniert man den Modifizierer x mit m, um den / als normales Zeichen gebrauchen zu können. Der Perl-Befehl m schaltet explizit die Wahl eines neuen Begrenzers für den Ausdruck ein, er gilt nur für die Suche. Die übliche Schreibweise /Ausdruck/ ist eine Abkürzung für m/Ausdruck/ .

$path =- rn#([^/]*)$#;
$filename = $1;


erledigt dasselbe wie

$filename = (split("/", $path))[-1];


Beides extrahiert den Dateinamen ( "alles nach dem letzten / " ) aus einer Zeichenkette. m# sorgt dafür, dass man / ohne besondere Vorsichtsmaßnahmen im regulären Ausdruck benutzen kann. Beim Suchen und Ersetzen mit s kann ebenfalls ein anderes Zeichen benutzt werden. Vorsicht: Die Operatoren s und m, die vor einem Ausdruck stehen (s/ / / oder m/ /), sind nicht dasselbe wie die Modifizierer s beziehungsweise m, die ihm folgen. Perl unterstützt mit / /s und / /m zwei Varianten, wie ein. mit einem (Newline) umgehen soll und wie sich ^ (Zeilen- oder Stringanfang) und $ (Zeilen- oder Stringende) verhalten. Diese beiden Modifizierer schalten den Single- beziehungsweise Multiline-Mode ein. Ohne sie trifft der. kein Newline, und ^/ $ erreichen den Anfang respektive das Ende der Zeile, Letzteres erkennbar durch " ". Schaltet man nun den normalerweise auf " " stehenden Zeilentrenner $/ zum Beispiel auf "<BR>" um, passt ^ auf den Zeilenanfang und $ auf "<BR>". Dadurch hat sich nur die Definition von "Zeile" verändert. In Perl kann man eine Datei zeilenweise in ein Array @datei einlesen:

@datei = <datei.txt>;


Hier enthält jedes Element des Arrays eine durch $/ definierte Zeile. Alternativ lässt sich eine Datei als ein langer String in einem Skalar speichern:

undef $/;
$datei = <datei.txt>;


Da hier $/ nicht definiert ist, gibt es keine "Zeile", und Perl liest die ganze Datei in einem Rutsch. In diesem Fall matchen ^ und $ Stringanfang beziehungsweise -ende.


Newline ist nicht immer Zeilenende

Diese Verarbeitung einer Datei an einem Stück hilft beispielsweise beim Erfassen von Zeilenumbrüchen innerhalb von C-Kommentaren. Für diesen Fall steht der Singleline-Mode bereit, der den zu bearbeitenden Text wie einen langen String behandelt. Die Newlines sind noch vorhanden - aber nun erkennt der. sie. Im Singleline-Mode kann also //*.+*//s C-Kommentare mit Zeilenumbrüchen finden. In einem langen String ($/ = undef; ) hätte. ohne s die Newlines nicht gefunden. Im Multiline-Mode ändert sich wiederum das Verhalten von ^ und $ bezogen auf Zeilenanfang beziehungsweise -ende; der. findet hier kein Newline. Der Unterschied zwischen Multiline- und normalem Mode offenbart sich, wenn man $/ verändert. Setzt man es wie oben auf "<BR>", passen ^ und $ nun auf zweierlei: das durch $/ neu definierte Zeilenende - aber ebenso das logische Zeilenende und den logischen Zeilenanfang. Das steckt noch in der Datei, deshalb ist der logische Zeilenanfang die Stelle hinter dem und das logische Zeilenende das Zeichen vor dem Newline. Newlines verschwinden nicht durch eine Änderung von $/ , sondern markieren nur nicht mehr das Zeilenende. Wurde die gesamte Datei in einen String eingelesen, finden ^ und $ im Multiline-Mode auch die logischen "Zeilenenden Zeilenanfänge", die sich mitten im Text befinden - und nicht nur den Anfang der Datei beziehungsweise das Ende wie im normalen Modus. Dies sei an dem Beispieltext

Dies ist eine Gedichtzeile

und hier ist noch eine Zeile

und hier noch eine

und alles ohne Satzzeichen


erläutert ( ist hier nur der Deutlichkeit halber gezeigt), der als langer String in einem Skalar enthalten ist. Benutzt man nun den Multiline-Ausdruck /\w+$/m findet Perl "Gedichtzeile", denn $ erkennt auch logische Zeilenenden. Sucht man stattdessen /\w+$/s, heißt der Treffer "Satzzeichen", denn $ bezieht sich nun auf das Ende der gesamten "Datei".


Funktionen in Ausdrücken

Perl erlaubt in seinen regulären Ausdrücken "Formulierungen", die einen korrekten String ergeben. Liefert beispielsweise eine Funktion einen String zurück, darf man sie im Ersetzungsteil eines Ausdrucks verwenden, wenn man den Modifizierer e benutzt. Er sorgt dafür, dass die RegEx-Maschine alle Variablen interpoliert, den Ausdruck übersetzt, \1 etc. belegt und schließlich den Ersetzungsteil evaluiert. Man verwendet den e-Modifizierer zum Beispiel so:

$system = 'Ich benutze Windows.';
$system =- s/[\W\w]indows/&os/eg;
print "$system
";
sub os {
$string = `uname -a`;
return $1 if ($string =~ /(^\w+)/)


s/[\W\w]indows/&os/eg ersetzt jedes "Windows" und "windows" durch den Rückgabewert von os(). Diese Routine wiederum liefert das erste von `unome -a` zurückgelieferte Wort, so dass auf der Standardausgabe der Name des benutzten Betriebssystems erscheint - vorausgesetzt, uname ist vorhanden.


Umlaute und Sonderzeichen

Sonderzeichen außerhalb des ASCII-Bereichs unterstützt Perl nur unter bestimmten Bedingungen. Es wertet dann Umgebungsvariablen wie LC_CTYPE, LANG oder LC_ALL aus, wenn der Rechner die Locale-Einstellungen unterstützt. Das wirkt sich auf reguläre Ausdrücke vor allem bei Zeichenklassen sowie bei den Abkürzungen w und W aus. Kurz gesagt: In der Zeichenklasse [A-Za-z_] fehlen deutsche Umlaute normalerweise, man muss sie selbst hinzufügen. Ganz sicher geht, wer dabei statt des Umlauts die Nummer des Zeichens benutzt. Wendet man /w+/ auf den String "Wörter" an, findet Perl in der Regel keinen Treffer - mit /[A-Za-z_ö]+/ schon. Der Ausdruck /.+/ akzeptiert den Umlaut ebenfalls je nach Einstellungen. Deutsch-kompatibel wird Perl durch die Zeilen

use POSIX;
use locale;
setlocale(LC_CTYPE,"de_DE.ISO-8859-1");


am Anfang eines Skripts. Dann passt /\w+/ auch auf " Wörthersee". Die genauen Angaben für den zweiten Parameter von setlocale() unterscheiden sich leider je nach System. man perllocale liefert eine ausführliche Beschreibung. Dies war nur ein knapper Überblick über die Fähigkeiten, die Perl und seine regulären Ausdrücke mitbringen. Daneben bietet die Sprache eine Vielfalt spezieller Variablen und Funktionen, die ebenfalls für die Bearbeitung von Zeichenketten und Text verwendbar sind. split(), join(), substr() oder splice() mit regulären Ausdrücken kombiniert, erlauben anspruchsvollere Anwendungen als das zweihundertste Gästebuch im Web.


Aufbau regulärer Ausdrücke

.

ein beliebiges Zeichen außer Newline; passt im Singleline-Mode auf das -Zeichen

x

ein x

Wort

"Wort"

[xyz]

Zeichenklasse: trifft x, y oder z

[a-z]

Zeichenklasse: passt auf jedes Zeichen zwischen a und z

(Wort)

eingefangenes " Wort", wird in $i gespeichert; mit i im Ausdruck verwendbar

(?:Wort)

gruppieren, aber nicht speichern

x(?=y)

x wenn y folgt; Klammern speichern nicht

x(?!y)

x wenn kein y folgt. Klammern speichern nicht

\w

Wortzeichen

\w+

ganzes Wort {Wortzeichen mit folgendem Leer- oder Satzzeichen)

\W

Nicht- Wortzeichen: Satzzeichen, Leerzeichen und so weiter

\s

Leerraum: Leerzeichen, Tabulator, Newline

\S

alles, was kein Leerraum ist: Buchstaben, Ziffern etc.

d

Ziffer

*

vorangehender Ausdruck beliebig oft, auch gar nicht

+

vorangehender Ausdruck beliebig oft, mindestens einmal

?

vorangehender Ausdruck höchstens einmal

{4}

vorangehender Ausdruck genau 4-mal

{2,7}

vorangehender Ausdruck 2- bis 7 -mal

|

oder, auf Zeichen und Gruppen anwendbar: (ganz)|(gar nicht)

^

Anfang der Zeile oder des Ausdrucks

$

Ende der Zeile oder des Ausdrucks

[^x]

alles, was nicht "x" ist

\

Backslash, nimmt Sonderzeichen die Spezialbedeutung

\U

alle Zeichen bis \E in Großbuchstaben

\L

alle Zeichen bis \E in Kleinbuchstaben

\Q

alle Sonderzeichen bis \E mit schützen

\E

Ende von \U, \L, \Q

m//

suche

S///

suche und ersetze

S###

suche und ersetze, aber verwende # als Trenner

//g

jedes Vorkommen finden

//i

Groß- und Kleinschreibung ignorieren

//m

Multiline-Mode, ^ und $ passen auf logische Zeilenanfänge und -enden,

. matched kein Newline

//s

Singleline-Mode, ^ und $ erkennen Anfang/Ende des gesamten Strings,

. matched Newline

//sm

kombiniert: Logische Zeilen plus Newline

//x

Kommentare und Leerzeichen im Suchen- Teil erlaubt

s///e

kann im Ersetzungsteil einen Ausdruck erst evaluieren und dann ersetzen; erlaubt ist alles, was einen ersetzungsfähigen Ausdruck ergibt

$1

Wert der ersten einfangenden Klammer; nur im Ersetzungsteil verwenden

\1

Rückbezug auf die erste einfangende Klammer

$&

letzter gefundener Treffer

$'

Text nach dem Treffer

$`

Text vor dem Treffer

$+

Treffer der letzten einfangenden Klammer

$/

Zeilentrenner (Input Record Separator)