Die Region des Matchers

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

Seit Java 1.5 kann man dem Matcher eine Region im Suchtext vorgeben, auf die die Mustersuche beschränkt bleiben soll. Normalerweise umfasst diese Region den ganzen Suchtext, sie kann aber mit der region-Methode verändert werden.

Im nächsten Programmbeispiel wird ein String untersucht, der HTML enthält, und IMG-Tags ohne ALT-Attribute werden ausgegeben. Es werden zwei Matcher mit unterschiedlichen regulären Ausdrücken verwendet, die auf den gleichen Suchtext (den HTML-Text) angewendet werden. Der eine Matcher findet IMG-Tags, der andere findet ALT-Attribute.

Die zwei Matcher befassen sich zwar mit dem gleichen Suchtext, es sind aber dennoch zwei völlig unabhängige Objekte, die nur miteinander verbunden sind, weil wir die Suche nach ALT-Attributen explizit auf die IMG-Tags begrenzen. Dazu geben wir nach einem erfolgreichen IMG-Matching mit den start- und end-Methoden dem ALT-Matcher eine explizite Region vor, an die sich die danach aufgerufene find-Methode hält.

Wenn wir das komplette IMG-Tag auf diese Weise isolieren, sagt uns die Suche nach einem ALT-Tag in der Tat, ob ein ALT-Tag innerhalb dieses IMG-Tags vorkommt und nicht einfach irgendwo sonst im gesamten HTML-String.

// Matcher für ein IMG-Tag. Die Variable 'html' enthält den zu untersuchenden HTML-Code.
Matcher mImg = Pattern.compile("(?id)<IMG\\s+(.*?)/?>").matcher(html);

// Matcher für ein ALT-Attribut (wird auf ein IMG-Tag innerhalb der 'html'-Variable angesetzt).
Matcher mAlt = Pattern.compile("(?ix)\\b ALT \\s* =").matcher(html);

// Für alle IMG-Tags im HTML-Code ...
while (mImg.find()) {
   // Region für die ALT-Suche auf den eben gefundenen Treffer beschränken.
   mAlt.region( mImg.start(1), mImg.end(1) );

   // Komplettes IMG-Tag ausgeben, wenn darin kein ALT-Attribut gefunden wurde.
   if (! mAlt.find())
       System.out.println("Kein ALT-Attribut in: " + mImg.group());
}

Es sieht vielleicht etwas seltsam aus, wenn der Suchtext an einer Stelle (beim Erzeugen des mAlt-Matchers) angegeben wird und die dazugehörige Region woanders (beim Aufruf von mAlt.region). Man kann stattdessen das mAlt-Objekt zunächst mit einem leeren String erzeugen und den eigentlichen HTML-Text bei der Angabe der Region nachreichen: mAlt.reset(html).region(...). Der zusätzliche Aufruf von reset ist etwas ineffizient, aber der Code wird dadurch leichter verständlich.

Hätten wir die Suchregion für den ALT-Matcher nicht beschränkt, wäre der ganze HTML-String abgesucht worden. Wenn irgendwo darin der String ›ALT=‹ vorkäme, würden die IMG-Tags ohne ALT-Attribut nicht gefunden.

Erweitern wir dieses Programm, so dass es zu den beanstandeten IMG-Tags auch deren Zeilennummer ausgibt. Wir definieren dazu eine Region, die vom Anfang des HTML-Strings bis zum Anfang des Treffers reicht, und zählen die in dieser Region vorkommenden Newlines. Der neue Code ist in den hervorgehobenen Zeilen:

// Matcher für ein IMG-Tag. Die Variable 'html' enthält den zu untersuchenden HTML-Code.
Matcher mImg = Pattern.compile("(?id)<IMG\\s+(.*?)/?>").matcher(html);

// Matcher für ein ALT-Attribut (wird auf ein IMG-Tag innerhalb der 'html'-Variable angesetzt).
Matcher mAlt = Pattern.compile("(?ix)\\b ALT \\s* =").matcher(html);

// Matcher für ein Newline.
Matcher mLine = Pattern.compile("\\n").matcher(html);

// Für alle IMG-Tags im HTML-Code ...
while (mImg.find())
{
   // Region für die ALT-Suche auf den eben gefundenen Treffer beschränken.
   mAlt.region( mImg.start(1), mImg.end(1) );
   // Komplettes IMG-Tag ausgeben, wenn darin kein ALT-Attribut gefunden wurde.
   if (! mAlt.find()) {
      // Die Region zum Zählen von Newlines reicht bis zum Anfang des IMG-Tags.
      mLine.region(0, mImg.start());

      int lineNum = 1;     // Erste Zeile ist Nr. 1.
      while (mLine.find())
              lineNum++;   // Hinaufzählen.
      System.out.println("Fehlendes ALT-Attribut auf Zeile " + lineNum);
   }
}

Wie vorhin setzen wir mit start(1) die Region für den ALT-Matcher und bestimmen dadurch, wo innerhalb des HTML-Strings das gerade gefundene IMG-Tag beginnt. Umgekehrt verwenden wir zum Aufsetzen der Region für den Newline-Matcher start(), denn das ist die Position im String, an der das komplette IMG-Tag beginnt (denn bis dahin wollen wir die Newlines zählen).

