ruby-mine

exploring the mine

Reguläre Ausdrücke, Teil 1: Gruppen, Quantoren und Kino-süchtige Programmierer

von wonado am 05.06.2006 (23 Uhr)

Dieser Beitrag wurde von Wolfgang Nádasi-Donner alias WoNáDo verbrochen. Damit ist die Rolle des Prügelknaben vergeben.

Richtig geraten - dieser Beitrag dreht sich um Reguläre Ausdrücke und deren Anwendung zur Mustersuche und Textersetzung in Ruby. Allerdings geht es auch nicht um die ganze reguläre Ausdruckswelt, sondern um die Teile, die sie mächtig und manchmal unübersichtlich machen.

Reguläre Ausdrücke in Ruby helfen - manchmal einem Programmierer beim Schreiben von Programmen, oft jedoch der Pharma-Industrie durch einen erhöhten Bedarf an Kopfschmerztabletten. Ein Regulärer Ausdruck, der seine Arbeit nicht so macht wie man will, kann einen schon zur Verzweiflung treiben.

Eine Vorbemerkung über gruppierende Klammerungen

Die brauchen wir laufend. Einfangende gruppierende Klammern sind schlicht und einfach Klammern - also (…), wobei bedeutet, dass dort irgendwelche Regulären Teilausdrücke stehen. Einfangend bedeutet, dass die durch in diesen Klammern stehenden Regulären Teilausdrücke erkannten Textteile an Variablen zugewiesen werden. Auf die kann dann im laufenden Muster durch \1, \2, usw. zugegriffen werden kann. Wenn die gesammte Mustererkennung erfolgreich war, werden diese erkannten Teile in Variablen und im MatchData-Objekt bereitgestellt.

text = 'hier kommt das Wort Aua mehr als einmal vor. Dazu kann man nur Aua sagen!'
if md = text.match(/\b(\w+)\b.*\b\1\b/)
  puts md[1]
end

Gibt völlig korrekt Aua aus. Die geklammerte Gruppe \w+ sucht nach einem Wort, merkt es sich und sucht dann weiter hinten nach genau diesem noch einmal mittels \1.

Wichtiger ist jedoch die Eigenschaft, dass diese Klammern gruppieren. Ein derartig geklammerter Regulärer Teilausdruck wird durch die Quantoren als Einheit behandelt. Die nur gruppierende Klammerung (?:…) macht genau dies auch, weist allerdings keine Zwischen- oder Endergebnisse an Variablen zu. Sie ist dadurch während der Mustersuche auch schneller.

Quantoren

Zurück zum Thema. Was sind und was machen die Quantoren. Sie machen Aussagen darüber, wie oft ein Regulärer (Teil-)Ausdruck vorkommen darf. Anders als im täglichen Gebrauch, da sagt man “ich hatte vier bis sechs Bier getrunken”, muss man sich an “ich hatte Bier getrunken, vier bis sechs” gewöhnen, weil die Quantoren immer hinter dem (Teil-)Ausdruck stehen, auf den sie sich beziehen.

Gierige Quantoren

Nehmen wir erst mal die, die hinten nicht noch ein Fragezeichen angehängt bekommen haben, also *, +, {m,n} und ?. Das sind die gierigen (greedy) Quantoren. Sie verhalten sich wie Krümelmonster mit Keksen - sie nehmen so viel wie möglich zu sich. Erst wenn dann das Restmuster nicht mehr auf dem übriggelassenen Text zieht, geben sie wieder etwas her, allerdings so wenig wie möglich (ich verzichte jetzt auf Vergleiche, falls jemand gerade isst…).

Die Bedeutungen der gierigen Quantoren sind:

*
so viel wie möglich, aber (zur Not) auch nichts
+
so viel wie möglich, mindestens aber eines
{m,n}
wenn möglich n-mal, mindestens aber m-mal
?
einmal, wenn es nicht geht (zur Not) auch nichts

Damit jeder sie mal im Einsatz sieht, ein Beispiel:

