Tutorial: Reguläre Ausdrücke in Ruby einfach erklärt

Winfried Mueller, www.reintechnisch.de, Start: 09.05.05, Stand: 27.06.05

Reguläre Ausdrücke sind wahre Zaubersprüche. Es gibt in der Softwareentwicklung kaum etwas, was in so kompakter Form so mächtig ist. Mit einer kurzen Zeile von Buchstaben und Sonderzeichen kann man Dinge vollbringen, wofür man sonst hunderte Zeilen Code bräuchte.

Wer reguläre Ausdrücke grundlegend verstanden hat, wird fasziniert sein von den Möglichkeiten. Andererseits eignen sich diese auch gut, um Programmieranfänger durch kryptische Zeichenfolgen vollends abzuschrecken. Selbst Profis können sich durchaus ein paar Stunden mit einer komplizierten RegExp rumschlagen, bis sie sie verstanden haben.

Bei regulären Ausdrücken hat man es geschafft, nur durch die entsprechende Aneinanderreihung von Zeichen eine so komplexe Sache zu kreieren, dass man zum Verständnis ganze Bücher füllen kann.

Reguläre Ausdrücke lernt man also nicht an einem Tag. Viel Übung gehört dazu, um sie als tägliches Handwerkszeug nutzen zu können.

Und doch glaube ich, dass ein Einstieg eigentlich recht einfach sein könnte, wenn es eine vernünftige Einführung gäbe. Für die Programmiersprache Ruby möchte ich das hiermit versuchen. Diese Einführung ist jedoch nicht auf Ruby beschränkt, viele andere Werkzeuge nutzen reguläre Ausdrücke und die Handhabung ist ganz ähnlich.

Bei der Erstellung dieser Dokumentation stand im Vordergrund: Wie beschreibe ich die komplexe Materie möglichst einfach. So einfach, dass ich all die erreichen kann, die bisher einen großen Bogen um reguläre Ausdrücke gemacht haben.

Aus diesem Grund habe ich mich auch auf die wichtigsten Möglichkeiten beschränkt. Dies schafft eine Basis, mit der man loslegen kann - hinausziehen in die Welt der regulären Ausdrücke, um selbst seine Erfahrungen zu machen.

Bei der Durcharbeitung dieses Regexp-Tutorials sollte man ein installiertes Ruby und einen Texteditor bereithalten. Jedes Codebeispiel lässt sich schnell in eine Textdatei kopieren und auf der Kommandozeile ausführen. So lernt man am Beispiel und kann weitere Experimente machen. Kann sich spielerisch der Materie nähern.

Reguläre Ausdrücke = Mustererkennung

Wozu braucht man eigentlich reguläre Ausdrücke? Man braucht sie, um Textmuster zu erkennen. Man glaubt gar nicht, wie oft einem in der Softwareentwicklung diese Aufgabe begegnet. Das geht schon los, wenn jemand ein Datum in ein Feld einträgt und wir prüfen wollen, ob es ein korrektes Format hat.

Das Wort Format sagt es schon: Da soll irgendwas in einer bestimmten Form sein - da haben wir's: Ein Muster. Als nächstes haben wir vielleicht eine Text-Datei, vielleicht ein Logfile. Auf jeder Zeile finden wir Uhrzeit und Datum. Hier wollen wir dieses Muster finden, um daraus ein Datumsobjekt anzulegen.

Jeder Interpreter oder Compiler einer Programmiersprache muss den Quelltext parsen. So nennt man den Prozess der Interpretation des Programmtextes. Der Parser muss nach Schlüsselwörtern, Zahlen, Ausdrücken, Operatoren usw. suchen - auch das sind wieder Textmuster. Vielleicht werden wir selber keinen Interpreter schreiben wollen, vielleicht jedoch eine Konfigurationsdatei, die ebenfalls eine Struktur hat, die wir parsen müssen.

In einer HTML-Datei will man vielleicht bestimmte Abschnitte automatisch durch bestimmte Inhalte ersetzen. Auch hier muss man anhand bestimmter Textmuster Stellen finden, die dann ersetzt werden. Man spricht dann oft von Template basierten Systemen - aus einer generischen HTML-Vorlage wird eine konkrete HTML-Datei erzeugt.

Konkrete Muster

Beginnen wir mit dem einfachsten Fall. Wir suchen ein konkretes Muster, also eine Aneinanderreihung festgelegter Zeichen, wie z.B. "Hello", "Fred", "Ja", "Nein". Hierzu basteln wir ein kleines Ruby Programm, welches Eingaben entgegennimmt und dann einen Mustervergleich durchführt.

 
loop do
  print "Eingabe (Ja/Nein/exit): "
  eingabe = gets.chomp

  if eingabe =~ /Ja/
   puts "Sie haben Ja eingegeben."
  elsif eingabe =~ /Nein/
   puts "Sie haben Nein eingegeben."
  elsif eingabe =~ %r(exit)
   puts "Verlasse das Programm..."
   break
  end     
end

Reguläre Ausdrücke werden immer zwischen die Begrenzer /<regexp>/ geschrieben, in etwa genauso, wie man für einen String die Anführungsstriche benutzt. Der Slash markiert einfach den Anfang und das Ende des Ausdrucks. Wahlweise kann man in Ruby auch %r(<regexp>) verwenden, wobei man statt der Klammer auch andere Sonderzeichen verwenden darf, also z.B. %r!<regexp>! oder %r{<regexp>}. Soviel nur am Rande.

Will man einen String mit einem Muster vergleichen, nimmt man nicht den == Operator sondern den =~ Operator. Das ist auch einfach eine Festlegung; Ruby hat das von Perl übernommen. Vergleiche mit einem Muster haben also immer die Form "string =~ regexp". Der Vergleich gibt true zurück, wenn der String dem Muster entspricht, ansonsten false.

