Methoden zum Bau von Scannern

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

In Java 1.5 gibt es die neuen Methoden hitEnd und requireEnd, die besonders für den Aufbau von Scannern ausgelegt sind.

Ein Scanner ist ein Programmteil, der einen Zeichenstrom sequenziell liest und in Symbole (oft Token genannt) zerlegt. Zum Beispiel liest der Scanner in einem Compiler den Text ›var<34‹ und erzeugt daraus die drei Token IDENTIFIER, LESS_THAN und INTEGER.

Diese Methoden werden in einem Scanner dazu verwendet, um festzustellen, ob die Resultate des eben abgeschlossenen Matching-Versuchs interpretiert werden sollen oder ob weitergesucht werden soll. Im Allgemeinen bedeutet der Rückgabewert true bei diesen Methoden, dass weitere Zeichen vom Eingabetext gelesen werden müssen und erst dann endgültig darüber entschieden werden kann. Wenn es sich beispielsweise um Eingaben des Benutzers in einem interaktiven Debugger handelt und eben ein < gelesen wurde, muss weitergelesen und überprüft werden, ob das nächste Zeichen ein = ist, erst dann kann entschieden werden, ob das erzeugte Symbol ein LESS_THAN oder ein LESS_THAN_OR_EQUAL sein wird.

In den allermeisten Anwendungsfällen von regulären Ausdrücken wird man diese Methoden kaum benutzen; in einigen wenigen Situationen sind sie dafür unentbehrlich. Umso schlimmer ist es, dass die Methode hitEnd einen Fehler hat, der sie in Java 1.5 nachgerade unbrauchbar macht. In Java 1.6 ist der Fehler behoben, und wir werden einen einfachen Weg sehen, wie man das Problem auch in Java 1.5 umgehen kann.

Die ausführliche Besprechung des Baus von Scannern, Parsern und dergleichen würde den Rahmen dieses Buches sprengen, ich beschränke mich deshalb bei der Besprechung dieser Methoden auf deren Definition und ein paar Beispiele. (Wenn Sie nur an einem Scanner interessiert sind, ist vielleicht die Klasse java.util.Scanner ohnehin die richtige Wahl.)

boolean hitEnd()

(Diese Methode ist in Java 1.5 unzuverlässig; eine Umgehungsmöglichkeit finden Sie weiter unten bei Der hitEnd-Bug und wie man ihn umschifft.) Diese Methode zeigt an, ob die Regex-Maschine beim letzten Matching-Versuch versucht hat, Zeichen über das gegenwärtige Eingabe-Ende hinaus zu lesen; unabhängig davon, ob dieser Versuch erfolgreich war oder nicht. Das umfasst auch die Untersuchung von Grenzen und Ankern wie bei ˹\b˼ und ˹$˼.

Wenn hitEnd den Wert wahr zurückgibt, bedeutet das, dass weitere Eingaben das Resultat noch verändern könnten (hinsichtlich Fehlschlag oder Treffer, oder auch nur die Länge des Treffers). Dagegen bedeutet falsch, dass das abschließende Resultat des letzten Matching-Versuchs nur die bisher gelesenen Zeichen verwendet hat und dass weitere Zeichen im Eingabestrom dieses Resultat nicht mehr umstoßen können.

Die übliche Anwendung sieht so aus: Solange hitEnd nach einem erfolgreichen Treffer den Wert wahr zurückgibt, muss auf weitere Eingaben gewartet werden. Wenn aber hitEnd nach einem Fehlschlag wahr zurückgibt, ist dies kein endgültiger Fehler, sondern es müssen weitere Zeichen von der Eingabe gelesen werden.

boolean requireEnd()

Diese Methode ist nur nach einem erfolgreichen Matching sinnvoll. Sie besagt dann, dass das Ende der Eingabe an diesem erfolgreichen Treffer beteiligt war. Anders gesagt: Wenn requireEnd den Wert wahr zurückgibt, könnte es zwar sein, dass weitere Zeichen die Details des Treffers noch verändern, nicht aber die Tatsache, dass es überhaupt einen Treffer gibt.

Bei der vorgesehenen Anwendung soll also weiterhin von der Eingabe gelesen werden, solange requireEnd den Wert wahr zurückgibt, und erst danach soll der Treffer ausgewertet werden.

Sowohl hitEnd als auch requireEnd berücksichtigen die Region.

Beispiele mit hitEnd und requireEnd

In der folgenden Tabelle sind Beispiele zum Gebrauch von hitEnd und requireEnd nach einer Suche mit lookingAt dargestellt. Die zwei regulären Ausdrücke sind unrealistisch einfach, aber man kann mit ihnen das Prinzip veranschaulichen.

Die Regex in der oberen Hälfte der Tabelle sucht nach einer natürlichen Zahl, gefolgt von einem der vier Vergleichsoperatoren »größer«, »kleiner«, »größer-gleich« oder »kleiner-gleich«. Die untere Hälfte ist noch einfacher, es wird lediglich nach den Wörtern set oder setup gesucht. Wie gesagt, die Beispiele sind überaus einfach, aber sie illustrieren die Konzepte.

Tabelle: hitEnd und requireEnd nach einer Suche mit lookingAt.

Regex Text Treffer hitEnd() requireEnd()
1 \d+\b|[><]=? ‘1234’ 1234’ wahr wahr
2 \d+\b|[><]=? ‘1234>567’ 1234>567’ falsch falsch
3 \d+\b|[><]=? ‘>’ > wahr falsch
4 \d+\b|[><]=? ‘>567’ >567’ falsch falsch
5 \d+\b|[><]=? ‘>=’ >= falsch falsch
6 \d+\b|[><]=? ‘>=567’ >=567’ falsch falsch
7 \d+\b|[><]=? ‘oops’ kein Treffer falsch

8 (set|setup)\b ‘se’ kein Treffer wahr
9 (set|setup)\b ‘set’ set wahr wahr
10 (set|setup)\b ‘setu’ kein Treffer wahr
11 (set|setup)\b ‘setup’ setup wahr wahr
12 (set|setup)\b ‘setx=3’ setx=3’ falsch falsch
13 (set|setup)\b ‘setupx’ setupx’ falsch falsch
14 (set|setup)\b ‘self’ kein Treffer falsch
15 (set|setup)\b ‘oops’ kein Treffer falsch

In Zeile 5 zum Beispiel wird zwar ein Treffer gefunden, aber hitEnd ist dennoch falsch. Das liegt daran, dass die Regex-Maschine zwar das letzte Zeichen im Text evaluiert hat (es gehört ja zum Treffer), aber nichts darüber hinaus, etwa ein weiteres Zeichen oder eine Wortgrenze.

Der hitEnd-Bug und wie man ihn umschifft

In Java 1.5 gab es bezüglich hitEnd einen sehr speziellen Fehler, der in Java 1.6 (Anmerkung: Und ab Update 9 zu Java 1.5; das Buch beschreibt im Allgemeinen die Regex-Implementation von Java 1.5 Update 7 (siehe In diesem Kapitel).) behoben ist. Der Fehler tritt nur dann auf, wenn eine optionale, ein Zeichen lange Komponente in einer Regex auftritt, wenn Groß- und Kleinschreibung nicht beachtet werden und wenn ein Versuch mit dieser optionalen Komponente fehlschlägt.

Der Fehler tritt beispielsweise bei ˹>=?˼ auf (als Teil einer Regex oder auch als ganze Regex), wenn Groß- und Kleinschreibung ignoriert wird, denn das = ist ein einzelnes Zeichen und durch das Fragezeichen ist es optional. Der Bug tritt auch bei ˹O|Oh|Huch˼ auf (wiederum nur bei ignorierter Groß- und Kleinschreibung, als ganze Regex oder als Teil davon), denn die erste Alternative ˹O˼ ist ein einzelnes Zeichen, und da es andere Alternativen gibt, ist es optional.

Andere Beispiele sind ˹Werte?˼ und ˹\r?\n\r?\n˼.

Wie man den Fehler umgeht

Man vermeidet den Fehler, indem man die Bedingungen für sein Auftreten aufhebt. Entweder man verzichtet auf die Äquivalenz von Groß- und Kleinbuchstaben (für die ganze Regex oder auch nur für Teile davon) oder man ersetzt das einzelne Zeichen durch etwas anderes, beispielsweise durch eine Zeichenklasse, die nur ein einziges Zeichen enthält.

Mit dem ersten Ansatz würde ˹>=?˼ zu ˹(?-i:>=?)˼, der Modus »Groß- und Kleinschreibung ignorieren« wird dabei für einen Bereich der Regex abgeschaltet (siehe Der Modus »Groß- und Kleinschreibung ignorieren«). Hier ist das ohne Weiteres möglich, weil es ja zum Zeichen = keine Groß- oder Kleinbuchstaben-Version gibt.

Mit dem zweiten Ansatz würde ˹O|Oh|Huch˼ zu ˹[oO]|Oh|Huch˼, auch hier bleibt die Forderung nach einer Gleichbehandlung von Groß- und Kleinbuchstaben bestehen.

  

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