Merkpunkte

Es ist wichtig, sich darüber Klarheit zu verschaffen, dass manche Suchfunktionen die gewählte Region nicht nur komplett ignorieren, sondern dass sie sie mittels reset auf den gesamten Suchtext zurücksetzen.

  • Suchfunktionen, die die Region berücksichtigen:

    matches
    lookingAt
    find()
    (nur die Version ohne Argumente)

  • Methoden, die den Matcher und damit die Region zurücksetzen:

    find(text) (die Version mit einem Argument)
    replaceAll
    replaceFirst
    reset
    (trivialerweise)

Merken Sie sich außerdem, dass sich die Offsets der Match-Resultate (also die Werte, die die start- und end-Methoden zurückgeben) immer auf den Anfang des gesamten Suchstrings beziehen und nicht auf den Anfang der gewählten Region.

Setzen und Abfragen der Regionsgrenzen

Der Matcher besitzt drei Methoden, die sich mit dem Setzen und der Abfrage der Grenzen der Region befassen:

Matcher region(int start, int ende)

Diese Methode definiert die Region und setzt deren Anfang und Ende auf die Zeichen bei den Positionen start und ende; es handelt sich dabei um Offsets bezüglich des Anfangs des Suchstrings. Der ganze Matcher wird dabei zurückgesetzt, insbesondere wird der Match-Zeiger auf den Anfang der Region gesetzt, so dass die nächste Suche mit find dort beginnt.

Die gewählte Region bleibt bis zum nächsten Aufruf von reset erhalten; das kann explizit geschehen oder durch eine der Methoden, die intern reset verwenden (siehe unter Weitere Matcher-Methoden).

Die Methode gibt das Matcher-Objekt selbst zurück und kann deswegen mit anderen Methoden verkettet werden (siehe Verketten von Methoden).

Wenn für start oder ende Offsets außerhalb des Suchtexts angegeben werden oder wenn start größer als ende ist, wird die Ausnahme IndexOutOfBoundsException ausgelöst.

int regionStart()

Gibt den Offset in Zeichen bis zum Anfang der gewählten Region zurück oder als Voreinstellung den Wert 0.

int regionEnd()

Gibt den Offset in Zeichen bis zum Ende der gewählten Region zurück. Voreingestellt ist die Länge des Suchtexts.

Die der region-Methode ist etwas unbequem, wenn man nur eine Grenze der Region verändern will, weil die Methode sowohl die Angabe von start als auch von ende erfordert. In der folgenden Tabelle wird gezeigt, wie man das macht.

Tabelle: Setzen von nur einer Regionsgrenze.

Anfang der Region Ende der Region Java-Code
wird explizit gesetzt unverändert m.region(start, m.regionEnd());
unverändert wird explizit gesetzt m.region(m.regionStart(), end);
wird explizit gesetzt wird auf Voreinstellung gesetzt m.reset().region(start, m.regionEnd());
wird auf Voreinstellung gesetzt wird explizit gesetzt m.region(0, end);

Über die Region hinausblicken

Wenn man die Region auf einen anderen Bereich als den ganzen Suchstring setzt, bleibt der Text außerhalb der Region der Regex-Maschine in jeder Beziehung verborgen. Beispielsweise passt ˹^˼ nun auf den Anfang der Region, auch wenn dieser nicht mit dem Anfang des Suchtexts übereinstimmt.

Es ist allerdings möglich, den Rest des Suchstrings für bestimmte Funktionen sichtbar zu machen. Wenn man die Option transparent bounds benutzt und anchoring bounds abschaltet, werden Anfang und Ende der gewählten Region für die »Look«-Konstrukte (Lookahead, Lookbehind und Wortgrenzen) sozusagen durchsichtig.

Wozu werden diese Optionen eingesetzt? Das hängt eng mit dem Grund zusammen, weshalb man Regionen überhaupt verwendet. Bei den bisherigen Beispielen war dafür kein Bedarf, weil wir in keinem dieser Beispiele Anker oder Lookahead-Konstrukte benutzt haben.

Nehmen wir an, ein CharBuffer-Objekt enthält den Text, der in einer Anwendung editiert wird. Wenn der Benutzer eine Suchen-und-Ersetzen-Funktion auslöst, ist es nur normal, dass diese Operation an der Position des Cursors beginnt und nicht am Anfang des gesamten Texts; dafür bietet sich eine Region von der Position des Cursors bis zum Ende des Texts an. Nehmen wir an, der Cursor befindet sich an der bezeichneten Stelle:

Madagas▵car is much too large to see on foot, so you'll need a car.