Geben wir in diesem Beispiel "Ja" ein, wird der erste if-Pfad gewählt und der Text dort ausgegeben. Bei "Nein" wird der zweite Pfad durchlaufen und bei Eingabe von "exit" wird die Schleife mit break verlassen.

So ganz toll ist das noch nicht, weil z.B. ein klein geschriebenes "ja" nicht erkannt wird, was hier aber sinnvoll wäre. Hierzu modifizieren wir das Programm:

 
loop do
  print "Eingabe (Ja/Nein/exit): "
  eingabe = gets.chomp

  if eingabe =~ /Ja/i
   puts "Sie haben Ja eingegeben."
  elsif eingabe =~ /Nein/i
   puts "Sie haben Nein eingegeben."
  elsif eingabe =~ %r(exit)i
   puts "Verlasse das Programm..."
   break
  end     
end

 

Lediglich ein kleines "i" hinter dem regulären Ausdruck verändert die Situation. Ein sogenannter Modifier, der den regulären Ausdruck anweist, sich nicht um Groß- und Kleinschreibung zu kümmern. Case-Intensitive nennt sich das oder Ignore Case. Es gibt noch weitere Modifier, die immer aus einem Buchstaben bestehen, dazu später.

Jetzt funktioniert also auch "JA" oder "jA" oder "ExIT". Wer ein wenig mit rumspielt, wird vielleicht auch festgestellt haben, dass "xyzJa" funktioniert, genauso wie "Nein Abcd" oder "BlaBlaExit". Der reguläre Ausdruck bedeutet nämlich nichts weiter als: "Finde im String das Muster Ja". Wenn also irgendwo ein "Ja" auftaucht, gilt das als gefunden.

Für unser Beispiel ist das nicht so gut. Was wir brauchen, sind Anker.

Anker

Beginnen wir mit dem Zeilenanfang-Anker:

 
loop do
  print "Eingabe (Ja/Nein/exit): "
  eingabe = gets.chomp

  if eingabe =~ /^Ja/i
   puts "Sie haben Ja eingegeben."
  elsif eingabe =~ /^Nein/i
   puts "Sie haben Nein eingegeben."
  elsif eingabe =~ %r(^exit)i
   puts "Verlasse das Programm..."
   break
  end     
end

 

Dieses Programm akzeptiert nun kein "blaJa" mehr und auch kein "vielleicht Nein". Das "^"-Zeichen am Anfang des regulären Ausdrucks legt fest, dass das folgende Muster am Stringanfang beginnen muss. Und auch der Ausdruck "\A" würde in diesem Fall das gleiche bewirken:

 
 if eingabe =~ /\AJa/i
   puts "Sie haben Ja eingegeben."
   ...

Die Unterschiede zwischen \A und ^ werde ich später noch erläutern. Wichtig ist hier, dass in regulären Ausdrücken oft von \-Steuersequenzen Gebrauch gemacht wird. \A soll ja nicht die Zeichen \ und A finden, sondern hat eine Sonderbedeutung. Der Backslash leitet dabei die Sonderbedeutung ein, immer gefolgt von einem Zeichen.

Unser regulärer Ausdruck heißt jetzt also nicht mehr: "Finde irgendwo ein Ja." sondern "Finde ein Ja am Zeilenanfang.". Damit ist es allerdings immer noch möglich "neinBlaBla" zu schreiben.

Dafür brauchen wir jetzt den Zeilenende-Anker:

 
loop do
  print "Eingabe (Ja/Nein/exit): "
  eingabe = gets.chomp

  if eingabe =~ /^Ja$/i
   puts "Sie haben Ja eingegeben."
  elsif eingabe =~ /^Nein$/i
   puts "Sie haben Nein eingegeben."
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end

 

Das $-Zeichen ist der Zeilenende-Anker. Jetzt haben wir also ein Muster festgelegt, was so funktioniert: "Finde einen String, der nur aus dem Wort Ja besteht, egal ob groß oder klein geschrieben."

Das ist im Grunde genau das, was wir in dieser Anwendung brauchen.

Alternativen

Nehmen wir einmal an, wir wollen eine Benutzereingabe validieren, also auf Gültigkeit prüfen. Der Benutzer darf einen Wochentag eingeben und wir müssen überprüfen, ob er wirklich Montag, Dienstag usw. eingegeben hat. Wir könnten das mit bisherigen Mitteln schon tun, in dem wir 7 reguläre Ausdrücke definieren, für jeden Wochentag einen. Es geht aber auch mit einem einzigen Ausdruck über Alternativen.

 
loop do
  print "Eingabe Wochentag: "
  eingabe = gets.chomp

  if eingabe =~ 
     /^(Montag|Dienstag|Mittwoch|Donnerstag|Freitag|Samstag|Sonntag)$/i
    puts "Sie haben einen gueltigen Wochentag eingegeben: " + $&
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end
 

Den senkrechten Balken "|" kann man als "oder" lesen: Montag oder Dienstag... In den meisten Fällen ist es sinnvoll, die Alternativen zu klammern, wie wir es hier getan haben.

Jetzt haben wir auch den Punkt erreicht, wo wir nicht mehr von einem konkreten Muster sprechen können. Wenn der reguläre Ausdruck nämlich im String gefunden wird, wissen wir nicht, was er denn nun wirklich gefunden hat, ob Montag, Dienstag usw. Wir haben ein abstraktes Muster geschrieben, was auf eine Reihe von konkreten Ausprägungen passt. Genaugenommen haben wir das oben aber auch schon, weil "ja" und "JA" und "jA" auch verschiedene Sachen sind.

Ruby liefert uns eine globale Variable, die immer den String enthält, der bei dem letzten Vergleich auf den regulären Ausdruck passte. Dies ist die Variable "$&", die wir in die puts Ausgabe mit einbezogen haben. Wenn wir jetzt z.B. "FrEItag" eintippen, dann steht in $& tatsächlich genau dieser String in dieser Schreibweise und nicht etwa "Freitag".

