von WoNáDo
Nun der letzte Teil der Sequenz, der sich noch mit den Möglichkeiten von Ruby 1.8 beschäftigt.
Wer mochte nicht schon mal wissen, was die Zukunft bringt. Wenn man kurz mal einen Blick in die Zukunft werfen könnte, wären manche Entscheidungen viel einfacher – oft würden sie sich sogar erübrigen.
Nun hat die Anwendung regulärer Ausdrücke auf Texte – also das Pattern-Matching – nicht wirklich etwas mit dem Blick in die Zukunft zu tun. Für die den Musterabgleich versuchende Mustermaschine gibt es allerdings eine Vergangenheit, nämlich all das, was sie bisher erkundet hat, und eine Zukunft, das, was sie noch erkunden soll. Es können also durchaus Situationen auftreten, in denen es sehr sinnvoll wäre, wenn man so etwas wie ‘schau mal kurz nach, ob … noch kommt / nicht kommt’ formulieren könnte. Für diesen Zweck gibt es die Musterelemente /(?=…)/ (Positive Look Ahead) und /(?!…)/ (Negative Look Ahead). … bezeichnet hier wieder irgendwelche Unterausdrücke.
Die positive Vorausschau /(?=…)/ macht nichts anderes, als ab der aktuellen Matching-Position zu versuchen, das Untermuster zu erkennen. Klappt das, macht die Maschine mit dem nächsten Musterelement weiter, jedoch ohne (wie sonst) die Position für das Matching zu verändern – sie hat eben nur mal kurz einen Blick in die Zukunft gewagt.
Nehmen wir mal ein Beispiel, welches sogar sehr realistisch ist. In Klammern steht eine Liste von Bezeichnungen:
text = '(a,b,c,d)'
Diese Liste möchte ich verändern, so dass an jedes Element .copy angehängt wird. Ich möchte also letztendlich folgendes Ergebnis:
neutext = '(a.copy,b.copy,c.copy,d.copy)'
Na fangen wir mal einfach an. Zuerst muss ich natürlich nach der linken Begrenzung eines Bezeichners suchen, also ( oder ,, eventuell gefolgt von Leerzeichen, dann nach dem Bezeichner und am Ende noch nach der rechten Begrenzung, also eventuellen Leerzeichen, gefolgt von , oder ). Da ich mit den Teilen ja etwas machen will, packe ich sie gleich mal in einfangende Klammern. Der naheliegende Versuch sieht nun so aus:
pattern = /([(,]) *(\w+) *([,)])/
Als Test schaue ich mir einfach mal einen Matchversuch an:
md = text.match(pattern) puts "'#{text}'.match(/#{pattern.source}/): #{md.pre_match}<#{md[0]}>#{md.post_match}"
Das Ergebnis sieht auf den ersten Blick zufriedenstellend aus. Die Klammer wird erkannt, der Bezeichner auch und das Komma am Ende macht auch keine Probleme. Leerzeichen kommen im Beispiel nicht vor.
'(a,b,c,d)'.match(/([(,]) *(\w+) *([,)])/): <(a,>b,c,d)
So, also ran an die Ersetzung. Die Leerzeichen lasse ich weg, sie wurden ja auch nicht konserviert. Also muss erst der gefundene linke Begrenzer rein, dann der Bezeichner, gefolgt von .copy und letztendlich der rechte Begrenzer. Das sieht dann so aus.
ersatz = '\1\2.copy\3'
Mutig ran an den ersten Test mit:
neutext = text.gsub(pattern, ersatz) puts "'#{text}'.gsub(/#{pattern.source}/, #{ersatz}): #{neutext}"
Zu guter letzt sollte das Ergebnis nun noch stimmen:
'(a,b,c,d)'.gsub(/([(,]) *(\w+) *([,)])/, \1\2.copy\3): (a.copy,b,c.copy,d)
Hmmmm – das ist aber nicht so ganz das, was ich mir vorgestellt habe. Zwar sind der erste und der dritte Bezeichner erfolgreich verändert worden, jedoch der zweite und vierte nicht. Wie kommt das denn nun zustande?
Analysieren wir mal ein bisschen das bisher geschehene. Da der erste Bezeichner erfolgreich ersetzt wurde, muss also der linke Begrenzer, der Bezeichner und der rechte Begrenzer erfolgreich erkannt worden sein. Im nächsten Versuch erkennt die Mustermaschine nun den linken Begrenzer – aber, der wurde doch schon beim ersten Versuch erkannt und ist damit schon konsumiert. Die Mustermaschine setzt im zweiten Versuch erst hinter dem rechten Begrenzer des ersten Bezeichners wieder auf!
Dann ist das Verhalten aber völlig klar. Im zweiten Durchlauf erkennt die Maschine erst wieder das nächste Komma, damit also ,c,, ersetzt das korrekt zu ,c.copy,, hat nun aber das Komma hinter c schon wieder konsumiert findet dementsprechend kein weiteres Vorkommen des Musters.
Was nun, habe ich etwa keine Chance, meinen Wunsch mit einem Muster zu erfüllen? – Doch, es geht: Das Musterelement Positive Vorausschau (Positive Look Ahead) hilft hier weiter!
Gehen wir noch mal ganz zum Anfang zurück und schauen auf das Ende der Musterbeschreibung: “also eventuellen Leerzeichen, gefolgt von , oder )”. Wenn ich nun diese Beschreibung abändere in “also eventuellen Leerzeichen, gefolgt von , oder ), die vorausschauend gesucht werden”, erhalte ich durch das Musterelement /(?=[,)])/ genau den erwünschten Effekt. Mein Muster sieht nun anders aus:
pattern = /([(,]) *(\w+) *(?=[,)])/
Ein kurzer Test um festzustellen, was jetzt im ersten Versuch erkennend konsumiert wird:
md = text.match(pattern) puts "'#{text}'.match(/#{pattern.source}/): #{md.pre_match}<#{md[0]}>#{md.post_match}"
Das Ergebnis sieht nun sehr schön aus:
(a,b,c,d)'.match(/([(,]) *(\w+) *(?=[,)])/): <(a>,b,c,d)
Das Komma nach dem ersten Bezeichner ist nicht mehr Bestandteil des ersten Match-Versuchs, es steht also für nachfolgende zur Verfügung. Ich brauche es daher auch nicht beim Ersatztext zu berücksichtigen.
ersatz = '\1\2.copy'
Ein letzter Versuch sollte nun das gewünschte Ergebnis bringen:
neutext = text.gsub(pattern, ersatz) puts "'#{text}'.gsub(/#{pattern.source}/, #{ersatz}): #{neutext}"
In der Tat – das wollte ich haben:
'(a,b,c,d)'.gsub(/([(,]) *(\w+) *(?=[,)])/, \1\2.copy): (a.copy,b.copy,c.copy,d.copy)
Das zweite Musterelement aus der Vorausschau-Familie ist die negative Vorausschau (Negative Look Ahead). Sie gestattet es mir zu formulieren, dass ich etwas nicht möchte, was im zukünftigen Teil der Analyse eventuell auftaucht. Im Alltag benutzt man manchmal einen ähnlichen Mechanismus beim Lesen, wenn man mitten im Buch ein Lesezeichen hineinlegt, auf den letzten Seiten nachschaut, ob es sehr schnulzig wird und nur dann ab dem Lesezeichen weiterliest, wenn das nicht der Fall ist.
Nehmen wir ein Beispiel aus dem Leben. Andauernd erhält man Mails – viele mit Inhalt, meist noch mehr Spam. Ich möchte jetzt unbedingt wissen, wo Feten stattfinden, dabei aber die Spam-Mails nicht berücksichtigen. Zuerst hole ich mir alle Subject-Zeilen aus den Headern in eine Datei, weil ich nur die ansehen will. Hierzu packe ich einen Auszug für Tests in einen String:
text = <<SUBJECTS subject: kaufen sie den unsinnigsten kram massenweise subject: re: morgen abend um 18:00 pizzeria al dente subject: re: re: kaufen sie kiloweise tabletten subject: montag gibts bei gerda eine geburtstagsfeier subject: re: borgen sie mir mal schnell 1000000 euro subject: borgen sie mir morgen ihr geld fuer eine fete subject: wir machen nachher eine fete bei heinz SUBJECTS
Nun will ich erst mal die Suche nach Festen formuliereren. Gruppierungen mache ich nur mit /(?:…)/, da ich keine Zuweisungen an Match-Variablen möchte – die werden nicht gebraucht und kosten nur Zeit.
pattern = /^ *subject:(?: *re:)*.*(?:feier|fete|fest).*$/
Also, ich suche nach eventuellen Leerzeichen am Zeilenanfang nach subject: gefolgt von beliebig vielen re:, wobei auch hier wieder Leerzeichen erlaubt sind. Anschließend sollen in der Zeile noch feier, fete oder fest vorkommen, sonst interessiert sie mich nicht. Ein Versuch:
text.scan(pattern) do |match| puts match end;
Gibt mit gefundene Feste aus.
subject: montag gibts bei gerda eine geburtstagsfeier subject: borgen sie mir morgen ihr geld fuer eine fete subject: wir machen nachher eine fete bei heinz
Ohhh – leider habe ich auch eine Spam-Mail erwischt. ‘borgen sie mir ihr geld fuer eine Fete’ gehört nicht zu den Sachen, die ich gesucht habe.
Was kann ich tun? – Das Musterelement der negativen Vorausschau einsetzen!
Dafür setze ich einfach vor den Feten-Suchmuster-Teil einen Kaufen-Borgen-Verweigerungsteil mit /(?!.*(?:kaufen|borgen))/. Sollte die Mustermaschine irgendwo im Rest kaufen oder borgen finden, so versucht sie nicht den Rest gar nicht mehr. Ansonsten macht sie an der gleichen Stelle weiter. Das umformulierte Muster lautet nun:
pattern = /^ *subject:(?: *re:)*(?!.*(?:kaufen|borgen)).*(?:feier|fete|fest).*$/
Ein kurzer Test:
text.scan(pattern) do |match| puts match end
bringt jetzt die gewünschte Feten-Liste ohne Spam-Anteile:
subject: montag gibts bei gerda eine geburtstagsfeier subject: wir machen nachher eine fete bei heinz
Die negative und die positive Vorausschau wirkt so, als ob ab der aktuellen Position eine Art fork gemacht wird (“aufgabeln”, also einen Prozess in zwei parallel laufende Prozesse aufspalten). Der Hauptteil wartet so lange, bis der Kind-Mustererkennungsprozess eine Rückmeldung gibt.
Das kommt in einem weiteren Beitrag, der sich mit Look Behind und Recursive Patterns befasst. Da diese erst ab Ruby 1.9 mit der Oniguruma-Mustermaschine laufen, haben sie hier nichts mehr zu suchen.
Alles, was hier und in den ersten beiden Beiträgen an Beispielen steht, geht mit Ruby 1.8. Genauer gesagt, habe ich es mit der OneClickInstaller-Version 182-15 getestet.
Kommentar schreiben
Kommentare
einen ganzen monat hat das gedauert! alles meine schuld :/ tut mir leid. hier ist jedenfalls endlich der nächste teil von WoNáDos' berüchtigtem Glasperlenspiel :)
Und wieder vielen Dank für diesen tollen BBBBeitrag.
Glänzend! Ich weiß schon warum Murphy diese Artikel immer solange für sich behält, dies sind wirklich echte Perlen die man am liebsten für sich alleine hätte (-: Einfach ein sehr gut geschriebener Artikel zu einem sehr interessanten Thema.
Für mich müssten sie eine Regex-Version erfinden, die sich der ganzen Klammern entledigt: Da hab ich mich schon an Ruby gewöhnt und dass ich fast immer klammerlos sein kann und dann kommen die Dinger da oben :) Aber ich hab da eine ganze einfache Lösung: 42 azuby
Klammerlose RegExes - kein Problem! Einfach eine UPN-Variante erfinden ;-) Allerdings sind die vielen "(?"-Klammerungen schon recht nervend und unübersichtlich. Da hat man mal angefangen die Mustersprache zu erweitern, und das ist dann Wildwuchs geworden. Ein Problem existiert natürlich - es gibt keine spezielle Kennzeichnung von Zeichen(-ketten) in RegExes, so dass man für alles eine spezielle Escape-Darstellung braucht (die "(?" sind ja nichts anderes). Andere Darstellungen von Textmustern, so wie in Snobol4 oder für interaktive Benutzung im damaligen Ediere, haben aber den Nachteil, dass sie sich nicht so kompakt schreiben lassen.
[...] 1: Gruppen, Quantoren und Kino-süchtige Programmierer 2: Atomare Zeitersparnis 3: Zukunftsaussichten und die Jagd nach Feten 4: Ungebetene Gäste, Theatertexte und formale Begrüssungen 4a: Vereinfachte formale Begrüssungen. 5: Benannte Gruppen, Palindrome, … [...]