group = ':Peter:Paul:Mary:Erna'
pattern = /.*/
md = group.match(pattern)
puts "'#{group}'.match(/#{pattern.source}/): #{md.pre_match}<#{md[0]}>#{md.post_match}"
pattern = /.+/
md = group.match(pattern)
puts "'#{group}'.match(/#{pattern.source}/): #{md.pre_match}<#{md[0]}>#{md.post_match}"
pattern = /.?/
md = group.match(pattern)
puts "'#{group}'.match(/#{pattern.source}/): #{md.pre_match}<#{md[0]}>#{md.post_match}"
pattern = /.{2,6}/
md = group.match(pattern)
puts "'#{group}'.match(/#{pattern.source}/): #{md.pre_match}<#{md[0]}>#{md.post_match}"

Die geben nun schön brav aus, was sie getan haben.

':Peter:Paul:Mary:Erna'.match(/.*/): <:Peter:Paul:Mary:Erna>
':Peter:Paul:Mary:Erna'.match(/.+/): <:Peter:Paul:Mary:Erna>
':Peter:Paul:Mary:Erna'.match(/.?/): <:>Peter:Paul:Mary:Erna
':Peter:Paul:Mary:Erna'.match(/.{2,6}/): <:Peter>:Paul:Mary:Erna

Das ist alles richtig!

Anmerkung mit Beispiel

Weiter oben steht sie nehmen so viel wie möglich zu sich. Das stimmt so leider nicht in allen Fällen. Zutreffender, aber viel umständlicher, ist es wird versucht das vom Quantor betroffene Muster so oft wie möglich anzuwenden. Dazu ein Beispiel.

group = 'aaaaaxx'
pattern = /(ax|a)*x/
md = group.match(pattern)
puts "'#{group}'.match(/#{pattern.source}/): #{md.pre_match}<#{md[0]}>#{md.post_match}"

Das gibt dann so wie erwartet:

'aaaaaxx'.match(/(ax|a)*x/): <aaaaaxx>

Es werden also nacheinander a-a-a-a-ax durch den quantifizierten Audruck erkannt, gefolgt vom x.
Nun ändere ich den Ausdruck minimal:

group = 'aaaaaxx'
pattern = /(a|ax)*x/
md = group.match(pattern)
puts "'#{group}'.match(/#{pattern.source}/): #{md.pre_match}<#{md[0]}>#{md.post_match}"

Nun kommt das unerwartete (=unerwünschte) Ergebnis:

'aaaaaxx'.match(/(a|ax)*x/): <aaaaax>x

??? - Wie konnte das passieren. Nun, ganz einfach: Der *-Quantor bedeutet doch, dass versucht wird, den betroffenen Teilausdruck so oft wie möglich anzuwenden. Das geht 5 mal mit dem a, womit dann der Teisstring aaaaa erfasst wurde. Das jetzt übrigbleibende x kann der Teilausdruck a|ax nicht erkennen, also hat der gefrässige *-Quantor erst einmal seine Arbeit getan. Das abschliessende x im Muster erkennt nun das übrig gebliebene x im String, mehr wird nicht verlangt, also ist die Mustermaschine fertig.

In diesem Fall, also bei diesem String, entspricht das dem Beispiel:

pattern = /(a|ax)(a|ax)(a|ax)(a|ax)(a|ax)x/
md = group.match(pattern)
puts "'#{group}'.match(/#{pattern.source}/): #{md.pre_match}<#{md[0]}>#{md.post_match}"

Mit dem schon bekannten Ergebnis:

'aaaaaxx'.match(/(a|ax)(a|ax)(a|ax)(a|ax)(a|ax)x/): <aaaaax>x

Nicht-gierige Quantoren

Es gibt auch andere Quantoren. Sie gebären sich anfänglich wesentlich bescheidener - jedoch auch fauler. Sie versuchen so wenig wie möglich zu erkennen und gehen nur auf Aufforderung weiter (wenn das restliche Muster noch nicht zutrifft.) Sie sehen fast genauso aus wie ihre gefrässigen Verwandten, haben jedoch ein ? am Ende, also *?, +?, {m,n}? und ??:

*?
nichts oder so wenig wie möglich
+?
mindestens eines, jedoch so wenig wie möglich
{m,n}?
mindestens m-mal, höchstens jedoch n-mal
??
wenn möglich nichts, sonst eines

Auch hier ein kurzes Beispiel zu jedem Quantor:

group = ':Peter:Paul:Mary:Erna'
pattern = /.*?/
md = group.match(pattern)
puts "'#{group}'.match(/#{pattern.source}/): #{md.pre_match}<#{md[0]}>#{md.post_match}"
pattern = /.+?/
md = group.match(pattern)
puts "'#{group}'.match(/#{pattern.source}/): #{md.pre_match}<#{md[0]}>#{md.post_match}"
pattern = /.??/
md = group.match(pattern)
puts "'#{group}'.match(/#{pattern.source}/): #{md.pre_match}<#{md[0]}>#{md.post_match}"
pattern = /.{2,6}?/
md = group.match(pattern)
puts "'#{group}'.match(/#{pattern.source}/): #{md.pre_match}<#{md[0]}>#{md.post_match}"

Die erwarteten Ergenisse sehen dann so aus:

':Peter:Paul:Mary:Erna'.match(/.*?/): <>:Peter:Paul:Mary:Erna
':Peter:Paul:Mary:Erna'.match(/.+?/): <:>Peter:Paul:Mary:Erna
':Peter:Paul:Mary:Erna'.match(/.??/): <>:Peter:Paul:Mary:Erna
':Peter:Paul:Mary:Erna'.match(/.{2,6}?/): <:P>eter:Paul:Mary:Erna

Ein realistischeres Beispiel

Angenommen, es gibt ein bisschen Text, in dem mit dem HTML-üblichen <b>…</b> Teile fett markiert sind. Ich möchte nun alle derartigen Textstücke aufgelistet haben. Ein erster Versuch:

text = <<'BLABLA'
Dieser <b>Text</b> hier enthält einige <b>Elemente</b>,
die durch <b>Bold-Markierungen</b> gekennzeichnet sind. All
jene möchte ich jetzt aufgelistet haben, auch wenn sie <b>über
Zeilengrenzen</b> hinaus gehen.
BLABLA
n=0
text.scan(/<b>.*<\/b>/m){|bold|puts "Match #{n+=1}: #{bold}"}

Das bringt mir aber nicht das gewünschte Ergebnis.

Match 1: <b>Text</b> hier enthält einige <b>Elemente</b>,
die durch <b>Bold-Markierungen</b> gekennzeichnet sind. All
jene möchte ich jetzt aufgelistet haben, auch wenn sie <b>über
Zeilengrenzen</b>

Woran liegt das? Ganz einfach! Ich habe fälschlicherweise den gierig gefrässigen Quantor * benutzt. Der sorgt dafür, dass nach dem ersten <b> erst mal der ganze Rest versucht wird. Da streikt natürlich die Mustermaschine, denn die verlangt ja noch nach einem abschliessenden </b>. Also rückt der gefrässige Teil Zeichen für Zeichen wieder heraus (das vorhin vorgeführte Problem tritt hier nicht auf), bis ein </b> freigegeben wird - es ist allerdings das letzte! Da dies Ergebnis nicht meinen Erwartungen entspricht, nun ein zweiter Versuch. Bei diesem möchte ich nebenbei noch auf murphys Beitrag Rubinrote Strings - Teil 3 Heredocs verweisen.

n=0;<<'BLABLA'.scan(/<b>.*?<\/b>/m){|bold|puts "Match #{n+=1}: #{bold.gsub(/\n/, ' ')}"}
Dieser <b>Text</b> hier enthält einige <b>Elemente</b>,
die durch <b>Bold-Markierungen</b> gekennzeichnet sind. All
jene möchte ich jetzt aufgelistet haben, auch wenn sie <b>über
Zeilengrenzen</b> hinaus gehen.
BLABLA

