Reguläre Ausdrücke

Ein häufig wiederkehrendes Problem besteht darin, Zeichenketten zu durchsuchen oder zu manipulieren. Wir haben bereits früher Programme geschrieben, die nach einer Teilzeichenkette suchen oder eine solche ersetzen. Da dieses Problem so häufig auftritt, wird dazu in der Regel ein Mechanismus verwendet, der ohne explizite Programmierung auskommt. Dieser erlaubt es auch, statt konkrete Teilzeichenketten zu suchen oder zu ersetzen, ganze Klassen von Teilzeichenketten anzugeben.

Diese Klasse sind Mengen von Wörtern, also Sprachen, die wir mit Hilfe von (E)BNF beschreiben könnten. Reguläre Ausdrücke sind eine einfachere Methode, Sprachen (also Mengen von Wörtern) zu beschreiben. Sie erlauben effizientere Methoden zur Implementierung von Such- und Ersetzungs-Algorithmen als (E)BNF, sind jedoch auch nicht so ausdrucksstark.1 Es gibt also Sprachen, die mit (E)BNF, nicht aber mit regulären Ausdrücken beschrieben werden können. Die früher beschriebene Sprache für arithmetische Ausdrücke ist zum Beispiel so eine Sprache. Reguläre Ausdrücke sind jedoch ausdrucksstark genug für viele relevante Problemstellungen und werden wegen ihrer vergleichsweisen Einfachheit oft bevorzugt.

Um reguläre Ausdrücke in Python verwenden zu können, muss man zunächst das Modul re importieren:

>>> import re

Reguläre Ausdrücke sehen in Python genau wie Strings aus, man schreibt aber meist ein kleines r vor das öffnende Anführungszeichen, um anzuzeigen, dass dieser String ein regulärer Ausdruck ist.2 Im einfachsten Fall bestehen Sie einfach aus einer Zeichenkette wie zum Beispiel r'elf'. Möchte man nun einen regulären Ausdruck gegen einen String matchen, so kann man z. B. die Funktion match aus dem Modul re verwenden:

>>> m = re.match(r'elf','elfen')
>>> print(m)
<re.Match object; span=(0, 3), match='elf'>

Wir sehen also, dass Python ein Match-Objekt anlegt, welches alle relevanten Informationen zu dem erfolgreichen Match beinhaltet. Dies ist insbesondere der span, welcher ein Paar bestehend aus Anfangsposition des Matches und der Position des Zeichens hinter dem Match ist. Darüber hinaus noch das Wort, welches gematcht wurde. Diese beiden Komponenten können mit Hilfe der Methoden span() und group() aus einem Match-Objekt ausgelesen werden.

Wir haben die Methode match verwendet, welche nur dann erfolgreich ist, wenn der reguläre Ausdruck am Anfang des Strings matcht:

>>> m = re.match(r'elf','Spielfigur')
>>> print(m)
None

