Iterative Mustersuche – Skalarer Kontext mit dem /g-Modifikator

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

Das m/.../g im skalaren Kontext unterscheidet sich deutlich von den drei anderen Konstrukten. Wie beim normalen m/.../ wird nur ein Treffer gesucht, aber es spielt wie beim m/.../g im Listenkontext eine Rolle, an welcher Stelle im String der letzte Treffer geendet hat. Bei jeder neuen Anwendung von m/.../g, meist in einer Schleife, wird der jeweils »nächste« Treffer gefunden. Wenn kein Treffer mehr gefunden wird, wird beim nächsten Mal im String wieder von vorne begonnen.

Hier ein einfaches Beispiel:

$text = "NAJA! Dies ist ein DUMMER Test.";

$text =~ m/\b([a-z]+\b)/g;
print "Das erste Wort nur aus Kleinbuchstaben: $1\n";

$text =~ m/\b([A-Z]+\b)/g;
print "Das erste darauf folgende GROSSGESCHRIEBENE Wort: $1\n";

Beide Match-Operatoren werden im skalaren Kontext aufgerufen und verwenden /g. So ergibt sich:

Das erste Wort nur aus Kleinbuchstaben: ist
Das erste darauf folgende GROSSGESCHRIEBENE Wort: DUMMER

Die zwei Mustersuchen mit skalarem /g arbeiten zusammen: Die erste setzt die »aktuelle Position« hinter das gefundene Wort, das nur aus Kleinbuchstaben besteht, und die zweite fährt von dieser Position aus fort und findet das darauffolgende Wort, das nur aus Großbuchstaben besteht. Das /g ist in beiden Fällen notwendig: im ersten, damit nach dem Treffer die »aktuelle Position« gesetzt wird, im zweiten, damit ab dieser Position gesucht wird. Wenn das /g bei nur einem der Match-Operatoren fehlte, würde in der zweiten Zeile ›NAJA‹ gefunden.

Ein Matching mit einem skalaren /g ist sehr praktisch bei der Formulierung von while-Schleifen wie dieser:

while ($KonfigurationsDaten =~ m/^(\w+)=(.*)/mg) {
    my($schluessel, $wert) = ($1, $2);
       .
       . 
       .
}

Wie im Listenkontext werden alle Treffer gefunden, aber dazwischen (genauer: nach jedem erfolgreichen Treffer) wird eine Iteration der while-Schleife ausgeführt. Wenn ein Fehlschlag auftritt, ist der Rückgabewert falsch, und die Schleife wird beendet. Außerdem wird die »aktuelle Position« auf den Anfang des Strings zurückgesetzt. Die nächste Mustersuche mit /g beginnt also wieder von vorn.

Vergleichen wir

while ($text =~ m/(\d+)/) { # Achtung!
    print "Gefunden: $1\n";
}

und

while ($text =~ m/(\d+)/g) {
    print "Gefunden: $1\n";
}

Der Unterschied ist das kleine /g, aber die Auswirkungen sind beträchtlich. Wenn $text beispielsweise die IP-Adresse aus dem früheren Beispiel enthielte, bekämen wir mit der zweiten Variante:

Gefunden: 64
Gefunden: 156
Gefunden: 215
Gefunden: 240

Die erste Variante hingegen spuckt immer wieder »Gefunden: 64« aus. Ohne /g bedeutet die Mustersuche ganz einfach: »Finde das erste ˹(\d+)˼ in $text«, und das ist in der Tat ›64‹, ganz egal, wie oft wir danach suchen. Mit dem angehängten /g wird die Mustersuche im skalaren Kontext aber zu »Finde das nächste ˹(\d+)˼ in $text«. Das findet der Reihe nach alle Zahlen und beendet dann die Schleife.

Die »aktuelle Position« und die pos()-Funktion

Zu jedem String in Perl gibt es eine »aktuelle Position«, an der das Getriebe die Regex-Maschine für die nächste Mustersuche ansetzt. Es handelt sich um eine Eigenschaft des Strings, nicht eines regulären Ausdrucks. Wenn ein String erzeugt oder verändert wird, wird diese »aktuelle Position« auf den Anfang des Strings zurückgesetzt, aber nach einer erfolgreichen Mustersuche mit /g verbleibt diese Position am Ende des gefundenen Treffers. Beim nächsten Matching mit /g wird an dieser »aktuellen Position« im String weitergesucht.

Mit der Funktion pos(...) kann man die »aktuelle Position« eines Strings abfragen:

my $ip = "64.156.215.240";
while ($ip =~ m/(\d+)/g) {
   printf "Zahl '$1' endet an Position %d\n", pos($ip);
}

