Mit RegEx suchen und finden

Reguläre Ausdrücke: Einsatzgebiete im Entwickleralltag
Kommentare

Für reguläre Ausdrücke gibt es jede Menge Einsatzgebiete; bei der alltäglichen Programmierarbeit sind sie quasi unverzichtbar. Besonders hilfreich sind RegEx als Regelwerk zum Prüfen einer Syntax oder eines Formats. Dieser Bericht zeigt Einsatz und Beispiele aus den Gebieten Web, DB und Objektstrukturen. Denn wer sucht, will gefunden werden.

Doch damit nicht genug: Man kann mit regulären Ausdrücken die Eigenschaften, z. B. das Vorkommen bestimmter (Arten von) Zeichen, Reihenfolge, Häufigkeit etc. eines Textes oder einer Datenmenge allgemein beschreiben und danach suchen oder ersetzen lassen. Dazu sollten wir uns zunächst einmal das Regelwerk ansehen.

Ein Regelwerk

Reguläre Ausdrücke (regular expressions) sind sehr leistungsfähig und ermöglichen das komplizierte und herausfordernde Zerlegen (Parsing) von Texten. Durch einen Quasi-Standard sind auch die Muster hochgradig wiederverwendbar. In dem Sinne gibt es keine irregulären Ausdrücke.
Was sind RegEx? Sie dienen dazu, Muster als Suchmasken oder Filter zu definieren; Muster, oder neudeutsch Patterns, die Klassen von Zeichenketten beschreiben und festlegen.
RegEx-Methoden haben in den meisten Umgebungen das vordefinierte Objekt namens Regex. Sie können Regex schreiben, gefolgt von einem Punkt, um die verfügbaren Methoden zu sehen. Zum Beispiel: RegEx.Options:= [preCaseLess, preMultiline];
Alle Methoden akzeptieren einen Muster-String (pattern string) – das Muster des regulären Ausdrucks. Beachten Sie, dass die kompilierten Muster intern zwischengespeichert werden. Es gibt also keinen Performanceverlust bei mehrfacher Verwendung des gleichen Musters.

Betrachten wir mal einen ersten kleinen RegEx, der seinen Nutzen auch in Suchmasken oder Editoren hat (Abb. 1). In der Regel kennt man Ausdrücke wie *.txt z. B. vom Dateimanagement, somit ist das Prinzip der regulären Ausdrücke bereits bekannt. Einzelne Sonderzeichen (wie hier der Asterisk, *) haben eine spezielle Bedeutung und stehen z. B. für beliebigen Text. Im obigen Beispiel steht der * als Platzhalter für einen beliebigen Dateinamen. Es geht nun um das Suchen oder Ersetzen von leeren Zeilen, sozusagen das „Hello World“ bei RegEx: ^$
Bereits bei diesen ersten zwei Zeichen (auch Anchors genannt) kommt der Kontext ins Spiel, der mit Tabelle 1 einhergeht.

RegEx-Zeichen Funktionalität
^ , A Beginn des Strings
$ , Z String-Ende
[] Auswahlmöglichkeiten einzelner Zeichen/ODER
[^] Negative Auswahlmöglichkeiten einzelner Zeichen/NICHT ODER
. Beliebiges Zeichen
? Kein- oder einmal, {0,1}
* Keinmal bis beliebig oft {0,} (gierig, greedy)
+ Ein- oder mehrmals {1,} (gierig)
*? Keinmal bis beliebig oft {0,}? (nicht gierig)
+? Ein- oder mehrmals {1,}? (nicht gierig)
{7} Siebenmal
{3,5} Drei- bis fünfmal
{4,} Viermal oder mehr (mindestens)

Tabelle 1: RegEx-Kontext

Das „^“ steht für Zeilenumbruch oder Vergleich am Anfang einer Zeile aber auch als Negation. Beispielsweise: „^b“ bedeutet alle Zeichen außer „b“ – also eine Negativauswahl.
Das „$“ wiederum meint Zeilenumbruch oder Vergleich am Ende einer Zeile. Falls die Multiline-Option aktiv ist, wird zusätzlich jeder Zeilenanfang und jedes Zeilenende gefunden.
Falls dieser Mehrzeilenmodus gewählt ist, findet „^“ jeden Beginn einer neuen Zeile und „$“ jedes Zeilenende sowie die Position vor dem Ende der Zeichenfolge bzw. vor dem letzten „n“ (neue Zeile). Falls die Option fehlt, finden die beiden Ausdrücke nur den absoluten Beginn bzw. das Ende der Eingabezeichenfolge. Ein kleines Beispiel inklusive Optionen gefällig?

