ruby-mine

exploring the mine

Reguläre Ausdrücke, Teil 4: Ungebetene Gäste, Theatertexte und formale Begrüssungen

von wonado am 06.09.2006 (23 Uhr)

von WoNáDo

So - nun sind wir bei Ruby 1.9 gelandet. Was jetzt folgt geht nicht mehr mit Ruby 1.8. Dahinter steckt die Mustermaschine “Oniguruma”, zu der man mehr auf der offiziellen Seite mehr erfahren kann, speziell von Interesse ist das Dokument “RE.txt”, in welchem die gültigen Musterelemente beschrieben sind.

Ruby 1.9 kann man sich selber erstellen oder für Windows eine lauffähige Version herunterladen. Alle Programme dieses Beitrags laufen unter Windows mit der aus “ruby-1.9.0-20060415-i386-mswin32.zip” extrahierten Version ohne Probleme.

Nun, dann mal ab zum Thema - alles dreht sich diesmal um “Rückblicke”, etwas genauer gesagt, um “look-behind”-Konstrukte. Da gibt es zum einen /(?<=…)/ für das “positive look-behind”, und zum anderen /(?<!…)/ für das “negative look-behind” (die // bedeuten wie üblich irgendwelche Teilmuster.

Sie sind wie ihre “look-ahead”-Schwestern sogenannte “Zusicherungen der Länge Null”, was nichts anderes bedeutet, als dass sie nach etwas schauen, ohne irgendein Zeichen zu verbrauchen und die Position im String zu verändern.

Diese beiden Konstrukte schauen ab der aktuellen Position zurück, sie schauen also noch einmal das an, was schon erkannt wurde, allerdings (normaler- und sinnvollerweise) unter anderen Gesichtspunkten. Eine Besonderheit gibt es noch für diese Konstruktionen. Die in den “look-behind”-Konstrukten vorhandenen Muster müssen feste Länge haben und dürfen nur auf oberster Ebene unterschiedliche Länge besitzen. Es sind also alle Quantoren (/+/, /*/, /?/, /{,}/ (ausser beide Werte sind gleich), /+?/, /*?/, /??/ und /{,}?/) verboten, sowie unterschiedlich lange Muster (fester Länge) in Alternativen von Klammerstrukturen.

Klingt kompliziert, ist es aber nicht. /(?<=abc|ab)/ ist erlaubt, /(?<=a(bc|b))/ ist verboten. Ansonsten darf man im “negative look-behind”-Konstrukt auch keine “gruppierenden Klammern” benutzen - ich habe festgestellt, dass sie keinen Fehler verursachen, jedoch auch nichts liefern. Zusätzlich meldet Ruby einen Fehler, wenn man das “\G”-Musterelement in einem “look-behind”-Konstrukt benutzt. Mir ist nicht klar geworden ob das ein Fehler oder ein Leistungsmerkmal ist. Eine Mail zur Abklärung an “Oniguruma” muss ich noch schreiben.

Ohne weitere langatmige Erklärung nun ein paar Beispiele zu den “look-behind”-Konstrukten.

Ungebetene Gäste

Ich will eine Fete machen, aber auf keinen Fall irgendwelche Leute der “Geier-Bande” einladen. Die erkenne ich daran, dass ihr Nachname entweder “Geier” ist, oder der Doppelname auf “Geier” endet.

Alle Namen stehen in einer Datei in der Form “Nachname, Vorname” je Zeile - Im Beispiel packe ich sie auszugsweise in den String “gaeste”. Dann mal los …

gaeste = <<LISTE
Mueller-Luedenscheidt, Gerda
Schmeichel-Geier, Heidelinde
Johanson, Paul-Egon
Huber-Geier, Elfriede
Geier, Hans-Hubert
Feuerpfeiffer, Gustav
LISTE

gaeste.scan(/^([^,]+)(?<!Geier),\s+(.+)$/) do |name, vorname|
  puts(vorname +   + name)
end

Das hilft dann die gewünschte Einladungsliste zu erstellen, auf der niemand von der “Geier-Bande” steht:

Gerda Mueller-Luedenscheidt
Paul-Egon Johanson
Gustav Feuerpfeiffer

Wie geht das nun?

Schauen wir uns mal den Regulären Ausdruck an. Er enthält ein “negative look-behind”-Element: /(?<!Geier)/.

Lassen wir diesen Teil weg, gibt das den wohlbekannten Ausdruck /^([^,]+),\s+(.+)$/. Da steckt nun nichts anderes hinter, als aus jedem Satz alles vor dem Komma in die erste Gruppe (im Block “name”) zu packen, sowie alles hinter “, ” in die zweite (im Block “vorname”). Auf diese Weise erfasse ich alle Namen.

Das will ich aber nicht! - Die “Geier-Bande” will ich nicht auf meinen Feten sehen!

Ich muss also nachschauen ob der schon erkannte Nachname nicht mit “Geier” endet. Der Nachname ist direkt vor dem Komma beendet, also muss ich an dieser Stelle rückwärts blicken und will dort nicht “Geier” sehen. Genau das macht /(?<!Geier)/. Für das Muster bedeutet es ein Scheitern, sobald “Geier” am Ende des Nachnamens steht. Dann wird der Block nicht ausgeführt, also auch keine Einladung für die “Geierschen” gedruckt.

Alles klar?! - Na dann zum nächsten Beispiel.

Theatertexte

Diesmal möchte ich als fauler Theaterkritiker aus der Vorlage für die Schauspieler direkt einen Auszug für die Zeitung erstellen, den ich dann nur noch mit ein bisschen Fülltext ohne inhaltliche Bedeutung ergänzen muss (ich möchte darauf hinweisen, dass ich NICHT behaupte bestimmte Zeitungen würden so vorgehen).

Die Sprechvorlage für die Schauspieler steht in einer Datei. Ihr Aufbau ist spaltenorientiert (keine Proportionalschrift), und zwar steht am Anfang einer Zeile ein Sprechername, wenn dieser Sprecher jetzt etwas sagen soll. Stehen am Anfang einer Zeile nur Leerzeichen ist es eine Fortsetzungszeile für den gerade aktuellen Sprecher. Die eigentlichen Texte beginnen immer ab der Position 10 (ab 0 gezählt), also stehen entweder 10 Leerzeichen am Anfang oder der Sprechername, aufgefüllt mit Leerzeichen bis zur Position 10.

Meine Ausgabe will ich druckreif. Also jeweils den Sprechernamen gefolgt von einer “Aussagephrase” und dem Sprechtext in doppelten Anführungszeichen. Die “Aussagephrase” soll per Zufallsgenerator festgelegt werden, damit es nicht so eintönig ausssieht (schliesslich soll mein Auftraggeber glauben ich hätte gearbeitet).

Los gehts also - erst mal das Programm mit Ergebnis, über den Inhalt sprechen wir anschliessend.

dialog = <<TEXT
Gerda     Sag mal Paul, wollten wir heute nicht ein bisschen
          Freude mit Ruby haben?
Paul      Oh ja Gerda, lass uns mit Genuss beginnen!
Gerda     Uberlegen wir uns doch mal, wie wir Strategien fuer
          unsere zukuenftige Robo-Cup-Mannschaft planen koennen.
Paul      Na, dann schaffen wir uns doch mal eine Mannschaft, also
          erst mal ‘ne Klasse fuer Spieler eine Mannschaftsklasse,
          die Spieler enthalten soll.
Gerda     Ja, das ist schon mal sehr schoen. Nun können wir noch
          ein bisschen mit den Spielern spielen - in richtigen
          Mannschaften haben die Spieler doch alle unterschiedliche
          Faehigkeiten.
Paul      Klar! - Du meinst, wir sollen für die einzelnen Spieler
          noch individuelle Methoden erstellen. Schoen, dass so etwas
          in Ruby geht.
TEXT

def tab(n)
  raise RegexpError, negativer Tab-Wert if n < 0
  return /(?:.*(?<=^.{#{n}}))/
end

schreibtext = 

phrasen = [  meint ,
             sagt ,
             spricht ,
             bemerkt ,
             aeussert ,
             erwaehnt ,
             findet ]
srand

dialog.scan(/^(\w*)#{tab(10)}(.*)$/) do |sprecher, text|
  if sprecher.match(/\w/)
    schreibtext += \”\n unless schreibtext.length == 0
    schreibtext << sprecher << phrasen[rand(phrasen.length)] <<  << text
  else
    schreibtext << \n << text
  end
end
puts schreibtext

Das funktioniert so hervorragend, dass wohl kaum ein Leser meine Faulheit spüren wird:

Gerda findet "Sag mal Paul, wollten wir heute nicht ein
bisschen Freude mit Ruby haben?"
Paul erwaehnt "Oh ja Gerda, lass uns mit Genuss beginnen!"
Gerda meint "Uberlegen wir uns doch mal, wie wir Strategien
fuer unsere zukuenftige Robo-Cup-Mannschaft planen koennen."
Paul spricht "Na, dann schaffen wir uns doch mal eine Mannschaft,
also erst mal 'ne Klasse fuer Spieler eine Mannschaftsklasse,
die Spieler enthalten soll."
Gerda aeussert "Ja, das ist schon mal sehr schoen. Nun können
wir noch ein bisschen mit den Spielern spielen - in richtigen
Mannschaften haben die Spieler doch alle unterschiedliche Faehigkeiten."
Paul sagt "Klar! - Du meinst, wir sollen für die einzelnen
Spieler noch individuelle Methoden erstellen. Schoen, dass so
etwas in Ruby geht.

Bleibt die Frage, wie das funktioniert. Ich lasse mal die Detailfummelei weg, weil die zwar nett ist, aber nicht besonders aufregend. Beginnen wir also mit der entscheidenden Methode hinter dem Dialogtext.

def tab(n)
  raise RegexpError, negativer Tab-Wert if n < 0
  return /(?:.*(?<=^.{#{n}}))/
end

Zuerst einmal schmeisst sie einen Fehler wenn der Parameter kleiner 0 ist (eine Zahl erwarte ich sowieso, auf entsprechende Tests verzichte ich hier aus Platzgründen). Wird dagegen ein Zahlenwert ab 0 geliefert, ergibt der Aufruf der Methode einen Regulären Ausdruck, der mittels /#{tab(n)}/ in anderen Regulären Ausdrücken benutzt werden kann. Bleibt die Frage, was das denn für ein Ausdruck ist. Gehen wir mal für den (im Programm benutzten) Parameterwert 10 ins Detail. Nach Auswertung von /#{n}/ steht dort

return /(?:.*(?<=^.{10}))/

Wenn wir das näher betrachten wollen, kann erst einmal die äussere Gruppierung weg. Die hat nur den Sinn Konstruktionen wie /#{tab(15)}+/ sinnvoll zu ermöglichen. bleibt also /.*(?<=^.{10})/. Jetzt schauen wir mal das Ende an, nämlich /(?<=^.{10})/, Da steht aber nichts anderes als “an der aktuellen Position sollen vom linken Rand der Zeile beginnend 10 beliebige Zeichen stehen”. Das heisst doch aber anders ausgedrückt “ich muss mich an Position 10 befinden”.

So weit, so gut. Was bewirkt denn nun aber noch das .* am Anfang? - Es sagt doch nichts anderes als “wenn ich noch nicht an Position 10 bin, kann ich noch ein paar beliebige Zeichen zugeben”. Anders ausgedrückt, das von der Methode erzeugte Muster zieht immer, wenn die Position in der Zeile noch vor der im Parameter angegebenen liegt und es setzt die aktuelle Position im String auf die angegebene Position vom Zeilenanfang an gerechnet.

Es entspricht also genau der “Tabulator”-Funktion! Jetzt wird auch klar warum Werte kleiner 0 sinnlos sind: Die erste Position in einer Zeile ist 0.

Der Rest ist nun ganz einfach. In der Zeile

dialog.scan(/^(\w*)#{tab(10)}(.*)$/) do |sprecher, text|

wird ein eventuell vorhandener Text am Anfang der Zeile in die Blockvariable “sprecher” gegeben, dann wird alles bis zur Possition 10 übergangen und der Rest der Zeile wird an die Blockvariable “text” gereicht.

So einfach ist das!

Anmerkung: Ein derartiges Tabulatormuster lässt sich meiner Kenntnis nach nicht ohne “look-behind” in einem Muster ausdrücken. Falls jemand eine andere Lösung hat, bitte ich, diese als Kommentar bekannt zu geben.

Formale Begrüssungen

Bisher ging es immer um irgendwelche Veranstaltungen - Feten und Theater. Bleiben wir auch im letzten Beispiel in diesem Umfeld.

Das Programm werde ich nicht vollständig beschreiben, sondern nur die das “look-behind” betreffenden Teile. So bleibt für den Leser auch noch ein bisschen Analyse-Spass.

Um was geht es detailliert?

Es soll ein Bankett mit Büffet für alle Rubyistinnen und Rubyisten veranstaltet werden. Der Text steht in der Variablen “inpuff”. Nun soll der aber nicht auf diese Art ausgedruckt werden, sondern im Blocksatz in einer Breite von 40 Zeichen je Zeile.

Da bei der Aufbereitung der Zeilen einzelne Leerzeichen durch mehrere ersetzt werden, bis das letzte druckbare Zeichen der Zeile an der gewünschten Position erscheint, werden je Zeile an verschiedenen Positionen eventuell verschieden breite Zwischenräume erzeugt. Damit das Druckbild nicht zu einseitig ist, wird der Leerzeichenausgleich abwechselnd je Zeile von links, dann von rechts aus vorgenommen.

Einige Prüfungen dienen nur dem Zweck auch problematische Situationen zu beherrschen. Diese entstehen, wenn einzelne Worte länger als die vorgegebene Breite sind (die werden dann einfach eingefügt) oder wenn auf eine Zeile nur ein Wort passt (das wird dann linksbündig ausgegeben).

Los gehts - erst mal das Programm:

inpuff = <<INTEXT
Liebe Rubyistinnen und Rubyisten!
Anlaesslich der immer wieder, ja taeglich
anzutreffenden ausserordentlichen Freude,
die Ihr, liebe Rubyistinnen und Rubyisten,
bei der Benutzung unserer ausserordentlichen
Lieblingssprache Ruby empfinden duerft, haben
wir Euch alle hier zum Bankett der Rubyistinnen
und Rubyisten geladen. Benutzt die Angebote
des Bueffets genau so, wie Ihr Ruby nutzt:
Einfach nach Wunsch zusammenstellen und
geniessen.
INTEXT

class String
  def fillstring(len, lr = true)
    return  unless self.match(/[^ ]/)
    temp = self.strip.split( )
    return temp[0] if temp.length == 1
    dm = (len - temp.join().length).divmod(temp.length - 1)
    if lr
      (temp[0…(temp.length - dm[1])].join(  * dm[0]) +   * (dm[0] + 1) +
        temp[(temp.length - dm[1])…(temp.length)].join(  * (dm[0] + 1))).strip
    else
      (temp[0…dm[1]].join(  * (dm[0] + 1)) +   * (dm[0] + 1) +
        temp[dm[1]…temp.length].join(  * dm[0])).strip
    end
  end
end

l_to_r = false
outpuff = 
pat = /(\w+[:.,;?!]? )/
breite = 40
inpuff = inpuff.gsub(/\s+/,  ) +  
while inpuff.length > breite
  inpuff.sub!(/((#{pat}+)(?=.*(?<=^.{#{breite+1}}))|#{pat})/) do |m|
    outpuff << m.fillstring(breite, (l_to_r = !l_to_r)) << \n
    
  end
end
outpuff << inpuff.fillstring(breite, !l_to_r)
puts outpuff

Nachdem das Programm erfolgreich gelaufen ist, habe ich als Ausgabe die gewünschte Blocksatzformatierung mit der Breite von 40 Zeichen je Zeile.

Liebe   Rubyistinnen   und    Rubyisten!
Anlaesslich   der   immer   wieder,   ja
taeglich                  anzutreffenden
ausserordentlichen   Freude,   die  Ihr,
liebe Rubyistinnen  und  Rubyisten,  bei
der Benutzung unserer ausserordentlichen
Lieblingssprache Ruby empfinden  duerft,
haben wir Euch alle hier zum Bankett der
Rubyistinnen  und   Rubyisten   geladen.
Benutzt  die Angebote des Bueffets genau
so, wie Ihr  Ruby  nutzt:  Einfach  nach
Wunsch  zusammenstellen  und  geniessen.

Um es gleich zu sagen: Um die ärgerliche while-Schleife komme ich nicht rum, weil das /\G/-Konstrukt in “look-behind”-Mustern nicht erlaubt ist.

Wie schon gesagt besprechen wir nur den das Thema betreffenden Teil. Das Herz der ganzen Geschichte verbirgt sich hinter den Zeilen

inpuff = inpuff.gsub(/\s+/,  ) +  

sowie

pat = /(\w+[:.,;?!]? )/
inpuff.sub!(/((#{pat}+)(?=.*(?<=^.{#{breite+1}}))|#{pat})/) do |m|

Die erste aufgeführte Zeile macht nichts anderes als im gesamten “inpuff”-String alle Sequenzen von Leerzeichen und Zeilenvorschüben durch jeweils ein Leerzeichen zu ersetzen und fügt anschliessend noch eines am Ende an. Danach besteht “inpuff” aus einer Zeile, die aus aufeinander folgenden Worten mit eventuell angefügtem Satzzeichen und einem nachfolgenden Leerzeichen (am Ende können es auch zwei sein) besteht.

Das Muster in “pat” erkennt genau solch ein “Wort mit Anhang und Leerzeichen”. Es wird dann im eigentlichen Hauptmuster benutzt.

Was soll dieses Hauptmuster bewirken? - Es soll so viel wie möglich “Worte mit Anhang und Leerzeichen” vom Anfang von “inpuff” nehmen, jedoch nur so viele, dass sie insgesamt nicht mehr als 41 Zeichen lang sind. Warum 41 und nicht 40? - Weil am Ende immer ein Leerzeichen ist, welches noch entfernt wird.

Wenn ein einziges Wort mit einer Länge von über 40 Zeichen vorkommt, dann soll es als Ganzes komplett genommen werden. Es ist dann zwar zu lang, aber Worttrennungen will ich hier nicht versuchen. Dieser Sonderfall wird durch das Teilmuster /(…|#{pat})/ am Ende des Musters behandelt. Bei der weiteren Analyse lasse ich es genauso weg, wie die zuegehörige Gruppierung. Bleibt also das Muster

/(#{pat}+)(?=.*(?<=^.{#{breite+1}}))/

Um das klarer zu gestalten löse ich die /#{}/-Referenzen auf und erhalte

/((\w+[:.,;?!]? )+)(?=.*(?<=^.{41}))/

Den vorderen Teil kennen wir schon aus der Anforderung. Der versucht so viel wie möglich “Worte mit Anhang und Leerzeichen” zu sammeln. Werfen wir also einen Blick auf den zweiten Teil des Musters.

/(?=.*(?<=^.{41}))/

Das sieht ziemlich abgefahren aus - eine “look-ahead”-Gruppe, die eine “look-behind”-Gruppe enthält. Die “look-behind”-Gruppe kennen wir aber schon von der “tab-Methode” des letzten Beispiels, welches komplett fast identisch war: /.*(?<=^.{#{n}})/.

Erinnern wir uns - Beim “Tabulator” sollen beliebige Zeichen bis zur gewünschten Position übersprungen werden. Hier im Beispiel darf aber nichts wegfallen - Es soll nur “vorausblickend” geklärt werden, ob man von der aktuellen Position aus noch zur Spalte 41 vorrücken könnte.

Nun ist alles klar! - da der erste Teil des Gesamtmusters “gefrässig” ist, versucht er so viel wie möglich vom Text zu nehmen. Das gestatten ihm der zweite Teil aber nur, wenn nicht mehr als 41 Zeichen genommen wurden.

Dieser erkannte Teil wird dann im Block verarbeitet - darauf gehe ich aber, wie schon erwähnt, nicht ein.

Wichtig ist nur noch, dass der erkannte Teil mittels sub! gelöscht wird, bevor in der Schleife der nun Verkürzte String erneut behandelt wird. Das liesse sich vermeiden, wenn statt dem /(?<=^.{#{breite+1}})/-Teilmuster eines mit /\G/ benutzt werden könnte, also /(?<=\G.{#{breite+1}})/. Das ist jedoch leider nicht zulässig.

So - dann mache ich mal Schluss für heute. Das nächste Mal wird schlimmer - mein Wort drauf ;-)


Kommentar schreiben

Name (notwendig)

Mail (wird nicht veröffentlicht)

Webseite


Kommentare

  1. Ruby-Mine &raquo; Blog Archive &raquo; Reguläre Ausdrücke, Teil 4a: Vereinfachte formale Beg schrieb am 10.09.2006 (21 Uhr)

    [...] Es ist etwas wunderbares geschehen. Kurz nachdem ich K. Kosako (Oniguruma) das Problem mit dem nicht erlaubten /G/ in “look-behind”-Konstrukten gemailt habe (siehe Beitrag), stellte er eine neue Oniguruma Version (4.1.2) bereit. Diese habe ich dann in die Ruby 1.9-Quellen kopiert und Ruby 1.9 damit auf Windows erstellt. [...]

  2. cornelius schrieb am 23.10.2007 (21 Uhr)

    Der zentrale Ausdruck /((#{pat}+)(?=.*(?<=^.{#{breite+1}}))|#{pat})/ aus dem letzten Beispiel läßt sich eleganter, aber Deinem didaktischen Anliegen weniger dienlich durch /(.{0,#{breite}})\s/ ersetzen. Ansonsten noch: http://paste.pocoo.org/show/7254/

  3. WoNáDo schrieb am 08.01.2008 (00 Uhr)

    "läßt sich eleganter, aber Deinem didaktischen Anliegen weniger dienlich durch /(.{0,#{breite}})\s/ ersetzen" - ähnliches gilt wahrscheinlich für andere Beispiele dieser Serie auch. Das Problem für mich war, dass ich Konstrukte hatte und ein Problem suchte, dass nicht absolut langweilig ist und sich mit den Konstrukten lösen lässt. Bis auf die rekursiven Muster in Teil 5 und die Beschreibung der Fallen in Teil 6 werden wohl die meisten Beispiele eine Lösung erlauben, die übersichtlicher ist oder reguläre Ausdrücke weit sparsamer einsetzt. Es ging hier wirklich nur um die Veranschaulichung an Beispielen.