Das ergibt:

Zahl '64' endet an Position 2
Zahl '156' endet an Position 6
Zahl '215' endet an Position 10
Zahl '240' endet an Position 14

(Zur Erinnerung: Die Zählung beginnt bei Null, also ist »Position 2« genau vor dem dritten Zeichen im String.) Nach einer erfolgreichen Suche mit /g enthält außerdem $+[0] (das erste Element des Arrays @+, siehe @- und @+) den gleichen Wert, den pos() zurückgibt.

Ohne Argument bezieht sich pos() auf den gleichen Standard-Suchstring wie der Match-Operator: die Variable $_.

Aktuelle Position im String mit pos setzen

Interessant an pos() ist vor allem, dass man diesem Operator auch einen Wert zuweisen und so den Startpunkt im String für die nächste Mustersuche (mit /g natürlich) festlegen kann. Ein Beispiel: Die Webserver-Logdateien, mit denen ich bei Yahoo! zu tun hatte, hatten ein eigenes Format, bei dem der Pfadname der Seite nach 32 Bytes kommt. Diese 32 Bytes kann man auf mehrere Arten überspringen, beispielsweise mit ˹^.{32}˼:

if ($Logzeile =~ m/^.{32}(\S+)/) {
    $Webseite = $1;
}

Das ist nicht besonders elegant: Die Maschine muss sich durch jedes der 32 Zeichen durcharbeiten. Es ist effizienter und klarer, wenn wir den Startpunkt explizit setzen:

pos($Logzeile) = 32;          # Der Pfadname der Webseite beginnt nach dem 32. Zeichen ...
if ($Logzeile =~ m/(\S+)/g) { # ... also mit der Mustersuche dort beginnen.
    $Webseite = $1;
}

Das ist besser, aber nicht ganz dasselbe. Wir veranlassen die Maschine dazu, erst nach 32 Zeichen mit dem Matching anzufangen, aber wir verlangen nicht, dass bis dahin irgendetwas passt wie bei der ersten Version. Wenn – aus welchem Grund auch immer – das 32. Zeichen von ˹\S˼ nicht erkannt wird, schlägt die erste Variante fehl, aber bei der zweiten setzt einfach das Getriebe ein und fährt im String Zeichen für Zeichen fort. Vielleicht wird später im String etwas gefunden, worauf ˹\S+˼ passt, aber das wäre ein Fehler. Der nächste Abschnitt zeigt eine einfache Möglichkeit, das zu verhindern.

Gebrauch von ˹\G˼

Das Metazeichen ˹\G˼ ist der »Anker für die Position, an der der letzte Treffer geendet hat«. Genau das benötigen wir für das Problem aus dem letzten Abschnitt:

pos($Logzeile) = 32;            # Der Pfadname der Webseite beginnt nach dem 32. Zeichen ...
if ($Logzeile =~ m/\G(\S+)/g) { # ... also mit der Mustersuche dort beginnen.
    $Webseite = $1;
}

Das ˹\G˼ verhindert das Weiterschalten des Getriebes – wenn die Regex an der aktuellen Position nicht passt, muss ein Fehlschlag zurückgegeben werden.

Das Metazeichen ˹\G˼ wurde schon früher behandelt: unter Features und Dialekte in einer allgemeinen Einführung (siehe Beginn der neuen (oder Ende der letzten) Mustersuche: \G) und in einem ausführlichen Beispiel unter Regex-Methoden aus der Praxis (siehe Mit \G im Takt bleiben).

Beachten Sie, dass ˹\G˼ in Perl nur dann zuverlässig funktioniert, wenn es das erste Zeichen in einer Regex ist und wenn die Regex keine Alternation auf der äußersten Ebene enthält. Die optimierte Regex aus Die Kunst, reguläre Ausdrücke zu schreiben für das CSV-Problem (siehe Aufbrechen der Schleife bei CSV-Daten) beginnt mit ˹\G(?:^|,)...˼. An sich bräuchte man ˹\G˼ nicht, wenn das noch weiter einschränkende ˹^˼ passt, daher könnte man versucht sein, stattdessen ˹(?:^|\G,)...˼ zu benutzen. Das aber funktioniert in Perl nicht und ergibt unvorhersagbare Resultate. (Anmerkung: Das würde bei den anderen Regex-Dialekten, die ˹\G˼ unterstützen, durchaus funktionieren. Dennoch empfehle ich diese Form nicht. Der Gewinn bei der Optimierung eines ˹\G˼ am Anfang der Regex ist im Allgemeinen größer als der, in bestimmten Fällen nicht auf ˹\G˼ testen zu müssen (siehe Optimierungen mit Zeilen- und String-Ankern).)