Über Alternativen könnten wir jetzt tatsächlich auch eine Ziffer in einem String finden:

 
loop do
  print "Eingabe eines Satzes mit einer Ziffer: "
  eingabe = gets.chomp

  if eingabe =~ /(0|1|2|3|4|5|6|7|8|9)/
    puts "Die erste Ziffer im Satz lautet: " + $&
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end  

Egal, was man auch immer eintippt, sobald auch nur eine Ziffer im Text ist, wird die auch gefunden und ausgegeben. Natürlich könnten wir diesen Ausdruck auch noch einmal dahinterhängen und dann alles finden, wo zwei Ziffern auftauchen.

 
loop do
  print "Eingabe eines Satzes mit mindestens 2 Ziffern: "
  eingabe = gets.chomp

  if eingabe =~ /(0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)/
    puts "Die erste Ziffer im Satz lautet: " + $&
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end  

Interessant wäre nun auch noch, wenn wir auf die erste und zweite Ziffer getrennt zugreifen könnten. Und das geht tatsächlich. Wenn wir nämlich mit Klammern arbeiten, dann haben wir auch Teilausdrücke. Man kann also sagen, gib mir den String zurück, der auf das erste Klammernpaar zutrifft und dann den, der auf das zweite zutrifft. Auch hierfür gibt es wieder globale Variablen: $1..$n.

 
loop do
  print "Eingabe eines Satzes mit mindestens 2 Ziffern: "
  eingabe = gets.chomp

  if eingabe =~ /(0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)/
    puts "Die erste Ziffer lautet: " + $1
    puts "Die zweite Ziffer lautet: " + $2
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end  

Mitunter interessiert auch der Teilstring, der vor einem Muster oder nach einem Muster steht. Auf diese haben wir ebenfalls Zugriff, über die globalen Variablen $` und $'.

 
loop do
  print "Eingabe eines Satzes mit mindestens 2 Ziffern: "
  eingabe = gets.chomp

  if eingabe =~ /(0|1|2|3|4|5|6|7|8|9)(0|1|2|3|4|5|6|7|8|9)/
    puts "String vor Muster: " + $`
    puts "Die erste Ziffer lautet: " + $1
    puts "Die zweite Ziffer lautet: " + $2
    puts "String nach Muster: " + $'
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end   

Wer sich jetzt fragt, wer denn auf so kryptische Variablennamen gekommen ist, der muss ein Blick in die Vergangenheit werfen. In Shell-Skripts unter unixoiden Systemen wurde sehr gerne ein absolut minimalistischer Stil gepflegt. Die Programmierer von damals waren Assembler und C gewöhnt, Sprachen, die auch keine Zeichen unnötig verschenken. Mitunter war es auch ein großer Ehrgeiz nach dem Motto "Wer schreibt den kompaktesten Code." Und da sind Variablennamen wie $match viel zu lang und was für Warmduscher. Ein $& ist dagegen perfekt: Kurz und kryptisch. Ein Stück Herausforderung für alle, die das verstehen wollen. Und um Herausforderung und "Wer ist der Bessere"-Spielchen geht es oft im Programmierer-Lager. Seltener darum, eine Sache so einfach und verständlich wie möglich zu machen. Ruby hat nun von Perl geerbt und Perl hat von Shellskript geerbt. Das hat Vorteile, weil das eine gewisse Konsistenz bringt. Wer in beiden Sprachen programmiert, wird dankbar sein, dass es solche Ähnlichkeiten gibt.

So ganz optimal ist die Sache mit den Alternativen für Zahlen allerdings nicht. Es führt zu langen Ausdrücken. Wenn man für ein Zeichen A-Z zulassen wollte, wären dass ganze 26 Alternativen, die Kleinbuchstaben noch nicht mit eingerechnet. Es muss bessere Wege geben, sowas zu lösen. Und in der Tat, es gibt sie: Die Zeichenklassen.

Zeichenklassen

Mit Zeichenklassen bewegen wir uns ein Stück mehr hin zu regulären Ausdrücken, die nicht nur auf was Konkretes passen sondern auf eine Menge von konkreten Ausprägungen. Die Muster werden also immer abstrakter, es gibt immer mehr konkrete Ausprägungen, die auf ein Muster passen.

Mit einer Zeichenklasse kann man festlegen, welche Werte ein bestimmtes Zeichen im String annehmen darf. Um das zu demonstrieren, nehmen wir ein Beispiel, bei dem wir wieder ein Feld validieren. Diesmal darf der Benutzer nur eine zweistellige Zahl eintippen.

 
loop do
  print "Eingabe einer zweistelligen Zahl: "
  eingabe = gets.chomp

  if eingabe =~ /^[0123456789][0123456789]$/
    puts "Die eingegeben Zahl lautet: " + $&
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end  

Eine Zeichenklasse wird mit den eckigen Klammern eingeschlossen. Sie legt fest, welche Zeichen an einer bestimmten Stelle im String erlaubt sein sollen. Eine Zeichenklasse steht immer für ein Zeichen im String. Wollten wir nur den String "Hallo" mit Zeichenklassen suchen, könnte man auch schreiben /[H][a][l][l][o]/. Wenn der erste Buchstabe sowohl klein wie auch groß sein dürfte, hätten wir geschrieben /[Hh][a][l][l][o]/.

Bei dem Zahlenbeispiel sagen wir also, dass das erste Zeichen im String aus den Zeichen 0-9 bestehen muss und das zweite Zeichen ebenso. Ein weiteres Beispiel soll das nochmal verdeutlichen:

 
loop do
  print "Eingabe eines Satzes: "
  eingabe = gets.chomp

  if eingabe =~ /[xyz][bcd][01234]/
    puts "Treffer."
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end  

Wenn wir das Muster treffen wollen, dann wäre eine gültige Eingabe z.B. "xb0" oder "zd4", nicht jedoch "ab4".

Zurück zum Zahlenbeispiel. Immer alle Zeichen einzutippen, die Gültigkeit haben sollen, ist nervig. Man kann sich das auch vereinfachen.

 
loop do
  print "Eingabe einer zweistelligen Zahl: "
  eingabe = gets.chomp

  if eingabe =~ /^[0-9][0-9]$/
    puts "Die eingegeben Zahl lautet: " + $&
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end  

Hierbei wird also nur das erste und letzte Zeichen angegeben, alles dazwischen wird automatisch eingeschlossen. Genauso kann man [a-z] oder [a-zA-Z] schreiben, um alle Buchstaben einzuschließen. Umlaute müssen jedoch extra angegeben werden. Wenn man mal in einer ASCII-Tabelle nachschaut, wird klar, dass die so nicht eingeschlossen sind.

Nach Zahlen oder Wortzeichen zu suchen ist so etwas übliches, dass man dafür noch weitere Verkürzungen erfunden hat. Jetzt sind wir wieder bei den \-Ausdrücken gelandet, wie oben schonmal bei \A. Für Zahlen gibt es \d, für Wortzeichen \w.

 
loop do
  print "Eingabe einer zweistelligen Zahl: "
  eingabe = gets.chomp

  if eingabe =~ /^\d\d$/
    puts "Die eingegeben Zahl lautet: " + $&
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end  

Bei Zeichenklassen gibt es auch die negierte Möglichkeit: Finde alles, wo diese Zeichen nicht drin vorkommen.

 
loop do
  print "Eingabe: "
  eingabe = gets.chomp

  if eingabe =~ /^[^0-9][^0-9]$/
    puts "Der String lautet: " + $&
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end  

Dieses Programm findet also einen String mit genau 2 Zeichen, die aber keine Zahlen sein dürfen, also z.B. "az" oder "ab" oder "pq", nicht aber "09" oder "75". Mit dem "^"-Zeichen direkt hinter der öffnenden Klammer negiert man also die Zeichenklasse, kehrt deren Bedeutung um.

Hier mal eine Auflistung der Kurzausdrücke mit den Entsprechungen:

 
\d  entspricht  [0-9]
\D  entspricht  [^0-9]
\w  entspricht  [a-zA-Z]
\W  entspricht  [^a-zA-Z]
\s  entspricht  Space, Tab, \n, \r, \f  (auch whitespace genannt) 
\S  entspricht  alles, außer whitespaces
.   entspricht  ein beliebiges Zeichen außer \n (newline)

Für manche etwas unerwartet, gehört auch \n zu den Whitespaces. In der Praxis ist das oft günstig, wenn man mehrzeilige Strings bearbeitet.

Leider schliest \w die Umlaute nicht mit ein, was man bei der Verarbeitung von deutschen Texten beachten sollte. Der Punkt dagegen schließt sämtliche Zeichen ein. Möchte man also z.B. testen, ob eine Eingabe genau 5 Zeichen lang ist, funktioniert folgender Code.

 
loop do
  print "Eingabe 5 Zeichen: "
  eingabe = gets.chomp

  if eingabe =~ /^.....$/
    puts "Treffer!"
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end  

Mittlerweile können wir schon eine ganze Menge mit regulären Ausdrücken machen. Und auch schon recht kryptischen Code damit schreiben. Wir haben uns aber gerade fast unbemerkt ein Problem eingehandelt. Wie suche ich z.B. nach der Zeichenfolge "\A"? Der Backslash hat ja die Sonderbedeutung und ich kann nicht einfach /\A/ benutzen, weil dies für Zeilenanfang steht.

Zeichen, die eine Sonderbedeutung haben, und das sind im Moment der Backslash, das "^", das "|", die eckigen und runden Klammern und das $-Zeichen, kann man maskieren. Maskieren heißt, die Sonderbedeutung aufheben. Das tut man, in dem man einen Backslash voranstellt. Unser "\A" finden wir also so:

 
loop do
  print "Eingabe: "
  eingabe = gets.chomp

  if eingabe =~ /^\\A$/
    puts "Treffer!"
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end  

Wenn wir also reguläre Ausdrücke schreiben, müssen wir immer auf Zeichen acht geben, die eine Sonderbedeutung haben. Diese müssen mit einem Backslash maskiert werden, damit die ursprüngliche Bedeutung wieder hergestellt wird.

Wir haben übrigens ein weiteres Zeichen, was wir maskieren können müssen: Den Bindestrich bzw. Minuszeichen in Zeichenklassen. Das hat ja die Sonderbedeutung, Bereiche darüber angeben zu können (z.B. [0-9]). Die Zeichenklasse [\-+] findet dann z.B. die Zeichen + und -, wobei der Backslash das Minuszeichen von der Sonderbedeutung befreit.

Hier nochmal alle Zeichen, die eine Sonderbedeutung haben:

 
( ) [ ] { } . - + * |  $  ? \ ^ 

Manche dieser Zeichen sind nur außerhalb von Zeichenklassen von Bedeutung, andere auch innerhalb. Generell gilt immer, auf diese Sonderzeichen zu achten, wenn sie so direkt im Suchmuster auftauchen sollen. Bei vielen schadet auch eine Maskierung nichts, selbst wenn sie im Kontext nicht nötig ist.

Wir treiben es auf die Spitze, und wollen einen String finden, der so aussieht: "|()[]{}???"

 
loop do
  print "Eingabe: "
  eingabe = gets.chomp

  if eingabe =~ /^\|\(\)\[\]\{\}\?\?\?$/
    puts "Treffer!"
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end  

Spätestens an diesem Punkt sieht man, dass reguläre Ausdrücke einerseits eine total kryptische Angelegenheit sein können, dass man sie andererseits aber auch begreifen kann, die Sache gar nicht so wild ist, wie sie aussieht.

Nun kommt es ja selten vor, dass man Zahlen mit einer festen Länge hat. Unsere nächste Aufgabe wird also sein: "Finde in einem Satz eine beliebig lange Zahl." So etwas können wir mit bisherigen Mitteln nicht, was uns fehlt sind die Quantifier.

Quantifier

Wer mal auf einer Kommandozeile, ob nun Dos oder Unix gearbeitet hat, kennt die sogenannten Jokerzeichen. Ein *.txt steht für alle Textdateien, wobei der Stern nicht nur für ein beliebiges Zeichen sondern für beliebig viele beliebige Zeichen steht. Unter Unix heißt das auch Globbing.

So ähnlich ist das mit Quantifier-Zeichen. Diese geben an, dass ein bestimmtes Zeichen 0-n mal bzw. 1-n mal auftauchen soll.

 
loop do
  print "Eingabe Satz mit einer Zahl: "
  eingabe = gets.chomp

  if eingabe =~ /[0-9]+/
    puts "Die Zahl lautet: " + $&
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end  

Das Pluszeichen ist ein Quantifier. Es legt fest, dass das vorherige Zeichen 1-n mal auftauchen darf. Ein /b+/ würde also sowohl "b" finden wie auch "bb" oder "bbbbb".

In unserem Beispielcode wird jetzt ein String gefunden, der aus beliebig vielen Ziffern bestehen kann. Diese Zahl darf irgendwo im String auftauchen. Geben wir also "Die Zahl lautet 39394" ein, so wird "39394" gefunden.

Quantifier sind mächtig und man braucht sie oft. Hier ein Beispiel, welches das erste und dritte Wort aus einem Text herausholt. (ohne Umlaute)

 
loop do
  print "Eingabe eines Satzes: "
  eingabe = gets.chomp

  if eingabe =~ /(\w+)\s+\w+\s+(\w+)/
    puts "Erstes Wort: " + $1
    puts "Drittes Wort: " + $2
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end  

Wir suchen hier mit (\w+) nach einem ersten Wort, klammern es, damit wir auf diesen Teilausdruck später zugreifen können. Danach erlauben wir beliebig viele Leerzeichen, mindestens jedoch eins. Dann das nächste Wort, wieder beliebig viele Leerzeichen (auch andere Whitespaces) und dann ein Wort, welches wir wieder klammern, um auf das Teilmuster mit $2 zugreifen zu können.

Im Grunde werden reguläre Ausdrücke erst über die Quantifier richtig interessant, weil man nun eine enorme Flexibilität erreicht hat.

Neben dem + als Quantifier gibt es noch den *, der 0-n mal das vorherige Zeichen findet. Er hat also eine andere Bedeutung, wie der Stern als Dateinamen Joker. Man sollte das nicht verwechseln.

Das Fragezeichen, steht für 0-1 mal. Der Ausdruck /Matt?hias/ würde sowohl "Mathias" wie auch "Matthias" finden.

Quantifier sind gefräßig, sagt man. Was das bedeutet zeigt folgendes Beispiel. Wir könnten nämlich auf die Idee kommen, ein erstes Wort so zu finden:

 
loop do
  print "Eingabe eines Satzes: "
  eingabe = gets.chomp

  if eingabe =~ /^(.+)\s/
    puts "Erstes Wort: " + $1
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end  

Hört sich logisch an: Der Punkt steht für "Jedes Zeichen". Dies beliebig oft bis zum ersten whitespace. Hinter dem ersten Wort ist ein Whitespace, sollte also klappen. Wenn wir das jetzt mit diesem Satz testen: "Das ist ein Testsatz", bekommen wir für das erste Wort jedoch "Das ist ein" zurück.

Das zeigt die Gefräßigkeit. Es wird nämlich nicht nach dem kürzesten String gesucht, der auf das Muster passt, was der String "Das " wäre. Es wird vielmehr der längste String gesucht, der auf das Muster passt, und das ist nunmal "Das ist ein ".

Mitunter ist dieses Verhalten gut und gewollt, manchmal möchte man das aber auch nicht. Ein typisches Beispiel wäre eine Datendatei, die aus Zeilen in diesem Format aufgebaut ist:

 
Meier:Heiner:0190-999999:nospam@sonstwie.tld

In Ruby würde man sowas natürlich mit split zerlegen, hier machen wir das aber mal mit einer regulären Expression.

 
line = "Meier:Heiner:0190-999999:nospam@sonstwie.tld"

if line =~ /(.*?):(.*?):(.*?):(.*)/
  puts "Zeile erkannt"
  puts "Name: " + $1
  puts "Mail: " + $4
else
  puts "Fehler in Zeile"
end 

Mit der gefräßigen Version der Quantifier hätte das nicht geklappt. Das Fragezeichen hinter einem + oder * Quantifier macht dagegen aus diesen genügsame Quantifier, die das kleinste passende Muster finden. Folgt also auf + oder * direkt ein Fragezeichen, dann hat dieses eine Sonderbedeutung, es bedeutet nicht 0..1 mal vorheriges Zeichen, was ja auch Unsinn wäre. Der erste (.*?) Ausdruck ißt den String also nur bis zum ersten Doppelpunkt und hört dann auf. Der nächste ißt den String weiter bis zum nächsten Doppelpunkt usw. Nur der letzte darf den kompletten Rest essen.

Perl hat reguläre Ausdrücke ein ganzes Stück erweitert und Ruby hat diese Erweiterungen größtenteils übernommen. Hier wurde ein weiterer Quantifier eingeführt. Bei diesem kann man eine feste Zahl an Wiederholungen festlegen.

 
str = "192.243.125.7"
puts str
if str =~ /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/
  puts "Ist eine IP-Adresse!"
  puts "3: " + $1
  puts "2: " + $2
  puts "1: " + $3
  puts "0: " + $4
end


Der Quantifier {1,3} legt fest, dass das vorherige Zeichen mindestens einmal, maximal aber 3 mal vorkommen darf. Damit stellen wir bei einer Formatprüfung einer IP-Adresse zumindest schonmal sicher, dass wir 4 durch . separierte Zahlen haben, die maximal 3 stellig sind. Natürlich müsste man weitere Validierungen vornehmen, weil die Zahl jeweils nicht größer als 255 werden kann. Diese Validierung über einen regulären Ausdruck zu lösen, ist durchaus möglich, sinnvoller ist wohl aber, die jeweiligen Zahlen in Integer zu wandeln und dann auf Größe zu testen.

Die Anwendung dieses Quantifiers kann auf mehrere Arten geschehen:

 
re{m,n}  mindestens m, maximal n
re{m}    genau m mal
re{m,}   mindestens m mal

Interessant wird es nun nochmal, wenn man Quantifier auf geklammerte Ausdrücke anwendet. Denn dann zählt nicht das Zeichen vor dem Quantifier sondern der ganze Ausdruck.

 
loop do
  print "Eingabe: "
  eingabe = gets.chomp

  if eingabe =~ /^(Ja){2,5}$/
    puts "Treffer"
  elsif eingabe =~ %r(^exit$)i
   puts "Verlasse das Programm..."
   break
  end     
end  

Im Beispiel wäre "Ja" nicht ok, dagegen wäre "JaJa" ein Treffer, wie auch "JaJaJa". Über 5 "Ja" hintereinander wäre dann wieder außerhalb des Musters. Wer also zuviel "JaJaJaJaJa" sagt, fliegt raus.

Für die, denen das noch nicht reicht, möchte ich verraten, dass man Klammern auch verschachteln kann. Das schafft wiederum einige neue Möglichkeiten.

An diesem Punkt soll es erstmal gut sein. Wir haben alle grundsätzlichen Bereiche regulärer Ausdrücke durchwandert. Mit diesem Wissen im Gepäck, lassen sich viele Fälle des Alltags bewältigen. Es ist ein gutes Basiswissen, mit dem man nun seine praktischen Erfahrungen machen kann.

Trotzdem: Ein paar nette Sachen gibt es noch, die ich nicht vorenthalten möchte.

Ergänzungen

Neben dem Modifier i, den wir ganz zu Anfang besprochen haben, gibt es noch den vielbenutzten Modifier m, der für Multiline Mode steht. Hiermit lassen sich Strings besser auswerten, die Zeilenumbrüche enthalten, weil die Zeichenklasse "." nun auch Zeilenumbrüche mit einschliest. Der String "Das\nist\nein\nTest\n" könnte z.B. mit /Das.*Test/m gefunden werden. Der Modifier m scheint aber nur Auswirkung auf die Interpretation der Zeichenklasse "." zu haben, unabhängig davon machen reguläre Ausdrücke nicht am Zeilenende halt. Dies wird gerne falsch gedeutet.

Bei Strings, die über mehrere Zeilen gehen, die also Zeilenumbrüche enthalten, ergibt sich ein Unterschied zwischen den Ankern "^" und \A sowie "$" und \z. Erstere Version findet immer den Zeilenanfang, während \A und \z den Stringanfang und das Stringende bedeuten.

Es gibt einen weiteren Anker: \b findet Wortgrenzen. \B findet alles, was keine Wortgrenze ist.

Mit dem Modifier u kann man UTF-8 kompatible reguläre Ausdrücke unter Ruby einstellen.

Der Modifier x schaltet erweiterte reguläre Ausdrücke ein. Diese Erweiterungen gibt es sowohl in Ruby, wie in Perl. Sie schaffen zusätzliche Möglichkeiten für Fortgeschrittene. Ich möchte hier nicht tiefer darauf eingehen.

Zusammenspiel Ruby und reguläre Ausdrücke

Reguläre Ausdrücke sind ein Basiskonzept von Ruby. Deshalb sind sie stark mit der Sprache verwoben, man begegnet ihnen in vielen Sprach-Konstellationen.

Basiswissen

Eine Einfache Form haben wir schon kennengelernt: Der Vergleich mit einem regulären Ausdruck:

 
 str = "Hello"
 if str =~ /ll/
   puts "str enthaelt: #{$&}"
 end
 

Jeder reguläre Vergleich setzt eine Reihe von globalen Variablen. Bei der Programmierung kann das mitunter zur Falle werden, wenn man versehentlich durch eine zweiten regulären Vergleich die Variablen wieder neu setzt.

 
def myaction( str )
  if str =~ /(x)(x)/
    puts $1  
    #...
  end
end

str = "abcdxx"
if str =~ /(ab)(cd)xx/
  myaction( $& )  # >> verändert $&, $1, $2 ..
  puts $1          # >> Fehler, weil falsches $1. Oder doch nicht?
end

Interessant ist dieses Beispiel, weil man eigentlich erwarten würde, dass dies fehlerhaft implementiert ist. Man möchte auf $1 des ersten regulären Vergleiches zugreifen, nachdem man myaction aufgerufen hat, welches aber seinerseits einen eigenen Vergleich durchführt. Damit verändert es $1. In Perl würde das auch so passieren.

Das Beispiel funktioniert aber merkwürdigerweise in Ruby korrekt. Das liegt daran, dass $1, $& usw. in Wirklichkeit keine globalen Variablen sind. Jeder aktuelle Gültigkeitsbereich (Scope) hat hier Eigene. Das ist in nahezu allen Fällen ein vernünftige Implementierung und vermeidet Fehler. Wer allerdings zwischen Perl und Ruby öfters mal wechseln muss, sollte sich die Ausnutzung dieser Sache nicht angewöhnen. In Perl sind diese Variablen nämlich wirklich global. Ich finde das auch in Ruby keinen guten Stil, weil es eher verwirrt. Wenn man $variable liest, geht man immer von global aus. Im Grunde ist das auch eine Inkonsistenz von Ruby, wenn auch oft mit positivem Resultat.