Dabei sollen alle Treffer für ˹\bcar\b˼ gegen »automobile« ausgetauscht werden. Die Region wird auf den Text rechts vom Cursor eingeschränkt und die Suche wird gestartet. Es erstaunt vielleicht, dass gleich zu Beginn der Region ein Treffer gefunden wird, bei ›Madagascar‹. Die Regex passt dort, weil die Option transparent bounds ausgeschaltet ist und das ˹\b˼ in der Tat auch auf den Anfang des Suchbereichs (in diesem Fall der Region) passt. Wenn dagegen transparent bounds eingeschaltet ist, wäre das ›s‹ vor der gewählten Region und dem Zeichen ›c‹ sichtbar und die Wortgrenze ˹\b˼ würde nicht mehr passen.

Durchsichtige Regionsgrenzen

Die folgenden Methoden beziehen sich auf die transparent bounds:

Matcher useTransparentBounds(boolean b)

Schaltet die Matcher-Option transparent bounds ein oder aus. Die Voreinstellung ist abgeschaltet oder false.

Die Methode gibt das Matcher-Objekt selbst zurück und kann deswegen mit anderen Methoden verkettet werden (siehe Verketten von Methoden).

boolean hasTransparentBounds()

Gibt den Wert der Option transparent bounds zurück.

Die Option »durchsichtige Grenzen« ist normalerweise ausgeschaltet, die Grenzen der Region sind also im Normalfall für die »Look«-Konstrukte nicht durchsichtig. Zeichen vor und nach der Region sind für die Regex-Maschine unsichtbar und werden in jeder Beziehung ignoriert. (Anmerkung: In Java 1.5 Update 7 gibt es einen kleinen Bug, den ich Sun gemeldet habe. Die Regex ˹^˼ mit eingeschaltetem Pattern.MULTILINE passt auf den Anfang der Region, wenn direkt davor ein Zeilenendezeichen steht, auch wenn »Transparente Grenzen« und »Anker-Grenzen« beide ausgeschaltet sind. (Der Bug ist auch in Java 1.6 Update 1 noch vorhanden – Anm. d. Ü.)) Wenn also der Anfang der Region mitten in ein Wort fällt, kann ˹\b˼ dennoch auf den Anfang der Region passen – die Regex-Maschine sieht nicht, dass davor andere Wortzeichen stehen.

Das folgende Beispiel zeigt das Verhalten bei ausgeschalteter transparent bounds-Option, der Voreinstellung:

String regex = "\\bcar\\b"; // \bcar\b
String text  = "Madagascar is best seen by car or bike.";
Matcher m = Pattern.compile(regex).matcher(text);
m.region(7, text.length());
m.find();
System.out.println("Treffer beginnend bei " + m.start());

Das ergibt:

Treffer beginnend bei 7

Also hat die Wortgrenze tatsächlich auf den Anfang der Region gepasst, mitten im Wort Madagas▵car, wo natürlich keine wirkliche Wortgrenze ist. Die undurchsichtige Regionsgrenze hat uns eine Wortgrenze vorgegaukelt.

Wenn wir die Option vor dem Aufruf von find einschalten:

m.useTransparentBounds(true);

ergibt sich:

Treffer beginnend bei 27

Weil die Regionsgrenzen nun durchsichtig sind, erkennt die Regex-Maschine, dass das Zeichen direkt vor dem Anfang der Region ein Buchstabe ist (ein ›s‹) und dass ˹\b˼ an dieser Stelle nicht passt, weil davor und danach ein Wortzeichen kommt. Der Treffer wird erst weiter hinten in der Region gefunden, bei ›...bycarorbike‹.

Die Option transparent bounds spielt nur dann eine Rolle, wenn die Suchstring- und Regionsgrenzen nicht zusammenfallen. Beachten Sie, dass die reset-Methode diese Option nicht zurücksetzt.

Anker-Grenzen (anchoring bounds)

Die folgenden Methoden befassen sich mit der »Anker Grenzen«-Option:

Matcher useAnchoringBounds(boolean b)

Schaltet die Option anchoring bounds ein oder aus. Die Voreinstellung ist ein oder true.

Die Methode gibt das Matcher-Objekt selbst zurück und kann deswegen mit anderen Methoden verkettet werden (siehe Verketten von Methoden).

boolean hasAnchoringBounds()

Gibt den Wert der Option anchoring bounds zurück.

Normalerweise ist die Matcher-Option »Anker-Grenzen« eingeschaltet, das bedeutet, dass die Zeilenanker (^ \A $ \z \Z) auf die Grenzen der Region passen, auch wenn sie nicht mit Anfang und Ende des Suchstrings übereinstimmen. Wenn die Option auf false gesetzt wird, passen diese Anker nur noch auf Anfang und Ende des gesamten Suchtexts und können daher nur noch passen, wenn diese mit dem Anfang oder dem Ende der gewählten Region übereinstimmen.

Man wird oft die Option »Anker-Grenzen« aus dem gleichen Grund abschalten, wie man die Option »durchsichtige Grenzen« einschaltet, damit nämlich das Bild »der Cursor steht nicht am Anfang des Texts« erhalten bleibt.

Wie die Option »durchsichtige Grenzen« hat auch die Option »Anker-Grenzen« nur eine Bedeutung, wenn die gewählte Region vom voreingestellten Wert (gesamter Suchtext) abweicht. Beachten Sie, dass die reset-Methode diese Option nicht zurücksetzt

  

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