Jedes Text-Element einzeln erkennen mit /gc

Normalerweise wird bei einem Fehlschlag von m/.../g die mit pos verbundene aktuelle Position des Suchstrings auf den Anfang zurückgesetzt. Genau das kann man mit dem /c-Modifikator unterbinden. (/c wird immer zusammen mit /g benutzt, ich spreche daher oft von /gc.) Das m/.../gc-Konstrukt wird zusammen mit ˹\G˼ oft benutzt, um einen Lexer oder Scanner zu implementieren, der einen String in Komponenten zerlegt. Hier sehen Sie ein einfaches Beispiel, das HTML-Code in der Stringvariablen $html einliest:

while (not $html =~ m/\G\z/gc) { # Solange wir noch nicht alles durchgearbeitet haben ...
 if ($html =~ m/\G( <[^>]+> )/xgc) { print "Tag: $1\n" }
 elsif ($html =~ m/\G( &\w+; )/xgc) { print "Namen-Entity: $1\n" }
 elsif ($html =~ m/\G( &\#\d+; )/xgc) { print "Numerisches Entity: $1\n" }
 elsif ($html =~ m/\G( [^<>&\n]+ )/xgc) { print "Text: $1\n" }
 elsif ($html =~ m/\G \n /xgc) { print "Newline\n" }
 elsif ($html =~ m/\G( . )/xgc) { print "Unzulässiges Zeichen: $1\n" }
 else {
   die "$0: Ähm: Das kann gar nicht passieren!";
 }
}

Der halbfett gedruckte Teil der einzelnen regulären Ausdrücke passt auf jeweils eine bestimmte HTML-Komponente. Auf jede wird ab der »aktuellen Position« geprüft (wegen des /gc), und zwar nur an dieser Position (wegen des ˹\G˼).

Die regulären Ausdrücke werden der Reihe nach an derselben Position angewendet, bis einer einen Treffer findet; dieser wird ausgegeben. Die pos-ition von $html wird dabei auf den Anfang der nächsten Komponente gesetzt, die bei der nächsten Iteration der while-Schleife geprüft wird.

Die Schleife wird abgebrochen, wenn m/\G\z/gc passt, wenn also die aktuelle Position (˹\G˼) mit dem Stringende (˹\z˼) zusammenfällt.

Es ist wichtig, dass bei jeder Iteration der Schleife einer der regulären Ausdrücke passt. Wäre das anders (und würden wir in diesem Fall nicht abbrechen), würde bei einer solchen Iteration die aktuelle Position nicht verschoben, und wir erhielten eine unendliche Schleife: pos würde immer am selben Ort stehen bleiben. In diesem Beispiel gibt es dafür eine else-Anweisung, die in diesem Fall wirklich nie erreicht wird. Aber wenn wir das Programm erweitern, kann es leicht sein, dass sich ein Fehler einschleicht, dann macht sich diese Vorsichtsmaßnahme bezahlt. Wenn bei der jetzigen Version eine Zeichensequenz auftritt, die wir nicht vorhergesehen haben (z.B. ›<>‹), wird pro Zeichen eine Warnmeldung ausgegeben.

Ein anderer wichtiger Aspekt ist die Reihenfolge der Tests – dass beispielsweise ˹\G(.)˼ zuletzt kommt. Wenn wir auch <script>-Blöcke erkennen wollen, könnten wir

$html =~ m/\G ( <script[^>]*>.*?</script> )/xgcsi

benutzen. (Wow! Fünf Modifikatoren!) Damit das richtig funktioniert, müssen wir diesen Test vor dem Test auf allgemeine Tags einsetzen, andernfalls würde das ˹<[^>]+>˼ zuoberst uns das öffnende <script>-Tag wegschnappen.

Im Kasten Ein Beispiel für den Gebrauch von \G in Perl unter Features und Dialekte gibt es ein etwas ausführlicheres Programm zum selben Thema.

Zusammenfassung: pos() und verwandte Bereiche

Diese Zusammenfassung zeigt, wie der Match-Operator mit der pos des Suchstrings zusammenhängt:

Matching Suche beginnt bei pos nach einem Treffer pos nach einem Fehlschlag
m/.../ Stringanfang (pos wird ignoriert) wird auf undef gesetzt wird auf undef gesetzt
m/.../g pos des Suchstrings wird auf das Ende des Treffers gesetzt wird auf undef gesetzt
m/.../gc pos des Suchstrings wird auf das Ende des Treffers gesetzt unverändert

Wenn der Suchstring in irgendeiner Art verändert wird, wird pos immer auf undef gesetzt (das entspricht dem Anfang des Strings).

  

<< 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