Reguläre Ausdrücke stammen aus dem Gebiet der Automatentheorie, die wiederum Teil der theoretischen Informatik ist. Zu jedem regulären Ausdruck (regulär deshalb, weil zur Familie der regulären Sprachen gehörend) besteht ein endlicher Automat, der den Ausdruck akzeptiert. Ein endlicher Automat ist einfach eine Zustandsmaschine mit einer endlichen Menge von Zuständen, die diese einnehmen kann.
In der Praxis werden reguläre Ausdrücke vor allem zum Mustervergleich auf Zeichenketten angewendet.
Diese Ausrücke sind weitgehend sprachübergreifend: bis auf einige kleine Abweichungen lässt sich eine regular expression in Programmiersprachen wie Python, Perl oder Java gleich schreiben. Auch viele Texteditoren bieten ebenso Unterstützung für reguläre Ausdrücke wie z.B. auch MS Word.
Wir haben bisher einige Möglichkeiten kennen gelernt, in einem String nach einem Substring zu suchen.
Den in-Operator:
In [ ]:
firstnames = ['Astrid', 'Ines', 'Christoph', 'Markus', 'Çınar', 'Đželila', 'Niklas', 'Anna', 'Stefanie', 'Raphael', 'Anna-Lena', 'Silvia', 'Julian', 'Simon', 'Katharina', 'Michael', 'Dominik', 'Maria', 'Kevin', 'Bianca', 'Thomas', 'Nora', 'Manuel', 'Selina', 'Gabriel', 'Daniel', 'Thomas', 'Nina', 'Michael', 'Fabio', 'Theresa', 'Manuel', 'Carina', 'Philipp', 'Lukas', 'Wolfgang', 'Anna', 'Doris', 'Thomas', 'Muhammed', 'Christoph', 'Lisa-Marie', 'Jessica', 'Maria', 'Thomas', 'Florian', 'Martin', 'Anna', 'Oliver', 'Gregor', 'Helmut', 'Florian', 'Matteo', 'David', 'Marlene', 'Vanessa', 'Lea', 'Jan', 'Béla', 'Verena', 'Manuel', 'Björn', 'Tobias', 'Denise', 'Emma', 'Lukas', 'Sarah', 'Oliver', 'Janine', 'Manuel', 'Georg', 'Lorenz', 'Verena', 'Caroline', 'Laura', 'Felix', 'Simon', 'Lea', 'Peter', 'Sandra', 'Julia', 'Sophie', 'Jacqueline', 'Nina', 'Sebastian', 'David', 'Matthias', 'Patrick', 'Selina', 'Fabian', 'Daniel', 'Sabine', 'Josef', 'Lisa', 'Carina', 'Florian', 'Fabian', 'Viktoria', 'Christoph', 'Emilia']
for firstname in firstnames:
if 'ie' in firstname:
print(firstname)
startswith() und endswith():
In [ ]:
[firstname for firstname in firstnames if firstname.startswith('A')]
In [ ]:
[firstname for firstname in firstnames if firstname.endswith('o')]
Reguläre Ausdrücke (oder regular expressions) erweitern die Möglichkeiten, in einem String nach einem Muster zu suchen, enorm. Sie sind auch nicht Python-spezifisch, sondern wie bereits gesagt, in den meisten Programmiersprachen verfügbar. Aber auch viele Texteditoren und Textverarbeitungsprogramme unterstützen reguläre Ausdrücke.
Ein regulärer Ausdruck ist ein in einer speziellen Syntax geschriebenes Muster, das auf einen String angewendet wird.
Hier gleich ein Tipp: Reguläre Ausdrücke sind nicht gerade berühmt für ihre einfache Nachvollziehbarkeit. Erfahrungsgemäß ist hier die Verwendung von Regex-Testern sehr hilfreich. Hier zwei Empfehlungen:
In Python werden reguläre Ausdrücke über das Modul re
bereitgestellt, das wir zuerst laden müssen:
In [ ]:
import re
Das re-Modul stellt eine Reihe von Funktionen bereit, darunter die Funktion search()
, mit der nach einem Muster in
einem String gesucht werden kann. search()
erwartet zwei Argumente: das zu suchende Muster und den String, auf den das Muster anzuwenden ist.
Wird das Muster gefunden, liefert search()
ein Match
-Objekt:
In [ ]:
re.search('de', 'abcdef')
Wird das Muster nicht gefunden, liefert search()
None
zurück:
In [ ]:
print(re.search('xyz', 'abcdef'))
Dasselbe hätte wir allerdings auch mit einem simplen 'xyz' in 'abcdef'
erreichen können:
In [ ]:
'xyz' in 'abcdef'
Die Mächtigkeit von regulären Ausdrücken ergibt sich erst aus der Möglichkeit, komplexere Muster zu definieren.
In [ ]:
re.search('^ab', 'abc')
In [ ]:
re.search('^ab', 'cab')
Reguläre Ausdrücke verwenden das Zeichen $
um das Ende des Strings zu markieren:
In [ ]:
re.search('z$', 'xyz')
In [ ]:
re.search('z$', 'xyz!')
Zum Nachdenken: Auf welchen String passt dieses Muster?
In [ ]:
re.search('^abc$', '')
Der Punkt steht in einem regulären Ausdruck für jedes beliebige Zeichen.
In [ ]:
re.search('v.n', 'Guido van Rossum')
In [ ]:
re.search('v.n', 'Anton von Webern')
Jedes Zeichen lässt sich mit einem Quantor kombinieren, der angibt, wie oft das Zeichen an dieser Stelle vorkommen muss (oder darf). Folgende Quantoren sollten Sie kennen:
*
: Das Stern-Zeichen bedeutet 0 bis beliebig viele Wiederholungen.+
: Das Plus steht für eine oder mehr Wiederholungen?
: Das Fragezeichen steht für keine oder eine Wiederholung
In [ ]:
for s in ['Pr', 'Par', 'Paar', 'Paaar']:
if re.search('a*', s):
print(s)
Kombiniert man den Quantor *
mit dem Platzhalterzeichen ., so bedeutet das, dass an dieser Stelle jedes Zeichen beliebig oft vorkommen kann, es wird also eine beliebige Menge beliebiger Zeichen damit abgedeckt.
In [ ]:
re.search('G.*v', 'Guido van Rossum')
Das Muster passt auf Guido v
, weil zwischen dem G
und dem v
beliebig viele Zeichen erlaubt sind.
Beliebig viele inkludiert aber auch keine, wie wir an diesem Beispiel sehen können:
In [ ]:
re.search('R.*o', 'Guido van Rossum')
In [ ]:
re.search('R.+o', 'Guido van Rossum')
Dieses Muster wird hingegen nach wie vor gefunden:
In [ ]:
re.search('G.+v', 'Guido van Rossum')
In [ ]:
for s in ['Ros', 'Rus', 'Rxs', 'Rs', 'Roos']:
if re.search('R.?s', s):
print(s)
In [ ]:
for s in ['Rs', 'Ras', 'Raas', 'Raus', 'Raaas']:
if re.search('R.{2}s', s):
print(s)
In [ ]:
for s in ['Rs', 'Ras', 'Rees', 'Riiis', 'Roooos']:
if re.search('R.{1,3}s',s ):
print(s)
In [ ]:
for s in ['Pr', 'Par', 'Pur', 'Paar', 'Paur', 'Paaar']:
if re.search('a+', s):
print(s)
Übung: versuchen Sie das obige Beispiel auch mit anderen Quantoren!
In [ ]:
re.findall('G.*o', 'Guido van Rossum')
Der gefundene Teilstring ist nicht, wie man vielleicht erwarten könnte, Guido
, sondern den längsten String, auf den das Muster passt: Guido van Ro
. Da dies nicht immer erwünscht ist, können wir einen Quantor durch ein nachgestelltes Fragezeichen als non-greedy markieren:
In [ ]:
re.findall('G.*?o', 'Guido van Rossum')
Um noch die Funktionsweise von findall()
zu demonstrieren, ein weiteres Beispiel:
In [ ]:
re.findall('.aa.', 'Ein paar Paare ohne Haar')
In [ ]:
re.findall('a.{0,2}', 'Anton arbeitet am liebsten allein.')
Wenn wir diese Unterscheidung nicht wollen, müssen wir die Regex-Funktion mit dem Flag re.I (oder: re.IGNORECASE) aufrufen:
In [ ]:
re.findall('a.{0,2}', 'Anton arbeitet am liebsten allein.', re.I)
Bisher haben wir nur mit bestimmten Zeichen oder mit dem Platzhalter für ein beliebiges Zeichen gearbeitet. Oft wäre es aber nützlich, wenn wir nur auf bestimmte Zeichen, wie zum Beispiel alle Vokale testen könnten. Regular Expressions stellen dazu das Konzept der Zeichenklasse zur Verfügung. Alle Zeichen innerhalb eckiger Klammern werden als Mitglieder der Zeichenklasse betrachtet. Der Ausdruck [aeiou]
passt somit auf jeden Vokal.
In [ ]:
re.findall('[aeiou].{0,2}', 'Anton arbeitet am liebsten allein.')
Alle Kleinbuchstaben können wir so angeben: [a-z]
In [ ]:
re.findall('[a-z]', 'Anton arbeitet am liebsten allein.')
Das funktioniert auch für Ziffern: [0-9]
In [ ]:
re.findall('[0-9]', 'Anton hat 1 Pferd und 2 Katzen.')
Übung: Versuchen Sie den folgenden Ausdruck so umzuschreiben, dass das Ergebnis ['12', '3']
ist:
In [ ]:
re.findall('[0-9]', 'Anna hat 12 Kühe Pferd und 3 Hunde.')
Wie können aber auch alle Groß- und Kleinbuchstaben als Zeichenklasse definieren: [a-zA-Z]
In [ ]:
re.findall('[a-zA-Z]', 'Anton arbeitet am liebsten allein.')
Einige Zeichenklassen sind vordefiniert und können uns das Leben erheblich erleichtern:
\s
steht für Whitespace (Leerzeichen, Tabulatoren, Zeilenumbrüche usw.)\S
steht für jedes Zeichen, dass kein Whitespace ist. (Diese Umkehrung durch Großschreibung funktioniert
für alle hier vorgestellten Zeichenklassen)\b
steht für einen leeren String am Anfang oder Ende eines Worts (word boundary)\d
steht für ein decimal digit
, eine Ziffer in irgendeinem Zeichensystem, das in Unicode definiert ist (Siehe hier).\w
steht für ein "Wortzeichen". In ASCII entspricht das [a-zA-Z0-9_]Da manche Backslash-Kombinationen (wie \b
) eine besondere Bedeutung haben, müssen wir sie entweder durch einen zweiten Backslash escapen oder den Ausdruck als Raw String schreiben (gekennzeichnet durch ein vor den String gestelltes r
). Dieses Beispiel funktioniert nicht:
In [ ]:
re.findall('\b[A-Z].*?\b', 'Anton hat 1 Pferd und 2 Katzen.')
Das hingegen schon:
In [ ]:
re.findall('\\b[A-Z].*?\\b', 'Anton hat 1 Pferd und 2 Katzen.')
Grundsätzlich sollten Sie sich zur Angewohnheit machen, Muster immer als Raw Strings zu schreiben, indem sie ein r
vor den Ausdruck stellen:
In [ ]:
re.findall(r'\b[A-Z].*?\b', 'Anton hat 1 Pferd und 2 Katzen.')
Dieses neue Wissen können wir wir zum Beispiel anwenden, um alle Wörter zu finden, in denen zwei Vokale aufeinander folgen:
In [ ]:
s = "Ein paar Paare in der Bar machen ohne Haare viele Klare alle"
re.findall(r'\b\w*[aeiou]{2}\w*', s)
Noch einmal zur Erinnerung: \b
steht für word boundary, \w
für ein Wortzeichen ([a-zA-Z0-9_]).
In [ ]:
s = 'Anton hat 12 Kühe, 1 Pferd, 2 Hunde und 3 Katzen'
re.findall(r'Katzen?|Pferde?|Hunde?', s)
Wir können uns sogar die Zahl der Tiere mitausgeben lassen, wenn wir davon ausgehen, dass die Zahl vor dem Tier steht und auf die Zahl ein Whitespace folgt:
In [ ]:
s = 'Anton hat 12 Kühe, 1 Pferd, 2 Hunde und 3 Katzen'
re.findall(r'\d+\s+Pferde?|\d+\s*Hunde?|\d+\s*Katzen?', s)
Greifen wir das Beispiel von oben mit den Vokalen noch einmal auf. Falls wir nur an Doppelvokalen (und nicht an zwei aufeinander folgenden Vokale) interessiert sind, können wir mit Alternativen arbeiten:
In [ ]:
s = "Ein paar Paare in der Bar machen ohne Haare viele Klare alle"
re.findall(r'\b\w*aa\w*|\b\wee\w*|\b\wii\w*|\b\woo\w*|\b\wuu\w*', s)
Dieses Muster lässt sich natürlich auch auf längere Texte anwenden. Finden wir heraus, welche Wörter mit Doppelvokalen Jakob Wassermann verwendet hat:
In [ ]:
with open('../data/wassermann/der_mann_von_vierzig_jahren.txt') as fh:
text = fh.read()
re.findall(r'\b\w*aa\w*|\b\wee\w*|\b\wii\w*|\b\woo\w*|\b\wuu\w*', text)
Übung: Hier wäre es natürlich übersichtlicher, wenn jedes Wort nur ein Mal vorkommen würde und die Ausgabe alphabetisch sortiert wäre!
In [ ]:
Zur Auffrischung: re.search()
, das wir bereits kennengelernt haben, liefert, wenn das Muster gefunden wurde, ein Match-Objekt:
In [ ]:
re.search('v.n', text)
Weisen wir das Match-Objekt einer Variable zu, um es genauer zu untersuchen:
In [ ]:
m = re.search('v.n', text)
dir(m)
Ein Match-Objekt hat Funktionen, mit denen wir die Position des Match im String feststellen können:
In [ ]:
print(m.start(), m.end())
Die Methode span()
liefert uns beide Werte als Tupel:
In [ ]:
m.span()
Besonders interessant sind die beiden Methoden groups()
und group()
. Damit wir diese nutzen können, müssen wir zuerst ein weiteres wichtiges Konzept von regulären Ausdrücken kennen lernen: Gruppierungen.
Wir haben oben an einem Beispiel gesehen, wie wir bestimmte Tiere mit ihrer Zahl extrahieren konnten:
In [ ]:
s = 'Anton hat 12 Kühe, 1 Pferd, 2 Hunde und 3 Katzen'
re.findall(r'\d+\s+Pferde?|\d+\s*Hunde?|\d+\s*Katzen?', s)
search()
liefert nur den ersten Treffer als Match-Objekt:
In [ ]:
re.search(r'\d+\s+Pferde?|\d+\s*Hunde?|\d+\s*Katzen?', s)
Wollen wir diese Daten weiter verarbeiten, wäre es nicht schlecht, die Zahl von der Tierart zu trennen. Dazu müssen wir unseren Ausdruck durch runde Klammern gruppieren.
In [ ]:
re.search(r'(\d+)\s+(Pferde?|Hunde?|Katzen?)', s)
Auf den ersten Blick ist kein Unterschied zu sehen. Allerdings gibt es einen wesentlichen Unterschied im Match-Objekt:
In [ ]:
m = re.search(r'(\d+)\s+(Pferde?|Hunde?|Katzen?)', s)
m.groups()
Mit diesem Wissen können wir zählen, wie viele Pferde, Hunde und Katzen in einem Text vorkommen. Da re.search()
immer nur den ersten Treffer liefert, müssen wir uns in einer Schleife durch den Text bewegen und so lange rechts vom letzten Treffer weitersuchen, bis kein Treffer mehr vorhanden ist. Dazu können wir die Eigenschaft string
des Match-Objekts verwenden, die den durchsuchten String beinhaltet, und die Methode end()
, die die Position des letzten Matches liefert.
In [ ]:
s = 'Anton hat 12 Kühe, 1 Pferd, 2 Hunde und 3 Katzen.'
pattern = r'(\d+)\s+(Pferde?|Hunde?|Katzen?|Kuh|Kühe)'
animal_counter = 0
m = re.search(pattern, s)
while m:
print(m.groups())
animal_counter += int(m.group(1))
m = re.search(pattern, m.string[m.end():])
print("{} Tiere gefunden".format(animal_counter))
In [ ]:
re.match('ab', 'abcdef')
In [ ]:
re.match('ab', '-abcdef')
In [ ]:
'1,2,3,4'.split(',')
re
stellt eine ähnliche split()
-Funktion zur Verfügung, bei der wir das Trennzeichen (bzw. den Trennstring) als
regulären Ausdruck angeben können. Dieses Split ist dadurch viel mächtiger.
Wollen wir etwa einen Text in Sätze zerlegen, müssen wir den Text an jedem Satzzeichen trennen: .!?
. Mit re.split()
ist das ganz einfach:
In [ ]:
with open('../data/wassermann/der_mann_von_vierzig_jahren.txt') as fh:
text = fh.read()
sentences = re.split(r'[.?!]\s*', text)
print(len(sentences))
In [ ]:
print(sentences[:4])
In [ ]:
with open('../data/wassermann/der_mann_von_vierzig_jahren.txt') as fh:
text = fh.read()
text = re.sub('\s+', ' ', text)
sentences = re.split(r'[\.\?\!]\s*', text)
print(sentences[:4])
Jetzt können wir beispielsweise die mittlere Satzlänge berechnen:
In [ ]:
# Für alle Fälle entfernen wir noch Sätze der Länge 0
sentences = [s for s in sentences if len(s) > 0]
sum([len(s) for s in sentences]) / len(sentences)
Oder die gemittelte Zahl von Wörtern pro Satz, den kürzesten und den längsten Satz:
In [ ]:
words_per_sentence = [len(s.split()) for s in sentences]
print('Mittlere Wortzahl pro Satz:', sum(words_per_sentence) / len(sentences))
print('Der längster Satz hat {} Wörter, der kürzeste {}'.format(max(words_per_sentence), min(words_per_sentence)))