Das Ergebnis sieht nun schon viel besser aus:

Match 1: <b>Text</b>
Match 2: <b>Elemente</b>
Match 3: <b>Bold-Markierungen</b>
Match 4: <b>über Zeilengrenzen</b>

Was ist hier nun anders? Ich habe den nicht-gierigen Quantor * benutzt, der ganz bescheiden so wenig wie möglich einnimmt. Dadurch erfasst </b> wirklich das nächste derartige Element, und nicht erst das letzte.

Ein gemischter Fall

Angenommen, ich säße in einer Firma mit lauter Kino-süchtigen Programmierern und bei mir gingen Datei für Datei Kritiken ein. Sei alle beginnen immer mit dem Titel und haben am Ende eine Bewertung. Da ich den Kritiken vertraue (ich möchte betonen, dass dies ein angenommer Fall ist) und die Kollegen von mir nur eine Liste mit Titel und der Beurteilung haben wollen, will ich genau so etwas erstellen. Nehmen wir mal eine Beispielkritik und stecken sie für die Programmbeispiele in einen Ruby-Rahmen.

text = <<'BEWERTUNG'
Rubinroter Morgen: Die Geschichte eines einsamen Java-Programmierers, der
durch seinen grauen Alltag bedingt immer mehr an Depressionen leidet. Er
zweifelt zunehmend an seinen Fähigkeiten und beginnt erst nach und nach zu
begreifen, dass seine Probleme mehr an der Sprache, als an seinen Fähigkeiten
liegen. Eines schönen Frühlingsmorgens dann stösst er im Netz auf eine ganz andere
Sprache: Ruby. Wie vom Blitz getroffen merkt er, dass aus stumpfsinner Arbeit
plötzlich lustvolles Kreieren wird, dass 'Arbeit' sogar zur falschen Bezeichnung
wird. - Sehenswert
BEWERTUNG

Also los mit einem ersten Versuch:

puts '>>>>> Nicht gierige Quantoren'
text.scan(/\A(.+?):.+?\-(.+?)\Z/m) do |titel, bewertung|
  puts "'#{titel}' erhält die Bewertung: #{bewertung}"
end

Mal sehen, was da raus kommt:

>>>>> Nicht gierige Quantoren
'Rubinroter Morgen' erhält die Bewertung: Programmierers, der
durch seinen grauen Alltag bedingt immer mehr an Depressionen leidet. Er
zweifelt zunehmend an seinen Fähigkeiten und beginnt erst nach und nach zu
begreifen, dass seine Probleme mehr an der Sprache, als an seinen Fähigkeiten
liegen. Eines schönen Frühlingsmorgens dann stösst er im Netz auf eine ganz andere
Sprache: Ruby. Wie vom Blitz getroffen merkt er, dass aus stumpfsinner Arbeit
plötzlich lustvolles Kreieren wird, dass 'Arbeit' sogar zur falschen Bezeichnung
wird. - Sehenswert

Nun ja, nicht sooooo gaaaanz das was ich wollte. Die Bewertung ist die gesamte Kritik ab dem Wort 'Programmierers'. Wie konnte das geschehen? - Ganz simpel. Ich suche am Anfang zweimal so wenig wie möglich, gefolgt von einem -. Der Rest bis zum Ende wird als Bewertung genommen. Nun gibt es aber zweimal - in dieser Kritik. Die bescheidenen Quantoren lassen die Mustermaschine zuerst das erste erkennen. Da der Rest aber sofort auf (.+?)\Z passt, kommt das gesehene Ergebnis zustande.

Also gut - nun ein gefrässiger Versuch:

puts '>>>>> Gierige Quantoren'
text.scan(/\A(.+):.+\-(.+)\Z/m) do |titel, bewertung|
  puts "'#{titel}' erhält die Bewertung: #{bewertung}"
end

Diesmal kommt es etwas anders heraus:

>>>>> Gierige Quantoren
'Rubinroter Morgen: Die Geschichte eines einsamen Java-Programmierers, der
durch seinen grauen Alltag bedingt immer mehr an Depressionen leidet. Er
zweifelt zunehmend an seinen Fähigkeiten und beginnt erst nach und nach zu
begreifen, dass seine Probleme mehr an der Sprache, als an seinen Fähigkeiten
liegen. Eines schönen Frühlingsmorgens dann stösst er im Netz auf eine ganz andere
Sprache' erhält die Bewertung:  Sehenswert

Das hat aber auch noch nichts mit der gewünschten Zusammenfassung zu tun. Diesmal ist der Titel viel zu lang und endet erst mit 'Sprache'. Warum das jetzt? - Auch hier ist die Antwort einfach. Schon der erste Teil (.+): enthält einen gierigen Quantor. Da aber zwei : im Text vorkommen, nimmt er den zweiten im Satz …andere Sprache: Ruby.. Da der Rest passt, gibt sich die Mustermaschine damit zufrieden.

Nun gut - vielleicht hilft ja eine durchdachte Mischung aus gierig und nicht gierig (sollte man auch beim Essen parat haben, sonst ist man satt ehe die Leckereien kommen.)

puts '>>>>> Richtige Mischung'
text.scan(/\A(.+?):.+\-(.+?)\Z/m) do |titel, bewertung|
  puts "'#{titel}' erhält die Bewertung: #{bewertung}"
end

Nun kommt endlich das, was ich haben wollte!

>>>>> Richtige Mischung
'Rubinroter Morgen' erhält die Bewertung:  Sehenswert

Hier ist nun der erste Teil bescheiden - findet also das erste : - während das Mittelstück gefrässig ist - findet also das letzte -.

Eine Gemeinheit zum Abschluss…

Mit diesem Text benutze ich nun ein leicht geändertes Programm:

puts '>>>>> Warum das Ergebnis?'
text.scan(/^(.+?):.+?\-(.+?)$/m) do |titel, bewertung|
  puts '***** Bewertung gefunden *****'
  puts "'#{titel}' erhält die Bewertung: #{bewertung}"
end

Das Ergebnis ist höchst seltsam:

>>>>> Warum das Ergebnis?
***** Bewertung gefunden *****
'Rubinroter Morgen' erhält die Bewertung: Programmierers, der
***** Bewertung gefunden *****
'durch seinen grauen Alltag bedingt immer mehr an Depressionen leidet. Er
zweifelt zunehmend an seinen Fähigkeiten und beginnt erst nach und nach zu
begreifen, dass seine Probleme mehr an der Sprache, als an seinen Fähigkeiten
liegen. Eines schönen Frühlingsmorgens dann stösst er im Netz auf eine ganz andere
Sprache' erhält die Bewertung:  Sehenswert

Das kläre ich aber nicht mehr auf…


Kommentar schreiben

Name (notwendig)

Mail (wird nicht veröffentlicht)

Webseite


Kommentare

  1. Der Notizblog &raquo; Blog Archive &raquo; Ruby &#38; Regex schrieb am 06.06.2006 (00 Uhr)

    [...] Aber vielleicht hilft ja der Artikel Reguläre Ausdrücke – Teil 1: Gruppen, Quantoren und Kino-süchtige Programmierer aus der Ruby-Mine ein paar meiner Probleme abzubauen. Ab ins Linkverzeichnis. [...]

  2. hehejo schrieb am 22.06.2006 (11 Uhr)

    Danke! Jetzt hab ich auch endlich wieder die RegExp für meine Homepage hinbekommen. Aber wenn ich mir die jetzt so ansehe, sehe ich keinen Unterschied mehr zu den alten RegExp in Php - welche aber nicht funktioniert hatten. Naja - jetzt geht's wieder. Danke.

  3. Ruby-Mine &raquo; Blog Archive &raquo; Reguläre Ausdrücke, Teil 6: Unerwartetes, Gemeinheite schrieb am 29.10.2006 (19 Uhr)

    [...] 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, … [...]