Im übrigen sind alle $-Regex-Variablen von einem MatchData Objekt abgeleitet. Das Ergebnis eines Vergleiches ist nämlich die Erzeugung solch eines Objektes, welches der Träger aller Informationen über diesen Vergleich ist. Erreichbar ist es über $~. Es besitzt Methoden, worüber man all die Dinge abfragen kann, die man auch über $&, $1, $` usw. erhalten kann.

 
str = "davor:wort1 wort2:danach"
if str =~ /:((\w+) (\w+)):/
  puts "Davor:  " + $~.pre_match
  puts "Muster: " + $~[0]
  puts "Teilm1: " + $~[1]
  puts "Teilm2: " + $~[2]
  puts "Teilm3: " + $~[3]
  puts "Danach: " + $~.post_match
end

Hier eine Übersicht, welche globalen Regexp-Variablen es gibt und die Entsprechung in MatchData:

 
$~                         MatchData Objekt
$&       $~[0]             Muster
$1..$n   $~[0]..$~[n]      Teilmuster
$`       $~.pre_match      Teilstring vor Muster
$'       $~.post_match     Teilstrring nach Muster
$+       $~.[-1]           Letzter Teilstring

Möchte man mehrere reguläre Vergleiche hintereinander ausführen, kann man $~ zwischenspeichern, um später nochmal drauf zugreifen zu können.

 
str = "Hello World"
match1 = nil
match2 = nil

if str =~ /Hello/
  match1 = $~
end
if str =~ /World/
  match2 = $~
end

puts "Erstes Muster:  " + (match1 ? match1[0] : "nicht gefunden")
puts "Zweites Muster: " + (match2 ? match2[0] : "nicht gefunden")


Man kann natürlich auch gleich mit einem Regexp-Objekt einen Vergleich durchführen. Das Ergebnis der Methode Regexp#match ist ein MatchData Objekt, genauso wie $~ es ist.

 
str    = "Hello World"
rxp1   = /Hello/
match1 = rxp1.match( str )
match2 = /World/.match( str )

puts "Erstes Muster:  " + (match1 ? match1[0] : "nicht gefunden")
puts "Zweites Muster: " + (match2 ? match2[0] : "nicht gefunden")


Wir können dabei das regexp-Literal /Hello/ erst einer Variablen zuweisen und mit dieser dann weiterarbeiten. Wir können, wie bei match2 auch direkt dem Literal /World/ ein .match(str) hinten anhängen. Genauso, wie wir "Hello".upcase in Ruby schreiben können.

Die Stringklasse kennt noch die Möglichkeit, ein Regexp-Ausdruck in eckige Klammern zu schreiben. Das Ergebnis ist dabei das gefundene Muster.

 
datum = "23.05.05"
day = datum[ /^\d+/ ]
puts "Tag: " + day

Mit dieser Form haben wir allerdings nur Zugriff auf das Muster, nicht auf Teilmuster, was nach oder vor dem Muster steht usw.

Ganz ähnlich funktioniert String#scan( regexp ), wobei jedoch jedes Vorkommen des Musters im String als Array zurückgegeben wird. Wahlweise kann man auch über eine Blockmethode jeden Treffer abarbeiten.

Ein etwas allgeingültiger Methode findet man in enumerable#grep. Weil enumerable in vielen Klassen included ist, hat man damit ein sehr mächtiges Werkzeug. Dabei sucht grep nicht nur nach regulären Expressions.

Mittels String#index kann man sich die Position ausgeben lassen, ab dem ein Muster greift. Ähnlich verhält sich String#rindex, welches jedoch die letzte Position zurückgibt, wo das Muster greift.

In regulären Ausdrücken funktioniert auch Variablen-Substitution, wie man es von Strings kennt. Und auch Strings kann man in reguläre Ausdrücke umwandeln.

 
str_pattern = '\w+'
pattern = Regexp.new( str_pattern )
puts pattern.match( "Hello 293" )

