ruby-mine

exploring the mine

Reguläre Ausdrücke, Teil 6: Unerwartetes, Gemeinheiten und Tricks.

von wonado am 29.10.2006 (19 Uhr)

von WoNáDo

Vorwort von bovi

So nun ist es soweit. Der letzte Teil von Wolfgangs Regexp Artikeln ist hiermit Online. Eine kleine Übersicht:

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

Mit diesem 6. Teil endet die Serie vorläufig. Und deshalb möchte ich die Chance nutzen und mich ganz herzlich bei Wolfgang bedanken. Für einen der weltweit tiefsten Einblicke in die Möglichkeiten Regulärer Audrücke mit Ruby…

DANKE

Weiter im Text

Am Ende dieser Textserie über reguläre Ausdrücke angekommen gibt es es noch eine Art Anhang. Hier stehen ungeordnet kurze Anmerkungen zu den Sachen, über die man immer mal wieder bei der Anwendung regulärer Ausdrücke stolpert.

Ein Teil der Themen wurde schon in den vorhergehenden Beiträgen am Rande behandelt. Also nicht über derartige Wiederholungen wundern, sie sind beabsichtigt. Dieser Teil hier soll eine Art “kostenloses Kopfschmerzmittel” darstellen, da er durch Hinweise auf problematische Situationen eventuelles Kopfweh im Vorfeld vermeiden soll.

Alle Hinweise und Beispiele gelten für Ruby 1.8.x und 1.9. Über Ruby 1.6 kann ich keine Aussagen (mehr) machen - ich habe es nicht installiert und eigentlich schon vergessen. Sollte ein Fall nur für eine bestimmte Version zutreffen steht das dabei.

Erwähnen muss ich noch, dass die folgenden Auflistungen nicht vollständig sind. Wer wirklich viel mit komplexen regulären Ausdrücken arbeiten will (oder muss) sollte den schon öfter erwähnten “Friedl” (”Reguläre Ausdrücke” - O’Reilly-Verlag) durcharbeiten.

Los gehts!

Versehentlich “nichts” suchen

Was jetzt kommt weiss eigentlich praktisch jeder - wenn man erlaubt, dass ein leerer String ("") zum Muster passt, zieht das Muster immer, egal wie der String aussieht!

Trotzdem erstellt man versehentlich derartige Muster immer wieder mal. Es mag wohl daran liegen, dass man völlig konzentriert am Muster und seinen Teilen arbeitet, und garnicht merkt, dass man ihm auch das “totale Nichtstun” erlaubt. Schnell mal ein paar Standardbeispiele - zuerst das Programm:

text = utschiutschiwahwah
count = 0
patternlist = [ /[1-4]+|42|wummwumm|/,
                /[1-4]+||42|buffbuff/,
                /(42|84|)+/,
                /x*/,
                /x?/,
                /(macht nur ab Ruby 1.9 Sinn){0}/,
                /(?>u*?)+/ ]
patternlist.each do |pattern|
  md = text.match(pattern)
  puts "Fall #{count+=1}: " +
    "#{text}‘.match(/#{pattern.source}/): " +
    "#{md.pre_match}<#{md[0]}>#{md.post_match}"
end

Dann die Ausgabe:

Fall 1: 'utschiutschiwahwah'.match(/[1-4]+|42|wummwumm|/):
'<>utschiutschiwahwah'
Fall 2: 'utschiutschiwahwah'.match(/[1-4]+||42|buffbuff/):
'<>utschiutschiwahwah'
Fall 3: 'utschiutschiwahwah'.match(/(42|84|)+/):
'<>utschiutschiwahwah'
Fall 4: 'utschiutschiwahwah'.match(/x*/):
'<>utschiutschiwahwah'
Fall 5: 'utschiutschiwahwah'.match(/x?/):
'<>utschiutschiwahwah'
Fall 6: 'utschiutschiwahwah'.match(/(macht nur ab Ruby 1.9 Sinn){0}/):
'<>utschiutschiwahwah'
Fall 7: 'utschiutschiwahwah'.match(/(?>u*?)+/):
'<>utschiutschiwahwah'

Die Fälle 1, 2 und 3 passieren manchmal dadurch, dass Alternativen gelöscht werden ohne das zugehörige /|/ zu entfernen. Da die “leere Alternative” erlaubt ist, gibt es keine Fehlermeldung. Ein derartiges Muster passt natürlich immer. Beim Fall 3 lenkt auch noch das /+/ von dieser leeren Alternative ab.

Die Fälle 4 und 5 zeigen nur drastisch, dass /*/ und /?/ (natürlich auch /*?/ und /??/) immer auch den Leerstring zulassen. Dies zu übersehen ist übrigens einer der häufigsten Fehler beim Schreiben von regulären Ausdrücken.

Den Fall 6 habe ich im letzten Beitrag zu Ruby 1.9 vorgestellt. Das wird jeder wohl nur mit voller Absicht schreiben, aber der Hinweis sei noch einmal erlaubt, dass /(…){0}/ immer das leere Muster, und nichts anderes, erkennt.

Fall 7 ist eine besonders gemeine Falle. Derartige Dinge passieren in dieser oder ähnlicher Variante immer wieder, ohne dass man den Grund auf Anhieb sieht. Kurz noch einmal in Erinnerung gerufen, /(?>…)/ ist die Darstellung für die sogenannte “Atomare Gruppierung”. Sie zeichnet sich dadurch aus, dass sie beim erfolgreichen Verlassen alle in der Gruppe gesammelten Alternativen verwirft. Ich habe das so beschrieben, dass sie wie eine Einbahnstrasse funktioniert: man kommt nur von links in die Gruppe hinein, von rechts muss sie jeweils komplett übersprungen werden. Nun ist in Fall 7 in dieser Gruppe nur ein Element, welches mit /*?/ versehen wurde, also zuerst (da “nicht gierig”) den Leerstring "" als Möglichkeit vorsieht. Diese erste Möglichkeit wird durch diese Gruppierung aber als einzige weitergegeben, so dass immer der Leerstring erkannt wird. Folgendes Beispiel zeigt das:

irb(main):001:0> 'utschiutschiwahwah'.match(/^(?>u*?)+t/)
=> nil

Neben den Gemeinheiten durch leere Strings zulassende Muster bei der einfachen Suche, gibt es auch noch Fallen beim mehrfachen Suchen (String#scan) und der mehrfachen Ersetzung (String#gsub und String#gsub!). Wie üblich zeige ich erst mal das Programm und die Ausgabe.

text = utschiwah
count = 0
text.scan(/()/) do |grp|
  puts "Block, die #{count+=1}.: ‘#{$`}<#{grp}>#{$’}"
end
puts *** Nun wird ersetzt***
neutext = text.gsub(/()/) do |grp|
  !
end
puts neutext

Da kommt nun raus:

Block, die 1.: '<>utschiwah'
Block, die 2.: 'u<>tschiwah'
Block, die 3.: 'ut<>schiwah'
Block, die 4.: 'uts<>chiwah'
Block, die 5.: 'utsc<>hiwah'
Block, die 6.: 'utsch<>iwah'
Block, die 7.: 'utschi<>wah'
Block, die 8.: 'utschiw<>ah'
Block, die 9.: 'utschiwa<>h'
Block, die 10.: 'utschiwah<>'
*** Nun wird ersetzt***
!u!t!s!c!h!i!w!a!h!

Wenn man sich die Logik hinter diesen Methoden ansieht, müsste “eigentlich” eine Endlosschleife herauskommen - Ruby also bis in alle Ewigkeit mit der Aufgabe beschäftigt sein. Diese Methoden bewirken doch, dass nach einem Match hinter dem Ende des erkannten Textes weitergemacht wird. Hier, also bei einem Muster (), welches den leeren String erkennt, bleibt diese Position jedoch unverändert. Die Mustermaschine müsste also “eigentlich” immer wieder an der Position 0 des Strings anfangen.

Hier haben nun die Entwickler der Mustermaschinen einen Riegel vorgeschoben. Alle mir bekannten Mustermaschinen (Ruby 1.8, Oniguruma-Ruby 1.9, Perl, Python, C#-.NET) handeln in den Fällen, in denen der jeweils letzte erkannte Teilstring die Länge 0 hatte so, dass sie ohne Warnung ein Zeichen weiterschalten. Auf diese Art und Weise werden die Blöcke im Beispiel für jede Position im String einmal aufgerufen.

Der Anker /\G/

Ich bin auf ihn schon einmal nebenbei zu Sprechen gekommen, ohne allerdings irgend etwas zu erklären. “Eigentlich” ist er recht harmlos, aber leider wegen einer winzigen Unklarheit nicht so 100%-ig.

Er bedeutet “Position am Ende des letzten Match” oder auch “Position am Anfang des aktuellen Match”.

Sieht doch harmlos aus? - Bedeutet doch “eigentlich” das gleiche? - … oder etwa doch nicht? …

Schauen wir uns doch noch einmal das letzte Beispiel (text.scan(/()/)…) etwas näher an. Zuerst wird der leere String an Position 0 erkannt, also vor dem "u".

Beim nächsten Durchgang schaltet die Maschine automatisch auf Position 1 um. Das ist dann der neue Match-Anfang, während der letzte Durchgang bei Position 0 endete. Wie jeder also leicht erkennen kann ist die Bedeutung nicht identisch.

Je nachdem wie die Bedeutung von /\G/ implementiert ist (auf die Beschreibungen kann man sich da leider nicht verlassen - das betrifft praktisch alle Sprachen), muss eine “globale Ersetzung” unterschiedliche Ergebnisse liefern, wenn als Muster /\Gx?/ angegeben wird und auf dem Text "abc" gearbeitet wird. Bedeutet es “Ende des letzten Versuchs” wird ein Block nur einmal ausgeführt, bei “Anfang des aktuellen Versuchs” jedoch für jede automatisch erreichte Position. In den nachfolgenden Beispielen habe ich den Perl-Code dem Buch “Friedl - Reguläre Ausdrücke” entnommen, da ich mich Dank Ruby in Perl mangels Benutzung nicht mehr so recht sattelfest fühle.

Perl:

F:>perl -e "$g='abc';" -e "$g=~s/\Gx?/!/g;" -e "print $g;"
!abc

Ruby 1.8:

F:>ruby -v
ruby 1.8.2 (2004-12-25) [i386-mswin32]

F:>ruby -e "puts 'abc'.gsub(/\Gx?/, '!')"
!a!b!c!

Ruby 1.9:

F:>ruby -v
ruby 1.9.0 (2006-04-15) [i386-mswin32]

F:>ruby -e "puts 'abc'.gsub(/\Gx?/, '!')"
!a!b!c!

Es lässt sich also eindeutig sagen, dass sowohl Ruby 1.8, als auch Ruby 1.9 mit /\G/ den Anfang des aktuellen Match bezeichnen, während sich in Perl das Ende des letzten dahinter verbirgt. Beim Entwurf von Mustern also unbedingt diese Kleinigkeit beachten!

Die Anker /\A/, /\Z/, /\z/, /^/ und /$/

Am Ende des ersten Beitrags dieser Folge, Reguläre Ausdrücke, Teil 1: Gruppen, Quantoren und Kino-süchtige Programmierer, brachte ich sozusagen als Rätsel das eigenartige Verhalten eines Programms.

Dahinter steckte als Muster /^(.+?):.+?\-(.+?)$/m. Dieses Unterschied sich bis auf manche /?/ speziell durch die Angabe von /^/ und /$/ statt /\A/ und /\Z/ von den vorhergehenden.

Um die Gewohnheiten der geneigten Leserschaft nicht zu enttäuschen nun ein erklärendes Beispiel zur Bedeutung der Anker:

text = <<ENDOFBLABLA
Zeile 1
Zeile 2
ENDOFBLABLA
md=text.match(/(\A)(^)(.*?)($)(\n)(^)(.*?)($)(\Z)(\n)($)(\z)/)
print "\\A" if md[1]
print "^" if md[2]
print "#{md[3]}" if md[3]
print "$" if md[4]
print "\\n" if md[5]
print "^" if md[6]
print "#{md[7]}" if md[7]
print "$" if md[8]
print "\\Z" if md[9]
print "\\n" if md[10]
print "$" if md[11]
print "\\z" if md[12]

Das ergibt den Text mit Anker- und Zeilenwechselzeichen in expliziter Darstellung:

\A^Zeile 1$\n^Zeile 2$\Z\n$\z

Das sollte man sich irgendwo als Beispiel aufheben, da es zeigt, wo die Anker ziehen. Die Benutzung von /^/ und /$/ am Ende des ersten Beitrags ermöglichte einen Mehrfach-Match, da diese beiden Elemente auch am Anfang beziehungsweise Ende einer Zeile im String ziehen, und nicht nur am Anfang und Ende des Strings, wie das bei /\A/, /\Z/ und /\z/ der Fall ist. /\Z/ gestattet im Gegensatz zu /\z/ sogar noch einen abschliessenden Zeilenvorschub im String.

Die Multiline-Option m

Diese Option beeinflusst die Bedeutung des /./-Musters. Normalerweise erkennt /./ nicht das Zeichen "\n". Wenn als Option jedoch m angegeben wurde, erkennt /./ jedes beliebige Zeichen inklusive "\n". Ein Beispiel dazu:

grussworte = <<BLABLA
Liebe Gaeste!
Herzlich Willkommen zum Treffen der Rubin-Edelsteinschleifer.
An Ihren Plätzen finden Sie jeweils ein schnelles Notebook,
auf welchem die Aktuelle Version von Ruby installiert ist.
Ich wuensche Ihnen viel Spass bei der Arbeit.
BLABLA
unless grussworte.match(/Rubin.*Ruby/)
  puts Kein Match ohne Option "m"!
end

if (md = grussworte.match(/Rubin.*Ruby/m))
  puts Match mit Option "m":
  puts $&
end

ergibt:

Kein Match ohne Option "m"!
Match mit Option "m":
Rubin-Edelsteinschleifer.
An Ihren Plätzen finden Sie jeweils ein schnelles Notebook,
auf welchem die Aktuelle Version von Ruby

Wie man sieht, muss man beim Entwurf des Musters berücksichtigen, ob eventuell Newline-Zeichen im String vorkommen können. Dann kann man entweder das Muster anders gestalten oder die Option /m/ für das gesamte Muster (//m) oder ein Teilmuster (/(?m:…)/) angeben.

Das bearbeitete Objekt innerhalb von String#gsub!-Blöcken

In dem zu gsub gehörenden Block steht das Objekt in seiner unveränderten Form zur Verfügung, nicht mit den schon in vorhergehenden Durchläufen durchgeführten Änderungen:

text = abcde
text.gsub!(/()/) do |t|
  puts "#{$`}<#{$&}>#{$’}"
  !
end
puts text

ergibt:

<>abcde
a<>bcde
ab<>cde
abc<>de
abcd<>e
abcde<>
!a!b!c!d!e!

Muster, deren Anwendung etwas länger brauchen könnte …

Hier sind wir praktisch am Ende angekommen, weil ich jetzt entweder noch 10 bis 100 Seiten schreiben könnte, oder ich verweise Euch auf den Beitrag Reguläre Ausdrücke, Teil 2: Atomare Zeitersparnis und einen grossen Teil des Buches “Reguläre Ausdrücke” von Jeffrey E.F. Friedl.

Ich habe mich für “oder” entschieden, weil das hier kein Konkurrenzbuch zum “Friedl” werden soll.

Als Hinweis nur die Anmerkung, dass alle regulären (Teil-)Ausdrücke der Form /(…+|…*)*/ auf eine unendliche Verarbeitungszeit (=Milliarden Jahre und mehr) hindeuten, wenn sie auf einem Text nicht ziehen - und das soll ja auch mal vorkommen …

Das war es nun aber endgültig - mehr Gruselgeschichten über reguläre Ausdrücke schreibe ich jetzt nicht mehr. Ich wünsche Euch allen vor allem kopfschmerzfreie und erfolgreiche Anwendungen regulärer Ausdrücke!


Kommentar schreiben

Name (notwendig)

Mail (wird nicht veröffentlicht)

Webseite


Kommentare

  1. WoNáDo schrieb am 22.02.2008 (13 Uhr)

    Oioi - es gab im Originaltext ein paar kleine aber gemeine Fehler, die bei der Übertragung der Programme in die Seitenaufbereitung passiert sind und offensichtlich lange unentdeckt blieben. Die Programme zu den Ankern "\G" und allen des nächsten Kapitels enthielten den Backslash nicht. Das ist jetzt korrigiert.