RegEx.Options:= [preCaseLess, preMultiline];
regex:= '^No.*0$';
Firstmatch: northwest   NW  Charles Main    30000.00
NextMatch:  Northeast   NE  AM Main Jr.     57800.10

Es wird also jede ganze Zeile (.*) gefunden, die mit „no“ beginnt und mit „0“ endet, zusätzlich wird die Groß- und Kleinschreibung nicht unterschieden. Trainieren wir gleich weiter.
Ausgehend von unserem Anfangszeichen wollen wir uns als Nächstes ansehen, wie wir alle Zeilen ausgeben lassen, die mit einem Großbuchstaben beginnen:

Regex:= '^[A-Z]';
Firstmatch: Western     WE   Sharon Gray    53000.89
NextMatch:  Southern    SO   Suan Chin      54500.10
NextMatch:  Northeast   NE   AM Main Jr.    57800.10

Neu hinzugekommen ist die Auswahlmöglichkeit „[]“ mit Oder-Logik (Tabelle 1). Aber aufgepasst, dies war eine Falle! Denn solange die Option „[preCaseLess]“ eingestellt ist, wird jede Zeile mit einem simplen Buchstabenanfang angezeigt, doch wir wollen ja explizit nur nach Großbuchstaben suchen! Bisher haben wir noch trocken geübt, doch wie sieht das im Tool Grep oder maXbox aus?

Beide sind im RegEx-Dialekt „PCRE“ (Perl-compatible regular expressions) zuhause. Und Sie haben richtig gelesen, Dialekte gibt es nicht nur in SQL. (PHP unterstützt übrigens ebenfalls RegEx, jedoch mit einer leicht anderen Syntax).
Das Programm, mit dessen Hilfe wir diesen RegEx formulieren werden, ist grep. Der Name „grep“ ist, wie so oft in der UNIX-Welt, ein Akronym. In diesem Fall für „global regular expression print“. grep arbeitet, wie die meisten RegEx-fähigen Programme, zeilenorientiert. Wird das geforderte Muster in einer Zeile der angegebenen Datei gefunden, wo wird diese auf stdout alias writeln (meist also dem Monitor) ausgegeben, andernfalls ignoriert grep ihre Existenz ganz einfach. RegEx – und damit auch grep – sind im Übrigen „case sensitive“, sie unterscheiden also zwischen Groß- und Kleinschreibung.
Trainieren lässt sich das obige Konstrukt auch in der Box. In Listing 1 finden Sie einen Auszug aus dem Script 309_regex_powertester3.txt, das Sie hier finden können.

rx:=TPerlRegEx.Create;
  try
    rx.RegEx:= '^[A-Z].*';
    rx.Subject:= fs; //fstr;
    rx.Options:= PR1.Options + [preMultiline, preCaseLess];
    if rx.Match then begin
      WriteLn('Firstmatch: '+rx.MatchedText);  
    while PR1.MatchAgain do
      WriteLn('Nextmatch: '+rx.MatchedText); // Extract subsequent

Es gibt auch die Möglichkeit einer Funktion (als Funktionsobjekt):

if ExecRegExpr('^[A-Z].*',fs)
  then writeln('regex found') else writeln('regex not found');

