Montag, 31. Januar 2011

RegEx: Eine Einleitung finden

Manchmal möchte man von Texten nur eine Einleitung, einen Anreißer sichtbar machen und erst auf einer Tiefenseite den gesamten Text zeigen. Den Text nach einer festen Anzahl von Zeichen abzuschneiden ist wenig elegant, auch nach einem Punkt als Satzende zu suchen ist unsicher.
Abhilfe schaffen reguläre Ausdrücke. Eine mögliche Lösung wollen wir nicht einfach aufschreiben, sondern mit den wesentlichen Schritten herleiten. Neben einem kleinen Lerneffeckt kann man so den regulären Ausdruck leichter für ähnliche Fälle abwandeln.
Erst brauchen wir einen beliebigen Text*:
$MeinText = <<< IrgendeinMarker
Erect and sublime, for one moment of time.
 In the next, that wild figure they saw
(As if stung by a spasm) plunge into a chasm,
 While they waited and listened in awe.

"It's a Snark!" was the sound that first came to their ears.
 And seemed almost too good to be true.
Then followed a torrent of laughter and cheers:
 Then the ominous words "It's a Boo-"
IrgendeinMarker;
Die Einleitung soll eine gewisse Länge haben. Wir wollen also nicht unbedingt den ersten Punkt, der vielleicht schon nach zwei oder drei Worten kommt.
Wir suchen also (beispielsweise) mindestens 30 beliebige Zeichen, gefolgt von einem Punkt:
"." stellt ein solches beliebiges Zeichen dar und mit "{30,}" legen wir unsere gewählte Mindestanzahl fest. Eine zweite Zahl in der Klammer wäre eine Obergrenze, die wir aber hier nicht benötigen. Dann soll noch ein echter Punkt als Satzabschluss kommen. Der Punkt sieht so aus "\." - alles sehr intuitiv.
preg_match("/.{30,}\./", $MeinText, $Ergebnis);
 print_r($Ergebnis);
Hier sieht man beispielhaft wie "gefährlich" reguläre Ausdrücke sein können: Wir erhalten ein scheinbar korrektes Ergebnis, zumindest für unser konkretes Beispiel.

Der Zeilenumbruch als Fallstrick

Wir haben aber einen schweren Fehler:
Unser Ausdruck arbeitet zeilenweise jede Zeile für sich getrennt ab und fängt immer wieder am nächsten Zeilenanfang neu an - falls er nicht fündig wird. Eine zusammenhängende Einleitung kann man so nicht finden. Man kann dieses Problem besser nachvollziehen wenn man die Untergrenze auf {50,} setzt. In diesem Fall hätte man als Ergebnis nur die erste Zeile der zweiten Strophe - herausgerissen aus dem Text.
Mit dem nachgestellten Parameter "s" teilen wir dem regulären Ausdruck deshalb mit, dass wir die Zeilen nicht isoliert, sondern im Gegenteil den ganzen Text als Einheit sehen wollen.
preg_match("/.{50,}\./s", $MeinText, $Ergebnis);
print_r($Ergebnis);
So sieht das schon besser aus, wir erhalten aber damit fast den gesamten Text bis zum letzten Punkt.

Gier ist niemals gut

Grundsätzlich sind reguläre Ausdrucke "gierig": Wenn keine feste Angabe gemacht wird, nehmen sie immer so viel wie irgend möglich. Mit dem "?" schränken wir bei einer Mengenangabe wie "{50,}" diese Gier ein und der erstmögliche Treffer, bzw. hier Punkt wird genommen.
preg_match("/.{50,}?\./s", $MeinText, $Ergebnis);
print_r($Ergebnis);
Hinweis: "?" hat eine andere Bedeutung haben wenn es nicht wie hier zu einer Mengenangabe gestellt ist. Das gehört zu den Dingen, die reguläre Ausdrücke manchmal etwas verwirrend machen! Böse Zungen behaupten, dass sei der Herkunft von Perl geschuldet.

Alternative Satzenden

Nun wollen wir nicht nur den Punkt als Satzende erkennen, sondern auch das "!" und "?". Dafür geben wir an der Stelle von "\." mögliche Alternativen in der Form "[]" an, konkret "[\.\?!]". Wir wollen auch verhindern, dass ein Punkt in einer Zahl in der Form 10.000 oder eine Datumsangabe gefunden wird. Eine einfache Möglichkeit ist es darauf zu vertrauen, dass nach einem echten Punkt ein Leerzeichen oder Zeilenumbruch folgt - vereinfacht angegeben als "\s".
preg_match("/.{50,}?[\.\?\!]\s/s", $MeinText, $Ergebnis);
print_r($Ergebnis);
Um uns gegen alle Eventualitäten abzusichern, geben wir zuletzt noch mit "^" an, dass der reguläre Ausdruck auch immer vom Textanfang beginnend gefunden werden muss. Geben wir nämlich beispielsweise mit (50,250) auch eine Obergrenze an, kann es bereits passieren, dass das Ergebnis einfach irgendwo in der Textmitte gefunden wird - nicht ganz passend für einen Anriss.
preg_match("/^.{50,}?[\.\?\!]\s/is", $MeinText, $Ergebnis);
print_r($Ergebnis);

Und was noch?

Es gibt keinen perfekten regulären Ausdruck. Ein passendes Suchmuster kann unter geänderten Bedingungen versagen und unerwartete Ergebnisse liefern. Bei jeder Suche geht man von bestimmten Voraussetzungen aus und muss auch darauf vorbereitet sein, dass nicht in allen Fällen ein Ergebnis gefunden werden kann - gerade bei Texten sind Abweichungen oder Fehler keine Seltenheit.
Nicht zu vergessen sind die drei Gesetze der regulären Ausdrücke:
1. Du hast immer einen Fehler in Deinem Ausdruck, also teste alles noch einmal
2. Es gibt immer einen besseren regulären Ausdruck
3. Es gibt immer jemanden anderen der lautstark 1. und 2. verkündet ;-)
*"The Hunting of the Snark" von Lewis Carroll.

Keine Kommentare:

Kommentar veröffentlichen