Verschachtelte reguläre Ausdrücke

(Auszug aus "Reguläre Ausdrücke" von Jeffrey E. F. Friedl)

Bei Programmiersprachen wie Perl können reguläre Ausdrücke eng mit dem Rest des Programms verzahnt werden. Als Beispiel bringen wir drei weitere Verbesserungen in unserem Programm an: Fließkommazahlen sollen wie vorher erlaubt sein, dazu aber auch die Kleinbuchstaben c und f, und zwischen der Zahl und dem Buchstaben sollen Leerzeichen zulässig sein. Damit sind Eingaben wie ››98.6‹ möglich.

Der Fließkomma-Teil ist bekannt; wir fügen ˹(\.[0­-9]*)?˼ in den Ausdruck ein:

if ($eingabe =~ m/^([-­+]?[0-­9]+(\.[0-­9]*)?)([CF])$/)

Der Teil kommt also innerhalb des ersten Klammerpaares zu liegen. Da wir den gefundenen Text aus diesem ersten Paar für die Temperatur-Berechnung brauchen, müssen wir sicherstellen, dass die Ziffern nach dem Dezimalpunkt auch dazugehören. Das neue Klammerpaar brauchen wir eigentlich nur zur Gruppierung für das nachfolgende Fragezeichen, trotzdem wird der dazu passende Text in einer Variablen aufgehoben. Die öffnende Klammer dieses Paares ist die zweite (von links), daher wird der Text in $2 abgelegt. Die folgende Abbildung illustriert das.

Verschachtelte Klammern

Abbildung: Verschachtelte Klammern.

Das Hinzufügen eines Klammerpaares in den regulären Ausdruck ändert die Bedeutung von ˹[CF]˼ nicht direkt, aber indirekt schon, weil jetzt die umgebenden Klammern zum dritten Klammerpaar des gesamten Ausdrucks geworden sind. Drittes Paar –- das bedeutet, dass wir im Programm bei der Zuweisung zu $Typ das $2 in $3 ändern müssen. (Eine andere Möglichkeit ist in Lösung 5 dargestellt.)

Das Zulassen von Leerzeichen ist nichts Besonderes. Wir wissen, dass einem Leerzeichen im regulären Ausdruck genau ein Leerzeichen im String entsprechen muss. Wenn wir also mit ˹*˼ beliebig viele Leerzeichen zulassen (aber nicht fordern), erhalten wir:

if ($eingabe =~ m/^([­-+]?[0­-9]+(\.[0-­9]*)?) *([CF])$/)

Das ist schon recht flexibel, aber da wir unser Programm praxistauglich machen wollen, brauchen wir einen regulären Ausdruck, der jede Art von Whitespace zulässt. Tabulatorzeichen (kurz Tabs) werden üblicherweise zum Whitespace gezählt. ˹TAB*˼ allein würde normale Leerzeichen nicht zulassen, also brauchen wir eine Zeichenklasse, die Tabs und Leerzeichen enthält: ˹[TAB]*˼.

Kurze Frage: Wie unterscheidet sich dies grundsätzlich von ˹(*|TAB*)˼? Die Antwort finden Sie in Lösung 6.

In diesem Buch sind Leerzeichen und Tabs wegen der typograschen Kennzeichnung als und TAB leicht zu erkennen. Auf dem Bildschirm sieht das unglücklicherweise anders aus. Wenn Sie irgendwo [ ]* sehen, können Sie vermuten, dass es sich um ein Leerzeichen und um ein Tab handelt, aber sicher sein können Sie nur durch Nachprüfen. Perl bietet dafür das Metazeichen ˹\t˼. Es erkennt nichts anderes als ein Tab. Sein einziger Vorteil ist der, dass es nicht wie eine Anzahl Leerzeichen aussieht, darum benutze ich es in meinen Beispielen. Damit wird aus ˹[TAB]*˼ der Ausdruck ˹[/t]*˼.

Ein paar andere »Abkürzungen«, Metazeichen, die das Leben erleichtern, sind ˹\n˼ (Newline), ˹\f˼ (ASCII Formfeed) und ˹\b˼ (Backspace). Aber hallo! Eben war ˹\b˼ noch das Metazeichen für eine Wortgrenze. Was ist es denn nun? Nun, es ist in der Tat beides!

Massenhaft Metazeichen