Grundsätzlich hat der Zeichensatz, vor allem bei Multiline, im jeweiligen Betriebssystem einen Einfluss. Denn jeder Zeichensatz ist von 0 bis 127 ASCII gültig, und genau dort spielt sich ja auch die CRLF-Tragödie ab („CarriageReturn#13, LineFeed#10“). Schließlich geht es um korrekte Datenverarbeitung und nicht um wilde Datenkorruption:

$zeile =~ /^n$/ ;

Oder dasselbe für Windows (manchmal):

$zeile =~ /^rn$/ ;

Dekodieren wir nun weiter die Abbildung 1 und beenden Anfang ^ und Ende $ – RegEx kann auch philosophisch sein. Jedes Zeichen aus dem Zeichenbereich a bis z oder A bis Z wird gefunden, d. h. das „+“ steht für ein oder mehr Zeichen. Und bei zwei Mengen, die einander folgen, gilt die Oder-Regel.

Abb. 1: RegEx im Editor

Für jede Methode gibt es zwei Varianten. Der Unterschied zwischen den Varianten ist, dass die zweite einen Option-Parameter enthält, der auf das Verhalten der RegEx-Engine wirkt. In der Regel hat jeder RegEx mindestens zwei Optionen, „CASE_INSENSITIVE“ und „MULTILINE“. Die Erstere macht den Mustervergleich unempfindlich gegen Groß- und Kleinschreibung, und MULTILINE setzt die String-Anker „^“ und „$“ auf Beginn und Ende jeder Zeile statt Beginn und Ende des ganzen Strings – je nachdem wie gierig die Engine ist. Nun, was heißt gierig (greedy)?
grep, Perl, PCRE ist in der Standardeinstellung „gierig“, d. h. ein „*“ oder „+“ versucht immer so viele Zeichen wie möglich zu sammeln, sodass die RegEx 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 so viele Zeichen wie absolut notwendig, um die RegEx noch gültig zu machen. Das müssen wir einmal klar als Beispiel sehen, da es hier immer wieder Differenzen gibt:

RegEx.Options:= [prepreCaseLess, preMultiline];
regex:= '^No.*';
Firstmatch: northwest  NW Charles Main  300000.00
NextMatch:  Northeast  NE AM Main Jr.   57800.10
NextMatch:  north NO   Ann Stephens     455000.52

Das ist also der Default: gierig. Nun explizit „ungierig“:

RegEx.Options:= RegEx.Options +[ preUnGreedy];
regex:= '^No.*';
Firstmatch: no
NextMatch:  No
NextMatch:  no

Er gibt nur das Minimum als „no“ aus. Eigentlich einfach, wenn man es einmal gesehen hat. Doch nun der Hammerausdruck: Bei eingestelltem End-Anker „$“ –  also „^No.*0$“ – ist dann wieder die ganze Zeile gefragt, auch wenn nicht gierig eingestellt ist (eigentlich auch logisch)!

Firstmatch: northwest   NW  Charles Main    30000.00
Nextmatch:  Northeast   NE  AM Main Jr.     57800.10

Das Wissen um greedy kann auch im Zusammenhand mit der Zwischenablage (Klammern (), auch Group Operator) notwendig sein, um zu definieren, wie viel Text in die entsprechende Zwischenablage kopiert werden soll.
Als Nächstes kommt die Geschichte vom Ersetzen, und da kann ich auch gleich die „Back Reference“ einführen. Nachdem wir das Suchen nun in seiner Tiefe behandelt haben, bereitet uns das Ersetzen keine großen Probleme mehr, hoffe ich doch.
Mit dem Konstrukt aus Listing 2 lässt sich ungierig (minimal) in einem Gruppen-Operator (Klammer) folgende Ersetzung bilden: „Foo is the name of the bar I like“.
Die RegEx nimmt das Wort vor „bar“ nach seiner Gültigkeit und fügt es in Replacement ein. Das Gebilde „1“ nennt man eine Back Reference (Rückverweis), der einen Buffer oder Zwischenspeicher ermöglicht. Ein Rückverweis sucht die durch die Nummer in „1“ nach Position oder die durch den Namen in „“ definierte aufgezeichnete Teilzeichenfolge, die in die runde Klammer gesetzt ist.

procedure PerlRegexReplace;
begin
  with TPerlRegex.create do try
    Options:= Options + [preUnGreedy];
    Subject:= 'I like to sing out at Foo bar';
    RegEx:= '([A-Za-z]+) bar';
    Replacement:= '1 is the name of the bar I like';
    if Match then ShowMessageBig(ComputeReplacement);
  finally
    Free;
  end; 
end;

Wirklich nützlich und kreativ ist folgendes kleine Webbeispiel (Ich nutze der Einfachheit wegen das Funktionsobjekt ReplaceRegExpr direkt:

Writeln(ReplaceRegExpr('<.*?>','Dies ist ein <b>Text</b> mit <i>HTML</i>-Kennzeichen', '',true));

Aufmacherbild: Happy cute girl holding paper with funny smiley drawing on gradient background von Shutterstock / Urheberrecht: ra2studio

[ header = Seite 2: Wer sucht, der bindet ]

Das Ergebnis: 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 „>“ wird alles durch nichts ersetzt; deshalb ist der dritte Parameter ein Leer-String. Die RegEx selbst ist als erstes Argument zu finden: <.*?>. Die spitzen Klammern gehören aber zum Text als HTML und sind kein RegEx-Operator oder dergleichen. Wichtig ist hier das „*?“ in der RegEx, also die Definition von nicht gierig, da ansonsten der Text vom ersten „<“ bis und mit dem letzten „>“ ersetzt werden würde (Was ein Fragezeichen alles bewirkt!):

Regex:= <.*> // falsch
// Dies ist ein –Kennzeichen
Regex:= <.> // auch falsch
// Dies ist ein Text</b> mit HTML</i>-Kennzeichen

Für weitere Tests lässt sich als Input String auch mal eine Datei nutzen: Mit der Funktion ReplaceRegExpr() sind folgende Parameter definiert:

function ReplaceRegExpr (const ARegExpr, AInputStr, AReplaceStr: string; AUseSubstitution: boolean = False): string;

Diese beiden Suchen-und-Ersetzen-Beispiele geben genug Hirnschmalz zum Braten und Trainieren. Lassen wir am Schluss noch die Musik erklingen und analysieren die nächste RegEx in Listing 3.

with TRegExpr.Create do try
  gstr:= 'Deep Purple';
  modifierS:= false; // non greedy
  Expression:= '#EXTINF:d{3},'+gstr+' - ([^n].*)';
     if Exec(fstr) then 
      Repeat
         writeln(Format ('Songs of ' +gstr+': %s', [Match[1]]));
        if AnsiCompareText(Match[1], 'Woman') > 0 then begin
          closeMP3;
          PlayMP3('..EKON166_Woman_From_Tokyo.mp3');
        end;
      Until Not ExecNext; 
  finally Free;
end;

Hier setze ich eine Bibliothek aus dem RegExp Studio (Abb. 2) ein, die sich auch am PCRE-Dialekt orientiert, wobei die Objektnamen Unterschiede zeigen.
Die RegEx #EXTINF:d{3},’+gstr+‘ – ([^n].*) sucht aus einer zuvor mit Exec(fstr) eingelesenen m3u-Liste alle Songs einer bestimmten Band und spielt die dann ab. Die runde Klammer dient wieder als Zwischenspeicher, der unter match[n] gespeichert ist. Wobei dann match[1] als Resultatliste einen definierten Song vergleicht und abspielt.
Das Erstaunliche ist die Iteration, die den Programmfluss erst verlässt, wenn kein definierter Song mehr gefunden wurde. Zudem lassen sich bei mehr als einem Resultat die Lieder asynchron abspielen, was aber offensichtlich hörbar keinen Sinn ergibt; oder hat man das 3-D-Hören noch nicht erfunden?

Abb. 2: RegEx in der Box

Ein Programm, mit dessen Hilfe wir auch RegEx formulieren und testen können, ist der RegexBuddy. Dieser Editor zum Erstellen und Testen regulärer Ausdrücke ist sehr beliebt, hat aber seinen kleinen Preis (ca. 30 Euro). Man kann auch normale englische Bausteine anstelle der komplizierten Syntax zum Erstellen eines RegEx nutzen. Besonders hilfreich ist der Codegenerator – vom definierten Ausdruck zum Code hin. Das mit dem Tool erstellte Konstrukt steht dann in den verschiedensten Sprachen als Code-Snippet zum Kopieren bereit (Abb. 3).

Abb. 3: RegexBuddy im Codegenerator

Wer sucht, der bindet

Wer versucht, der findet – so könnte dieser Absatz ebenfalls überschrieben sein. Besonders aufmerksame Leser werden sich fragen, wie man denn RegEx-Zeichen selbst findet. Wie zum Beispiel ist es möglich, die Zeichen „^“ oder „-“ innerhalb einer eckigen Klammer als solche zu matchen? Ganz einfach: Will man „^“ innerhalb eines solchen Ausdrucks finden, stellt man sie einfach nicht an den Anfang der Liste. Möchte man „-“ matchen, stellt man es schlicht an den Anfang, also direkt nach der eckigen Klammer, oder ans Ende. Ein analoger Fall ist das Aufspüren von „[“ und „]“, die wir auch an den Anfang stellen. Ein Muster, das alle unsere eben kennen gelernten „Problemzeichen“ erledigt, ist das folgende: []^[-].

'Extra2 ^[A-Z]****[0-9]..$5.00'+CR+LF;
// Firstmatch:^, Nextmatch: [-][-]

Also andere RegEx-Zeichen, bis auf den Escape-Char selbst, verlieren innerhalb von „[“ und „]“ ihre Sonderbedeutung und bereiten uns keine weitere Schwierigkeit mehr.
Ähnlich gelagert ist der Asterisk. In diesem Fall kommt das so genannte „Escapen“, das aus vielen Anwendungen der Programmierung vertraut ist, ins Spiel: einem gewissen Zeichen, meist „“ (als „Backslash“), wird eine besondere Bedeutung verliehen. Wird „“ vor ein Zeichen aus dem RegEx-Zeichensatz gestellt (Zeichensatz: $ ^ { [ ( | ) ] } * + ? ), verliert dieses Zeichen seine Sonderbedeutung – und wird zu einem ordinären Literal, sozusagen einem Zeichen mit „wörtlicher Bedeutung“. Das gilt übrigens auch für „“ selbst; will man einen oder mehr Backslash finden, muss man also „\“ notieren.
In vielen Situationen, in denen es um Wiederholungen geht, helfen uns Quantoren. Quantoren sind Zeichen aus dem RegEx-Zeichsatz (siehe oben), die vorangegangene Muster mit dem Kriterium „Anzahl von Wiederholungen“ erweitern. Manchmal will man z. B. bei einer IP-Adresse wissen, dass eine Zahl zwischen 12- oder 24-mal vorkommen muss. Es gibt drei vordefinierte (*/+/?) sowie selbstdefinierte Quantoren. Letztere werden durch Zahlen in geschweiften Klammern ausgedrückt. Hier also kommen die curly brackets ins Spiel.
Mittels „{“ ist es möglich, einen festgelegten Bereich gültiger Wiederholungen zu setzen. Und zwar mit „{n,m}“ – wobei n die Unter- und m die Obergrenze dieser Anzahl definieren. Als gültiges Jahr könnte man akzeptieren:

b(d{2,4}) // Jahrzahl
'd{1,3}.d{1,3}.d{1,3}.d{1,3}' // IP-Adresse

Mit solchen Quantoren hat man übrigens einen großen Teil der menschlichen DNA beim Human Genom Project entziffert. Ein besonderer Buchtipp hierzu ist „Perl for Exploring DNA“.
Hier kommt ein weiter Trumpf. Wenn ich bspw. nur nach Zahlen suche, lässt sich mit „d“ (auch schreibbar als „[0-9]“) die RegEx abkürzen, oder ich will keine Zahlen: „D“ (auch als „[^0-9]“). Alle alphanumerischen Zeichen, inkl. „_“ und Zahlen, aber ohne Umlaute, auch schreibbar als „[a-zA-Z0-9_]“, lassen sich einfach mit „w“ abkürzen.
Abschließend noch ein Funktionsmuster, das extensiv vom Zwischenspeicher Gebrauch macht. Die RegEx in Listing 4 sucht alle Telefonnummern mit dem Citycode 812. Die ersten beiden Matches sind mögliche Ländercodes/Vorwahl, dann folgt mit Match[3] der Citycode und abschließend die Telefonnummer.

procedure ExtractPhones(const AText: string; APhones: TStrings);
  begin
    with TRegExpr.Create do try
      Expression := '(+d*)?(((d+))*)?(d+(-d*)*)';
      if Exec (AText) then
      Repeat
        if Match[3] = '812'
        then APhones.Add(Match [4]);
      Until not ExecNext;
    finally Free;
  end;
end;

Wer erst einmal etwas tiefer ins Thema RegEx eingestiegen ist, wird sehr schnell erkennen, dass Reguläre Ausdrücke auch nicht wesentlich schwerer als Rechnen mit Brüchen und Prozenten zu erlernen sind.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -