ruby-mine

exploring the mine

Reguläre Ausdrücke, Teil 7: Oniguruma und statische Relativbezüge

von wonado am 07.02.2009 (21 Uhr)

von WoNáDo

Die Teile 7 und 8, die nun fast zweieinhalb Jahre nach Teil 6 erscheinen, behandeln die statischen und dynamischen Relativbezüge in Oniguruma, sind also in der gerade freigegebenen Version Ruby 1.9.1 verfügbar. Die statischen Relativbezüge, die Teil 7 behandelt, wurden erst in der letzten Version von Oniguruma bereitgestellt, während die in Teil 8 behandelten dynamischen Relativbezüge schon länger existieren und von mir in eingeschränkter Form auch bei den Palindrom-Beispielen von Teil 5 berücksichtigt wurden.

Die vollständige Beschreibung der in Oniguruma vorhandenen Möglichkeiten für reguläre Ausdrücke befindet sich hier.

Etwas möchte ich noch anmerken. Die Beispiele dienen als erklärende Programme für die betrachteten regulären Ausdrücke. Wahrscheinlich lassen sich andere und einfachere Lösungen für die Aufgaben finden, die ohne die betrachteten Elemente und unter Einsatz anderer Möglichkeiten von Ruby funktionieren.

Genug der Vorrede - los gehts.

Statische und dynamische Relativbezüge in Oniguruma

Diese Bezeichnungsweisen stammen von mir, weil sie meiner Meinung nach die Fakten am besten wiederspiegeln. Unter statisch verstehe ich, dass allein anhand des regulären Ausdrucks erkannt werden kann, auf was sich der jeweilige Relativbezug bezieht, während dynamisch bedeutet, dass der Relativbezug sich erst während der Anwendung des regulären Ausdrucks ergibt

Hier nun die Bescheibung der statischen Relativbezüge direkt aus dem oben angeführten Oniguruma-Dokument.

8. Back reference
  ...
  \k<-n>      back reference by relative group number (n >= 1)
  \k'-n'      back reference by relative group number (n >= 1)
  ...
9. Subexp call ("Tanaka Akira special")
  ...
  \g<-n>      call by relative group number (n >= 1)
  \g'-n'      call by relative group number (n >= 1)

Die dynamischen Relativbezüge aus dem Oniguruma-Dokument, um die es sich in Teil 8 dieser Serie drehen wird, werden dort beschrieben.

Kleine Beispiele für statische Relativbezüge

Im ersten Beispiel suche ich am String-Anfang ein Wort, welches dann noch einmal im String vorkommen soll. Dafür setze ich zuerst einmal die übliche Methode ein, indem ich die erste Gruppe mittels \1 referenziere, also den durch die erste Gruppe eingefangenen Text noch einmal suche. Im zweiten (auch erfolgreichen Versuch) beziehe ich mich dann durch \k<-1> relativ auf die letzte Gruppe vor diesem Ausdruck.

1
2
3
s = "Otto bleibt Otto!"
puts s.match(/^(\w+).*\1/)[1]     # => Otto
puts s.match(/^(\w+).*\k<-1>/)[1] # => Otto

Welche Gruppe ist nun gemeint, wenn mehrere einfangende Gruppen ineinander geschachtelt vorkommen?

1
2
3
s = "Paul und Otto bleiben Paul und Otto!"
puts s.match(/(\w*(\w)\2\w*).*\1/)[1]         # => Otto
puts s.match(/(\w*(\w)\k<-1>\w*).*\k<-2>/)[1] # => Otto

Die Antwort lautet: Wie üblich zählen hier die öffnenden Klammern der jeweiligen Gruppen. Im Beispiel eben suchte ich ein Wort, welches zwei gleiche aufeinanderfolgende Zeichen enthält. Anschliessend suche ich dieses gefundene Wort noch einmal im String. Bei der üblichen Methode suche ich das gefundene Wort dann mittels \1, da die äussere Gruppe die erste Klammer im regulären Ausdruck enthält (es zählen nur die Klammern von einfangenden Gruppen). Im zweiten Fall stehen vor der Referenz \k<-2> zwei einfangende Gruppen, (\w*(\w)\k<-1>\w*) und (\w). Da ich den gefundenen Text der äusseren Gruppe benötige, deren öffnende Klammer als erste steht, muss ich mich also auf die vorletzte Gruppe beziehen (zwei Gruppen vorher). Auf die innere Gruppe beziehe ich mich im zweiten Fall auch relativ. Sie ist die letzte Gruppe vor dem Bezug, also \k<-1>.

So, nun noch ein Beispiel zum Aufruf einer Gruppe durch eine Relativangabe

1
2
3
s = "a1b23c456d9: Blabliblu"
puts s.match(/(.[0-9]+($|: |\g<1>))(.*)/)[3]  # => Blabliblu
puts s.match(/(.[0-9]+($|: |\g<-2>))(.*)/)[3] # => Blabliblu

Der reguläre Ausdruck ist im Detail fast uninteressant (wer Lust hat, kann ihn ja mal auseinandernehmen - ist auch eine gute Übung), wichtig sind hier nur die durch \g<1> und \g<-2> durchgeführten rekursiven Aufrufe. Der erste klassische Fall ist klar. Es wird einfach die erste Gruppe, die durch die erste öffnende Klammer definiert ist, aufgerufen. Da dies von innerhalb dieser Gruppe aus geschieht, ist es also ein rekursiver Aufruf (nur nebenbei - diese Konstruktionen lassen sich auch ohne Rekursion sinnvoll einsetzen). Im zweiten Fall benutze ich den relativen Aufruf, indem ich die vorletze Gruppe durch \g<-2> aufrufe - wie schon erwähnt müssen die öffenden Klammern zur Orientierung benutzt werden.

Wenn ich also beispielsweise durch einen Relativbezug rekursiv genau die Gruppe aufrufen möchte, in der sich der Aufruf befindet, geschieht das immer durch \g<-1>.

1
2
3
s = "1234abc"
puts s.match(/([0-9]\g<1>|)(.*)/)[2]  # => abc
puts s.match(/([0-9]\g<-1>|)(.*)/)[2] # => abc

Erwähnen muss ich hier leider, dass all diese Relativbezüge nur dann erlaubt sind, wenn in dem regulären Ausdruck keine benannte Gruppe vorkommt. Oniguruma erlaubt zwar diesbezüglich einige Parametrisierungen bei der Übersetzung, die jedoch in Ruby nicht vorgesehen sind (ich werde mal irgendwann auf Ruby-Core nachfragen, ob etwas gegen eine Änderung spricht).

Sind statische Relativbezüge überhaupt nützlich?

Auf den ersten Blick erscheinen diese Relativbezüge ziemlich unnütz - wenn ich sowieso abzählen muss, welche Gruppe innerhalb eines regulären Ausdrucks ich meine, dann kann ich auch gleich deren absolute Position angeben.

Nun gibt es aber in Ruby mittels der #{...}-Konstruktion die Möglichkeit Unterausdrücke in reguläre Ausdrücke einzufügen. Dann weiss ich natürlich nicht mehr die absolute Position einer einfangenden Gruppe, falls die eingefügten Unterausdrücke auch einfangende Gruppen besitzen. Hier kommen nun die Relativbezüge hilfreich ins Spiel. Der Rest dieses Beitrages zeigt an einem Beispiel, wie man erfolgreich vorgehen kann.

Rückblick: Ein vereinfachtes bal-Muster

In Teil 5 dieser Beitragsserie habe ich ein Muster für balancierte Klammerungen vorgestellt. Dieses Muster benötige ich jetzt wieder, allerdings reicht eine vereinfachte Form, die sich ausschliesslich um normale Klammern kümmert.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# encoding: Windows-1252
module Matchelements
  def bal()
    return "(?<bal>" +
              "[^()]*?" +
              "(?:\\(\\g<bal>\\)" +
                "[^()]*?" +
              ")*?" +
           ")"
  end
end
include Matchelements

orgstring= <<EOT
firstproc(p1,p2(p21,p22),p3(p31,p32(p321,p322),p33),p4)
nochneproc   (   einfach, nur    , ein, paar,   parameter   )
exotischeproc ( (((param))), (i*(k-5)) )
EOT