Wir hatten \n schon früher gesehen, dann aber innerhalb von Strings, nicht von regulären Ausdrücken. In Perl haben Strings in Anführungszeichen ihre eigenen Metazeichen, und diese sind von denen der regulären Ausdrücke grundsätzlich verschieden. Anfänger verwechseln diese leicht. (In VB.NET gibt es nur wenige String-Metazeichen, da besteht diese Gefahr weniger.) Immerhin gibt es einige Metazeichen, die in Strings und in regulären Ausdrücken vergleichbare Aufgaben erfüllen. So kann man das String-Metazeichen \t benutzen, um ein Tab in einen String zu bekommen, und man kann das gleich aussehende Regex-Metazeichen ˹\t˼ in regulären Ausdrücken benutzen, um nach einem Tab zu suchen.

So angenehm diese Ähnlichkeit auch ist, kann ich kaum genug betonen, dass es sich um zwei verschiedene Arten von Metazeichen handelt. Bei einem einfachen Zeichen wie \t mag das nicht allzu wichtig sein, aber wir werden später sehen, wenn wir etliche andere Sprachen und Werkzeuge behandeln, dass es sehr wichtig ist zu wissen, mit welcher Art von Metazeichen wir es zu tun haben.

Wir haben nämlich auch schon Metazeichen-Konflikte kennengelernt. Unter Einführung in reguläre Ausdrücke, bei egrep, hatten wir unseren regulären Ausdruck grundsätzlich in Hochkommas eingeschlossen. Die ganze Befehlszeile wird zunächst von der Shell interpretiert, und die Shell hat ihre eigenen Metazeichen. Zum Beispiel ist für die Shell das Leerzeichen ein Metazeichen, das Befehle von Argumenten und die Argumente untereinander trennt. Bei vielen Shells dienen Hochkommas dazu, zwischen den Hochkommas keinerlei Interpretation von Metazeichen vorzunehmen (bei DOS sind das Anführungszeichen).

Mit den Hochkommas war es uns möglich, reguläre Ausdrücke mit Leerzeichen zu formulieren. Ohne die Hochkommas hätte die Shell die Leerzeichen als Metazeichen aufgefasst und sie selbst interpretiert, statt sie unbesehen an egrep weiterzugeben, das sie auf seine Art als Metazeichen auffassen kann. Viele Shells kennen die Metazeichen $, *, ? usw., Zeichen, die wir auch in regulären Ausdrücken brauchen.

Nun hat diese Diskussion von Shell-Metazeichen und den String-Metazeichen in Perl wenig mit regulären Ausdrücken direkt zu tun, aber sehr viel mit dem Gebrauch von regulären Ausdrücken in realen Situationen. Im Verlauf dieses Buches werden wir viele und manchmal komplexe Sachverhalte vorfinden, wo wir auf Metazeichen aus unterschiedlichen Quellen Rücksicht nehmen müssen, diese aber auch für unsere Zwecke ausnutzen können.

Und nun zurück zum ˹\b˼: Das bezieht sich schon auf reguläre Ausdrücke. In einer Regex in Perl bezeichnet es normalerweise eine Wortgrenze, aber in Zeichenklassen steht es für das Backspace-Zeichen. Da Zeichenklassen immer auf einzelne Zeichen passen, ist es nicht sinnvoll, in diesem Zusammenhang von Wortgrenzen zu sprechen. Perl kann daher das \b für einen anderen Zweck benutzen. Die Warnung aus Einführung in reguläre Ausdrücke über die »eigene Sprache« von Zeichenklassen im Vergleich zu regulären Ausdrücken gilt auch für Perl (und jeden anderen Regex-Dialekt).

»Whitespace« mit \s

Bei der Erläuterung von Whitespace haben wir mit ˹[/t]*˼ aufgehört. Das ist schön und gut, aber bei vielen Regex-Dialekten gibt es auch dafür eine eigene Abkürzung. Während ˹\t˼ das Metazeichen für ein literales Tab ist, steht ˹\s˼ für eine ganze Zeichenklasse, die auf irgendein »weißes«, nicht druckendes Zeichen passt. Das sind Leerzeichen, Tab, Newline, Formfeed und ein paar andere mehr. In unserem Beispiel spielen Newline und Formfeed keine Rolle, und ˹\s*˼ ist einfacher zu tippen als ˹[\t]*˼. Nach kurzer Zeit gewöhnt man sich daran, und es ist auch in komplizierten Ausdrücken leicht lesbar.

Unsere Bedingung sieht nun so aus:

$eingabe =~ m/^([­-+]?[0-­9]+(\.[0-­9]*)?)\s*([CF])$/

Als Letztes wollen wir auch Kleinbuchstaben zulassen. Wir erweitern dazu einfach die Zeichenklasse: ˹[CFcf]˼. Es gibt aber auch eine andere Möglichkeit:

$eingabe =~ m/^([­-+]?[0-­9]+(\.[0-­9]*)?)\s*([CF])$/i

Das i nach dem m/.../ ist ein Modifikator (engl. modifier), der Perl instruiert, bei der gesamten Mustererkennung Klein- und Großbuchstaben als gleich zu betrachten. Das i gehört nicht zum regulären Ausdruck, sondern zum m/.../-Konstrukt, mit dem man Perl mitteilt, was mit welchem regulären Ausdruck zu tun ist. Etwas ganz Ähnliches hatten wir bei der ­-i-Option von egrep kennengelernt.

»Der i-Modifikator« ist etwas holprig, deshalb wird oft einfach »/i« benutzt (auch wenn kein zusätzlicher Slash zu schreiben ist). In Perl kann man Modifikatoren auch auf andere Weise angeben, wir werden das unter Features und Dialekte besprechen, und außerdem wird dort gezeigt, wie man das in anderen Sprachen macht. In diesem Kapitel werden wir noch weitere Modifikatoren antreffen, den Modifikator /g (»globales Matching«) und /x (»freie Form«).

Und so läuft unser neues Programm:

% perl -­w konvert
Geben Sie eine Temperatur ein (z.B. 32F, 100C):
32 f
0.00 C = 32.00 F
% perl -­w konvert
Geben Sie eine Temperatur ein (z.B. 32F, 100C):
50 c
10.00 C = 50.00 F

Oha! Ist Ihnen aufgefallen, wie trotz des c die zweite Temperatur als 50° Fahrenheit interpretiert wurde? Finden Sie den Grund, wenn Sie die Programm-Logik anschauen?

Der relevante Teil des Programms sieht so aus:

if ($eingabe =~ m/^([­+]?[0­9]+(\.[0­9]*)?)\s*([CF])$/i) {
      .          
      .
      .
    $Typ = $3; # ... damit der Rest des Programms lesbarer wird.
    
    if ($Typ eq "C") { # ›eq‹ vergleicht zwei Strings
      . 
      .
      .
    } else {
       . 
       .
       .

Wir hatten zwar die Regex geändert, so dass Kleinbuchstaben zulässig sind, nicht aber den Rest des Programms. Wenn $Typ nicht genau ein C ist, wird angenommen, dass Fahrenheit eingegeben wurde. Da aber auch ein kleines c für Celsius stehen kann, müssen wir das Überprüfen von $Typ anpassen:

if ($Typ eq "C" or $Typ eq "c") {

Oder auch, weil dies ja ein Buch über reguläre Ausdrücke ist:

if ($Typ =~ m/c/i) {

Nun läuft's. Das fertige Programm ist unten aufgelistet. Diese Beispiele zeigen, wie eng reguläre Ausdrücke mit dem Rest des Programms verschränkt sein können.

Temperatur-Konversionsprogramm –(Endversion).


print "Geben Sie eine Temperatur ein (z.B. 32F, 100C):\n";
$eingabe = <STDIN>; # liest eine Eingabe-Zeile
chomp($eingabe);    # entfernt das Newline von $eingabe
     
if ($eingabe =~ m/^([­+]?[0­9]+(\.[0­9]*)?)\s*([CF])$/i) {
     # Mustererkennung war erfolgreich. $1 ist die Zahl, $3 ist "C", "F", "c" oder "f".
     $EingabeZahl = $1; # in benannte Variablen kopieren, ...
     $Typ = $3;         # ... damit der Rest des Programms lesbarer wird.
     if ($Typ =~ m/c/i) { # "C" oder "c"?
          # Eingabe war in Celsius, also Fahrenheit berechnen
          $celsius = $EingabeZahl;
          $fahrenheit = ($celsius * 9 / 5) + 32;
     } else {
          # Muss ein "F" oder ein "f" sein, also Celsius berechnen
          $fahrenheit = $EingabeZahl;
          $celsius = ($fahrenheit ­ 32) * 5 / 9;
     }
     # Hier haben wir beide Temperaturen, Resultat ausgeben:
     printf "%.2f C = %.2f F\n", $celsius, $fahrenheit;
} else {
     # Regex passt nicht: Warnung ausgeben.
     print "Zahl, gefolgt von \"C\" oder \"F\", erwartet,\n";
     print "nicht \"$eingabe\".\n";
}

  

<< zurück vor >>

 

 

 

Tipp der data2type-Redaktion:
Zum Thema Reguläre Ausdrücke bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:
   

Copyright der deutschen Ausgabe © 2008 by O’Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "Reguläre Ausdrücke" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O’Reilly Verlag GmbH & Co. KG, Balthasarstr. 81, 50670 Köln