Obwohl im Wort 'Spielfigur' ein 'elf' steckt, ist das Matching nicht erfolgreich, was durch das Ergebnis None anstelle eines Match-Objekts angezeigt wird. Möchte man schauen, ob ein regulärer Ausdruck an irgendeiner Postion im String matcht, verwendet man die Methode search, welche das erste (am (weitesten links stehende) vorkommende Matching zurückgibt:

>>> m = re.search(r'elf','Spielfigur')
>>> print(m)
<re.Match object; span=(3, 6), match='elf'>

Mit Hilfe von span() und group kann man wieder alle relevanten Informationen zu dem gefundenen Match extrahieren.

Bisher haben wir nur einfach Zeichenfolgen (Buchstaben-Sequenzen) als reguläre Ausdrücke verwendet. Oft möchte man aber auch bestimmte Komponenten des Strings variabel lassen. Hierzu gibt es diverse weitere Konstrukte im regulären Ausdruck, welche wir im Folgenden anhand von Beispielen kennenlernen werden.

Hierfür werden bestimmte Zeichen in regulären Ausdrücken nicht als solche interpretiert, sondern haben eine besondere Bedeutung. Zum Beispiel steht der Punkt für ein beliebiges Zeichen und nicht für einen Punkt. Auch ist es möglich, explizit Gruppen von Zeichen zu definieren und auch Gruppen, die alle nicht genannten Zeichen beschreiben.

>>> re.search(r'e.f', 'Spielfigur')
<re.Match object; span=(3, 6), match='elf'>
>>> re.search(r'e[a-z]f', 'Spielfigur')   # ein Kleinbuchstabe
<re.Match object; span=(3, 6), match='elf'>
>>> re.search(r'e[^A-Z]f', 'Spielfigur')  # kein Großbuchstabe
<re.Match object; span=(3, 6), match='elf'>

Für bestimmte Gruppen sind Sonderzeichen vordefiniert:

  • \d steht für eine beliebige Ziffer, also [0-9],

  • \w steht für ein Wortzeichen, also [a-zA-Z0-9] und

  • \s steht für ein beliebiges Leerzeichen (inklusive Tabulatoren und Zeilenumbrüchen).

Das Fragezeichen kennzeichnet ein optionales Zeichen:

>>> re.search(r'elfi?', 'Spielfigur')
<re.Match object; span=(3, 7), match='elfi'>
>>> re.search(r'elfe?', 'Spielfigur')
<re.Match object; span=(3, 6), match='elf'>

Durch Klammerung kann es auch auf mehrere Zeichen angewendet werden:

>>> re.search(r'elf(en)?', 'Spielfigur')
<re.Match object; span=(3, 6), match='elf'>

Durch einen senkrechten Strich werden (wie bei der BNF) Alternativen gekennzeichnet:

>>> m = re.search(r'i(e|g)', 'Spielfigur')
>>> print(m)
<re.Match object; span=(2, 4), match='ie'>
>>> m.group()
'ie'
>>> m.group(1)
'e'

Dieses Beispiel zeigt, dass immer der am weitesten links stehende mögliche Teilstring gematcht wird. Außerdem ist es möglich, auf die Matches der geklammerten Teile des regulären Ausdrucks mit der Methode group(n) zuzugreifen. Hierbei gibt der Parameter n an, um welches Klammerpaar im regulären Ausdruck es sich handelt (von 1 an gezählt). Der Aufruf der Methode group() stellt eine Abkürzung für den Aufruf group(0) dar, welcher den gesamten gematchten String liefert.

>>> m = re.match(r'(.....)((..)...)', 'Spielfigur')
>>> m.group()
'Spielfigur'
>>> m.group(1)
'Spiel'
>>> m.group(2)
'figur'
>>> m.group(3)
'fi'

Schließlich gibt es noch Möglichkeiten, Wiederholungen zu spezifizieren. Der Stern kennzeichnet eine optionale Wiederholung und das Plus-Zeichen eine mindestens einmal wiederholte Zeichenfolge.

>>> m = re.search(r'pi.*g', 'Spielfigur')
>>> m
<re.Match object; span=(1, 8), match='pielfig'>
>>> m = re.search(r'pi.+g', 'Spielfigur')
>>> m
<re.Match object; span=(1, 8), match='pielfig'>
>>> m = re.search(r'pi.*e', 'Spielfigur')
>>> m
<re.Match object; span=(1, 4), match='pie'>
>>> m = re.search(r'pi.+e', 'Spielfigur')
>>> print(m)
None

Statt Teilzeichenketten zu suchen, können wir diese auch ersetzen. Die Methode replace auf Zeichenketten ersetzt das erste Vorkommen einer Zeichenkette durch eine andere Zeichenkette:

>>> 'abc'.replace('b','x')
'axc'
>>> 'abc'.replace('bc','x')
'ax'

Entsprechend ersetzt die Methode sub alle Matchings eines regulären Ausdrucks durch einen vorgegebenen String:

>>> re.sub(r'i.+g', 'x', 'Spielfigur')
'Spxur'
>>> re.sub(r'i.', 'x', 'Spielfigur')
'Spxlfxur'

Entsprechend ist es mit der Methode findall auch möglich, alle Matchings eines regulären Ausdrucks in einem String zu finden. Hierbei wird aber nur eine Liste aller gematcheten Sub-Strings zurückgegeben, keine Match-Objekte:

>>> re.findall(r'i.', 'Spielfigur')
['ie', 'ig']

Falls der reguläre Ausdruck an keiner Stelle matcht, wird die leere Liste zurückgegeben:

>>> re.findall(r'ia', 'Spielfigur')
[]

  1. Es gibt Erweiterungen von regulären Ausdrücken, die ihre Ausdrucksstärke erhöhen, die wir aber hier nicht thematisieren. ↩︎

  2. Reguläre Ausdrücke sind also Werte und können wie andere Zeichenketten oder Zahlen in Variablen und Datenstrukturen gespeichert werden. ↩︎