oder

 
str_pattern = '\w+'
puts "Hello 293"[ /#{str_pattern} 2/ ]

Mit regulären Ausdrücke splitten

Ein String lässt sich wunderbar über eine regulären Ausdruck aufsplitten. String#split wird oft benötigt, um mehrspaltige Textdateien zu parsen.

 
line = "meier:hans:hans.meier@domain.tld"
a = line.split( /:/ )
puts "Name:    " + a[0]
puts "Vorname: " + a[1]
puts "Mail:    " + a[2]

Eine andere Einsatzmöglichkeit ist die Aufsplittung mehrerer Zeilen in einem String in ein Array.

 
lines = <<EOF
Du bist ein werdendes, nicht ein gewordnes ICH.
Und alles Werden ist im Widerspruch mit sich.
Unendliches das WIRD, muss ENDLICH sich gebaeren.
Und Endliches will, indem es wird,
unendlich werden.

(Friedrich Rueckert) 
EOF

a = lines.split( /\n/ )
puts "Zeile 3: " + a[2]

Bei split fällt immer das Muster heraus, mit dem man splittet. Von daher braucht man bei diesem Beispiel kein chomp mehr machen. Der Zeilentrenner ist schon aufgefressen.

Grundsätzlich gilt: Immer wenn mehrere Teile in einem String stecken, an die man ran möchte, sollte man über split nachdenken.

Reguläre Ausdrücke und Substitution

Reguläre Ausdrücke werden oft im Zusammenhang mit Substitutionen verwendet. Ein Teilstring, den man über einen Regexp gefunden hat, soll gegen etwas anderes ausgetauscht werden.

 
optimist = "Wir leben in einer guten Welt"
pessimist = optimist.sub( /guten/, "kaputten" )

puts "Optimist:  " + optimist
puts "Pessimist: " + pessimist 

Hierbei ist sub eine Methode der Klasse String, die als ersten Parameter einen regulären Ausdruck bekommt, der das Muster festlegt, was durch den zweiten Parameter ausgetauscht werden soll.

Die Methode sub tauscht genau einmal aus. Soll ein Muster öfter ausgetauscht werden, braucht des die Methode gsub.

 
str = "weiter, weiter, weiter!"
str1 = str.gsub( /weiter/, "schneller" )
puts str
puts str1

Sowohl sub wie auch gsub findet man auch im Kernel-Modul. Der String, auf den sie dort angewendet werden, ist $_, was der aktuellen Zeile der Eingabedatei entspricht (letzte Kernel#gets oder Kernel#readline Operation).

Backreferenzen sind eine nette Sache: Man kann Teilausdrücke, die im regulären Ausdruck gefunden wurden, gleich zur Ersetzung nutzen.

 
ansicht1 = "Ruby ist besser als Perl."
ansicht2 = ansicht1.sub( /(Ruby)(.*)(Perl)/, '\3\2\1' )
puts ansicht1
puts ansicht2

Backreferenzen erhält man über \1..\n. Hier gilt allerdings einiges zu beachten. Nicht umsonst habe ich die einfachen Anführungsstriche für den String genommen. Hätte ich die doppelten eingesetzt, würde der String an sich schonmal einer Substitution unterzogen: \3 würde durch das ASCII-Zeichen 0x03 ersetzt. Der Backslash wird einfach in vielerlei unterschiedlichen Zusammenhängen als Spezialzeichen mit Sonderbedeutung genutzt. Das führt öfters mal zu unerwarteten oder hakeligen Situationen. Man sollte deshalb z.B. auch vermeiden, einen Backslash in Texten zu verwenden, wenn man stattdessen andere Sonderzeichen benutzen kann.

Wollte man im obigen Beispiel trotzdem die doppelten Anführungsstriche verwenden, kann man "\\3\\2\\1" schreiben. Nach der normalen String-Substitution wäre \3\2\1 übriggeblieben.

Den Extremfall mit mehreren Überlagerungen haben wir, wenn wir den String 'a\b\c' in 'a\\b\\c' verwandeln wollen.

 
str  = 'a\b\c'
str1 = str.gsub( /\\/, '\\\\\\\\' )
puts str1

Warum das so ist, kann man durch etwa experimentieren herausfinden.

Es gibt eine Falle bei sub und gsub. Wenn nämlich der Ersetzungsstring \1..\n enthält, obwohl wir überhaupt keine Backreferenz machen wollten.

 
pattern = /@@1/
str  = 'Spielstand: @@1'
stand = '3\1'
str1 = str.gsub( pattern, stand)
puts str1

Wir erhalten "Spielstand 3" und nicht "Spielstand 3\1". Warum auch immer derjenige einen Backslash als Trenner genommen hat, das Programm sollte so jedenfalls nicht auf die Nase fallen. Eine Möglichkeit, diese Probleme zu umgehen, ist die Blockform von gsub zu nutzen.

 
pattern = /@@1/
str  = 'Spielstand: @@1'
stand = '3\1'
str1 = str.gsub( pattern ) { stand }
puts str1

Dies funktioniert deshalb, weil hier keine Backreferenzen über \1..\n funktionieren. Hier müsste man auf die Teilstrings wie gewohnt über $1..$n zugreifen.

Man sollte hier dringend darauf achten, dass sub/gsub ohne Blockform eine Sicherheitslücke darstellen können, wenn der Ersetzungsstring aus einer unkontrollierten Umgebung stammt.

Generell gilt auch: Backreferenzen oder Teilstrings sind nil, wenn sie im Muster nicht vorhanden sind. Es wird keine Fehlermeldung/Exception ausgegeben.

 
a = "12345".sub( /^(.)(.)/, "\3\4" )  #>> keine Exception

Beispiele für reguläre Ausdrücke in Ruby

...wird ergänzt

Referenzen

  • [1] Buch: Jeffrey E. F. Friedl; Reguläre Ausdrücke; O'Reilly; ISBN: 3897213494; ("Die Regexp-Bibel")
  • [2] Buch: Tony Stubblebine; Reguläre Ausdrücke kurz&gut; O'Reilly; ISBN: 3897212641
  • [3] Buch: Programming Ruby; Dave Thomas; Second Edition; The Pragmatic Programmers; ISBN: 0-9745140-5-5; ("Die Ruby-Bibel" bzw. PickAxe II)
  • [4] Wikipedia zu regulären Ausdrücken

Copyright

Copyright (c) 2005 Winfried Mueller, www.reintechnisch.de

Es wird die Erlaubnis gegeben dieses Dokument zu kopieren, zu verteilen und/oder zu verändern unter den Bedingungen der GNU Free Documentation License, Version 1.1 oder einer späteren, von der Free Software Foundation veröffentlichten Version; mit keinen unveränderlichen Abschnitten, mit keinen Vorderseitentexten, und keinen Rückseitentexten.

Eine Kopie dieser Lizenz finden Sie unter GNU Free Documentation License.

Eine inoffizielle Übersetzung finden Sie unter GNU Free Documention License, deutsch.

In diesem Artikel werden evtl. eingetragene Warenzeichen, Handelsnamen und Gebrauchsnamen verwendet. Auch wenn diese nicht als solche gekennzeichnet sind, gelten die entsprechenden Schutzbestimmungen.

Alle Informationen in diesem Artikel wurden mit Sorgfalt erarbeitet. Trotzdem können Inhalte fehlerhaft oder unvollständig sein. Ich übernehme keinerlei Haftung für eventuelle Schäden oder sonstige Nachteile, die sich durch die Nutzung der hier dargebotenen Informationen ergeben.

Sollten Teile dieser Hinweise der geltenden Rechtslage nicht entsprechen, bleiben die übrigen Teile davon unberührt.

Todo

  • Atomare Ausdrücke? => (?>...)
  • Lookahead?

Changelog

  • 02.04.2009: kleine Korrekturen. Dank an Dirk.
  • 27.06.2005: Ergänzung /m und \s und \n Eigenheit
  • 14.05.2005: WoNaDo: Warnung Backref bei sub/gsub -> nil wenn Backref nicht vorhanden
  • 11.05.2005: WoNaDo: puts "Zeile 4: " + a[3]
  • 11.05.2005: Anmerkung mawe: "Eingabe einer Zahl..." bereinigt.