pattern1 = /(?<n>\w+)\s*(?<p>\(#{bal()}\))/
pattern2 = /[(,]\s*#{bal()}(?=\s*[,)])/

orgstring.scan(pattern1) do
  puts "\nEingabe: #{$~[0]}"
  newcall = $~[1] + '('
  params = [] # sonst hinter dem Block nicht mehr bekannt
  $~[2].scan(pattern2) do
    params << ($~[1] + '.test')
  end
  puts "Ausgabe: #{newcall + params.join(', ') + ')'}"
end

Das Ergebnis ist wie schon früher beschrieben, Erklärungen dazu finden sich im genannten Beitrag.

ruby191-p0 balmusterEinfach.rb

Eingabe: firstproc(p1,p2(p21,p22),p3(p31,p32(p321,p322),p33),p4)
Ausgabe: firstproc(p1.test, p2(p21,p22).test, p3(p31,p32(p321,p322),p33).test, p4.test)

Eingabe: nochneproc   (   einfach, nur    , ein, paar,   parameter   )
Ausgabe: nochneproc(einfach.test, nur.test, ein.test, paar.test, parameter.test)

Eingabe: exotischeproc ( (((param))), (i*(k-5)) )
Ausgabe: exotischeproc((((param))).test, (i*(k-5)).test)

Eine Anwendung. die nicht mehr geht

Dieses Muster hat jedoch einen Schönheitsfehler. Wenn ich in einem String mehrere balancierte Substrings finden will, muss ich #{bal} mehrfach im regulären Ausdruck benutzen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# encoding: Windows-1252
module Matchelements
  def bal()
    return "(?<bal>" +
              "[^()]*?" +
              "(?:\\(\\g<bal>\\)" +
                "[^()]*?" +
              ")*?" +
           ")"
  end
end
include Matchelements

orgstrings= [
              'firstproc(x1(33, r(3, 4)), k(3, kk(3, 4)), l(3), x2(99))',
              'secondproc(x1(99,5), l(77, m( n(44), 29), x2(15))',
              'thirdproc(x1(66), x2(88))',
              'fourthproc(x1(44), 1, 2, 3, x2(234))',
              'fifthproc(a(23, b(24, c(999))), x1(45,66), 23, p(9,9,9, q(3,3)), x2(3456), d(22))',
            ]
      
pattern = /\w+\((#{bal},)*x1\(#{bal}*\),(#{bal},){1,2}x2\(#{bal}*\)/
orgstrings.each do |s|
  if s.match(pattern)
    puts "      O.K.: '#{s}'"
  else
    puts "Nicht O.K.: '#{s}'"
  end
end

Das führt dann zu einem Fehler, weil die benannten Gruppen mehrfach definiert werden, bei einem Aufruf also nicht mehr eindeutig identifiziert werden können.

>ruby191-p0 balmusterDontWork.rb
balmusterDontWork.rb:22:in `
': multiplex definition name call: /\w+\(((?[^()]*?(?:\(\g\)[^()]*?)*?),)*x1\((?[^()]*?(?:\(\g\)[^()]*?)*?)*\), ((?[^()]*?(?:\(\g\)[^()]*?)*?),){1,2}x2\((?[^()]*?(?:\(\g\)[^()]*?)*?)*\)/ (RegexpError)

Will man anstatt benannter Gruppen die normalen einfangenden Gruppen nehmen und diese dann absolut aufrufen, geht das auch nicht so ohne weiteres, weil die Aufrufe dann immer nur eine Gruppe benutzen würden. Hier kann man sich am einfachsten durch Relativbezüge helfen.

Originalbeispiel mit Relativbezügen

Um Relativbezüge benutzen zu können, muss zuerst einmal die benannte Gruppe
(?< bal>...) durch eine einfache einfangende Gruppe ersetzt werden. Anschliessend wird noch der Aufruf der benannten Gruppe \\g< bal> (die doppelten Backslashes sind nötig, da der Ausdruck in einen String geschrieben wird) durch den relativen Aufruf \\g<-1> ersetzt. Hierbei muss beachtet werden, dass sich der Aufruf selber in einer nicht-einfangenden Gruppe (?:...) befindet, deren öffnende Klammer beim Relativbezug nicht beachtet wird. Das Ergebnis sieht dann so aus:

1
2
3
4
5
6
7
8
9
10
11
12
# encoding: Windows-1252
module Matchelements
  def bal()
    return "(" +
              "[^()]*?" +
              "(?:\\(\\g<-1>\\)" +
                "[^()]*?" +
              ")*?" +
           ")"
  end
end
...

Funktionierende Version mit Relativbezügen

Die eben erstellte Variante des bal-Musters kann nun mehrfach in einem regulären Ausdruck benutzt werden. Im Beispiel suche ich nach Funktionsaufrufen, als deren Parameter die Funktionen x1 und x2 aufgerufen werden, wobei zwischen ihnen noch ein oder zwei andere Parameter vorkommen müssen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# encoding: Windows-1252
module Matchelements
  def bal()
    return "(" +
              "[^()]*?" +
              "(?:\\(\\g<-1>\\)" +
                "[^()]*?" +
              ")*?" +
              ")"
  end
end
include Matchelements

orgstrings= [
              'firstproc(x1(33, r(3, 4)), k(3, kk(3, 4)), l(3), x2(99))', # (x1, ., ., x2)
              'secondproc(x1(99,5), l(77, m( n(44), 29)), x2(15))',       # (x1, ., x2)
              'thirdproc(x1(66), x2(88))',                                # (x1, x2)
              'fourthproc(x1(44), 1, 2, 3, x2(234))'                      # (x1, ., ., ., x2)
            ]
      
pattern = /\w+\(x1\(#{bal}*\),(?>#{bal},){1,2} x2\(#{bal}*\)/
orgstrings.each do |s|
  if s.match(pattern)
    puts "      O.K.: '#{s}'"
  else
    puts "Nicht O.K.: '#{s}'"
  end
end

Das Ergebnis entspricht genau den Wünschen.

>ruby191-p0 balmusterWorks.rb
      O.K.: 'firstproc(x1(33, r(3, 4)), k(3, kk(3, 4)), l(3), x2(99))'
      O.K.: 'secondproc(x1(99,5), l(77, m( n(44), 29)), x2(15))'
Nicht O.K.: 'thirdproc(x1(66), x2(88))'
Nicht O.K.: 'fourthproc(x1(44), 1, 2, 3, x2(234))'

Auf das Muster selber will ich nicht eingehen, weil es sicherlich eine gute Übung ist, es zu analysieren. Ich stehe bei Fragen dazu über Kommentare jederzeit zur Verfügung.


Kommentar schreiben

Name (notwendig)

Mail (wird nicht veröffentlicht)

Webseite


Kommentare

  1. WoNáDo schrieb am 08.02.2009 (00 Uhr)

    Habe noch einige Kleinigkeiten korrigiert.