Python unter Linux: Reguläre Ausdrücke

Aus Wikibooks

Reguläre Ausdrücke helfen beim Finden von Textstellen. Sie werden angegeben in Form von Mustern wie "Ich suche eine Folge von Ziffern am Anfang einer Textzeile". Für "Ziffernfolge" und "Zeilenanfang" gibt es Kurzschreibweisen und Regeln, wie diese zu kombinieren sind. Um die Funktionen und Methoden rund um reguläre Ausdrücke benutzen zu können, müssen wir das Modul re einbinden.

Finde irgendwo[Bearbeiten]

Haben wir Textzeilen, in denen beispielsweise Telefonnummern und Namen aufgelistet sind, so können wir mit Hilfe von regulären Ausdrücken die Namen von den Telefonnummern trennen - zum Beispiel um nach einer bestimmten Telefonnummer in einer langen Textdatei zu suchen:

#!/usr/bin/python

import re

s = "Peter 030111"

m = re.search(r"(\w+) (\d+)", s)

length = len(m.groups())
for i in xrange(length + 1):
    print "Group", i, ":", m.group(i)
Align=none Ausgabe

user@localhost:~$ ./re1.py
Group 0 : Peter 030111
Group 1 : Peter
Group 2 : 030111

Haben wir eine Textzeile, die das Tupel enthält, so können wir mit der search()-Funktion nach den einzelnen Tupel-Elementen suchen. Hierbei ist "\w" ein einzelnes Zeichen aus der Menge der Buchstaben, der Modifizierer "+" bedeutet "mindestens ein" Zeichen zu suchen. Die Klammern gruppieren diese Ausdrücke. Der erste Ausdruck in Klammern bedeutet also: Finde eine nicht leere Zeichenkette aus Buchstaben und nenne es die erste Gruppe mit Gruppenindex 1. (\d+) sucht nach einer nicht leeren Ziffernfolge und gibt dieser Gruppe den Index 2, da sie an zweiter Stelle steht. Zwischen der Buchstabenfolge und der Ziffernfolge muss sich exakt ein Leerzeichen befinden. search() liefert ein so genanntes Match-Objekt, wenn etwas gefunden wurde, sonst None. groups() liefert ein Tupel mit allen gefundenen Gruppen, mit group() kann man auf einzelne Gruppen zugreifen, wobei der Index 0 eine Zusammenfassung aller Gruppen als einzelnen String anbietet.

Nun muss man nicht alles gruppieren, nach dem man sucht. Manches Teile eines Ausdruckes möchte man schlicht verwerfen, um an die relevanten Informationen eines Textes zu kommen:

#!/usr/bin/python
# -*- coding: utf-8 -*-

import re

s ="""Opa hat am 10. 12. 1903 Geburtstag.
Oma wurde am 14. 07. geboren.
Mutti, deren Eltern sich am 10.02.1949 über die Geburt freuten, hat heute selber Kinder.
Vater hat am 3. 4. 1947 Geburtstag"""

for line in s.splitlines():
    m = re.search(r"^(\w+)\D*(\d*)\.\s?(\d*)\.\s?(\d*)", line)
    print line
    print m.groups()
Align=none Ausgabe

user@localhost:~$ ./re2.py
Opa hat am 10. 12. 1903 Geburtstag.
('Opa', '10', '12', '1903')
Oma wurde am 14. 07. geboren.
('Oma', '14', '07', )
Mutti, deren Eltern sich am 10.02.1949 über die Geburt freuten, hat heute selber Kinder.
('Mutti', '10', '02', '1949')
Vater hat am 3. 4. 1947 Geburtstag
('Vater', '3', '4', '1947')

Die for-Schleife wird über jede Zeile ausgeführt, wobei die Methode splitlines() einen Text in einzelne Zeilen zerlegt. In diesem Beispiel interessieren uns der Name, der dem Zeilenanfang als erstes Wort folgt, der Geburtstag, Geburtsmonat und das Geburtsjahr, welches auch fehlen darf. Der Zeilenanfang wird mit einem Dach (^) abgekürzt. Die erste Gruppe beinhaltet ein Wort, in dem Fall den Namen.

Ausdrücke im Überblick[Bearbeiten]

Hier ein Ausschnitt aus der Sprache der regulären Ausdrücke. Alternativen werden durch Kommas getrennt.

Ausdruck Bedeutung
. Der Punkt passt auf jedes Zeichen
^ Anfang einer Zeichenkette oder Zeile
$ Ende einer Zeichenkette oder Zeile
[abc] Passt auf ein Zeichen aus der Menge "abc"
[a-z] Passt auf ein Zeichen aus der Menge "a bis z"
A|B Passt auf den regulären Ausdruck A oder auf B
\A Anfang einer Zeichenkette
\d, [0-9] Eine einzelne Ziffer
\D, [^0-9] Jedes Zeichen außer der Ziffer
\s Leerzeichen wie Tab, Space, ...
\S Jedes Zeichen außer dem Leerzeichen
\w Buchstaben, Ziffern und der Unterstrich
\W Alle Zeichen außer solchen, die in \w definiert sind
\Z Das Ende einer Zeichenkette

Neben einzelnen Zeichen kann auch die Anzahl der vorkommenden Zeichen begrenzt werden. Folgende Tabelle gibt einen Überblick:

Ausdruck Bedeutung Beispiel
* Der Ausdruck kommt mindestens 0-mal vor. .*
+ Der Ausdruck kommt mindestens 1-mal vor. \d+
? Der Ausdruck kommt 0- oder 1-mal vor. (LOTTOGEWINN)?
{n} Der Ausdruck kommt n-mal vor. \w{3}
{m, n} Der Ausdruck kommt mindestens m-mal, höchstens n-mal vor. \w{3, 10}
{n,} Der Ausdruck kommt mindestens n-mal vor. \w{3,}
{,n} Der Ausdruck kommt höchstens n-mal vor. \w{,10}
*?, +?, ??, {m, n}? die kürzeste Variante einer Fundstelle wird erkannt. .*?, .+?, .??, .{3, 10}?

Gieriges Suchen[Bearbeiten]

Um den Unterschied zwischen den verschiedenen Quantoren zu verdeutlichen, suchen wir in einem angegebenen String mit verschiedenen Kombinationen von Quantoren nach Ausdrücken, die auf beiden Seiten von einem "e" begrenzt werden. Wir bilden dazu drei Gruppen, die unterschiedlich lange Bereiche finden:

#!/usr/bin/python
import re

zeile = "Dies ist eine Zeile"

m = re.search(r"(.*)(e.*e)(.*)", zeile)
print m.groups()

m = re.search(r"(.*?)(e.*?e)(.*)", zeile)
print m.groups()

m = re.search(r"(.*?)(e.*e)(.*?)", zeile)
print m.groups()
Align=none Ausgabe

user@localhost:~$ ./gierig1
('Dies ist eine Z', 'eile', '')
('Di', 'es ist e', 'ine Zeile')
('Di', 'es ist eine Zeile', '')

Wie wir sehen können, finden alle drei Ausdrücke Text, jedoch unterschiedlich lang. Die Variante (.*?) findet möglichst wenig Text, während (.*) möglichst lange Fundstellen findet. Solche Quantoren werden als gierig bezeichnet.

Matching[Bearbeiten]

Während search() im gesamten übergebenen String nach einem Ausdruck sucht, ist match() auf den Anfang des Strings beschränkt. Suchen wir in einer Datei wie /var/log/syslog nach Ereignissen vom 26. Januar, können wir das leicht mit match() erledigen:

#!/usr/bin/python
import re

datei = open("/var/log/syslog", "r")
print "Am 26. Januar passierte folgendes:"
for zeile in datei:
    m = re.match(r"Jan 26 (.*)", zeile)
    if m != None:
        print m.group(1)
datei.close()
Align=none Ausgabe

user@localhost:~$ ./match1
Am 26. Januar passierte folgendes:
10:08:19 rechner syslogd 1.5.0#2ubuntu6: restart.
10:08:21 rechner anacron[4640]: Job `cron.daily' terminated
10:12:42 rechner anacron[4640]: Job `cron.weekly' started
...

Es wird hier nach allen Zeichen gesucht, die der Zeichenkette "Jan 26 " folgen. Dies ist der Anfang der Zeile. Gibt es solche Zeichen, werden nur diese ausgegeben.

Ich will mehr![Bearbeiten]

Gruppen muss man nicht aufzählen, man kann sie auch benennen, wie folgendes Skript zeigt:

#!/usr/bin/python
# -*- coding: utf-8 -*-

import re

wunsch ="Chef, ich brauche mehr Geld. 1000 oder 2000 Euro halte ich für angebracht!"

m = re.search(r"(?P<geld>\d{4})", wunsch)

print m.groups()
print m.group('geld')
Align=none Ausgabe

user@localhost:~$ ./re3.py
('1000',)
1000

Die Gruppe (?P<geld>A) gibt dem eingeschlossenen regulären Ausdruck einen Namen. Es wird nach einer genau 4-stelligen Zahl gesucht, und auch gefunden. Dieses Beispiel offenbart aber schon eine Schwierigkeit mit search(): Wir finden nur das erste Vorkommen des Ausdrucks (1000), nicht jedoch den Zweiten.

Hier die verbesserte Version, die wirklich alle Wunschvorstellungen zu finden in der Lage ist:

#!/usr/bin/python
# -*- coding: utf-8 -*-

import re
wunsch ="Chef, ich brauche mehr Geld. 1000 oder 2000 Euro halte ich für angebracht!"

liste = re.findall(r"\d{4}", wunsch)
print liste
Align=none Ausgabe

user@localhost:~$ ./re4.py
['1000', '2000']

findall() wird genauso aufgerufen wie search(), findet jedoch auch mehrfache Vorkommen der Ausdrücke im Text.

Modi[Bearbeiten]

Sowohl search() wie auch match() erlauben als dritten Parameter einen Wert, der die Bedeutung des angegebenen regulären Ausdrucks spezifiziert.

Die folgende Tabelle gibt einen Überblick über diese Flags:

Flag Bedeutung
IGNORECASE Groß- und Kleinschreibung wird nicht unterschieden
LOCALE \w, \W, \b, \B, \s und \S werden abhängig von der gewählten Spracheinstellung behandelt
MULTILINE Beeinflusst ^ und $. Es werden der Zeilenanfang und das -ende beachtet.
DOTALL Der Punkt (.) soll jedes Zeichen finden, auch Zeilenwechsel
UNICODE \w, \W, \b, \B, \d, \D, \s und \S werden als Unicode-Zeichen behandelt
VERBOSE Erlaubt unter anderem Kommentare in regulären Ausdrücken, Leerzeichen werden ignoriert. Damit lassen sich schöne Ausdrücke mit Kommentaren zusammensetzen.

Hier ein Beispiel, wie man diese Flags anwendet. Es wird nach einer IP-Adresse gesucht, die sich über mehrere Zeilen erstreckt. Zwischen zwei Zahlen kann sowohl ein Punkt stehen wie auch ein Punkt gefolgt von einem Neue-Zeile-Zeichen. Wir benutzen daher das Flag DOTALL, um den Punkt auch dieses Zeichen suchen zu lassen. Nebenbei zeigen wir, wie man den Aufzählungsquantor ({1,3}) benutzt.

#!/usr/bin/python
# -*- coding: utf-8 -*-
import re

zeile = """Dieses sind mehrere Zeilen, es geht
hier um die Darstellung einer IP-Adresse 192.168.
10.1. Diese wird über mehrere Zeilen verteilt."""

m = re.search(r"(\d{1,3}).+?(\d{1,3}).+?(\d{1,3}).+?(\d{1,3}).", zeile, re.DOTALL)
if m is None:
    print "nicht gefunden"
else:
    print m.groups()
Align=none Ausgabe

user@localhost:~$ ./modi1.py
('192', '168', '10', '1')

Wie wir sehen, wird die IP-Adresse korrekt gefunden.

Gruppen[Bearbeiten]

Man kann in regulären Ausdrücken Gruppen bilden. Einige von diesen Gruppen haben Sie schon kennen gelernt. In diesem Abschnitt geben wir einen Überblick über Möglichkeiten, Ausdrücke zu gruppieren. Folgende Gruppen werden unterstützt:

Gruppe Bedeutung
(?:Ausdruck) Diese Gruppe wird ignoriert. Man kann sie nicht per groups() herauslösen, ebenfalls kann man sie nicht innerhalb der Ausdrücke referenzieren.
(?P<Name>Ausdruck) Weist einer Gruppe einen Namen zu. Siehe das Beispiel Ich will mehr!
(?P=Name) Diese Gruppe wird ersetzt durch den Text, der von der Gruppe mit dem angegebenen Namen gefunden wurde. Damit lassen sich innerhalb von Ausdrücken Referenzen auf vorherige Suchergebnisse bilden.
(?#Inhalt) Inhalt wird als Kommentar behandelt.
A(?=Ausdruck) A passt nur dann, wenn als nächstes Ausdruck folgt. Sonst nicht. Dieses ist also eine Art Vorschau auf kommende Suchergebnisse
A(?!Ausdruck) A passt nur dann, wenn Ausdruck nicht als nächstes folgt.
(?<=Ausdruck)A A passt nur dann, wenn Ausdruck vor A kommt.
(?<!Ausdruck)A A passt nicht, wenn Ausdruck vor A kommt.
(?(Name)A1|A2) Der Ausdruck A1 wird nur dann als Suchmuster benutzt, wenn eine Gruppe mit Namen Name existiert. Sonst wird der (optionale) Ausdruck A2 benutzt.

Wir wollen nun den Gebrauch einiger regulärer Ausdrücke demonstrieren:

#!/usr/bin/python
# -*- coding: utf-8 -*-
import re

zeilen = ("Peter sagt: Peter, gib mir den Ball", "Hans sagt: Peter, sprichst Du mit dir selbst?")

print "Leute, die gerne mit sich selbst sprechen:"

for z in zeilen:
    m = re.match(r"(?P<Person>\w+) sagt: (?P=Person),", z)
    if m is not None:
        print m.group('Person')

In diesem Beispiel wird eine Gruppe Person erzeugt. In der ersten Zeile ist der Inhalt dieser Gruppe Peter, in der zweiten Hans. (?P=Person) nimmt nun Bezug auf den Inhalt dieser Gruppe. Steht an der Stelle der betreffenden Zeile ebenfalls das, was der Inhalt der ersten Gruppe war (also Peter oder Hans), dann passt der gesamte reguläre Ausdruck. Sonst nicht. In diesem Fall passt nur die erste Zeile, denn es kommt zweimal Peter vor.

Etwas komplizierter ist der Ausdruck im folgenden Beispiel. Es wird die Datei /etc/group untersucht. Es sollen all jene Gruppen aufgelistet werden, die Sekundärgruppen für User sind. Diese Zeilen erkennt man an:

cdrom:x:24:haldaemon,tandar
floppy:x:25:haldaemon,tandar

Es sind jedoch Zeile, die keine User beinhalten, zu ignorieren, wie die folgenden:

fax:x:21:
voice:x:22:

Ein Programm, das diese Zusammenstellung schafft, kann mit regulären Ausdrücken arbeiten. Nur jene Zeilen, die hinter dem letzten Doppelpunkt Text haben, sollen berücksichtigt werden. Der Gruppenname wie auch die Benutzernamen sollen in einer eigenen Ausdrucks-Gruppe gespeichert werden.

#!/usr/bin/python
# -*- coding: utf-8 -*-
import re

datei = open("/etc/group", "r")

print "User mit Sekundärgruppen"

for zeile in datei:
    m = re.match(r"(?P<Group>\w+)(?=:x:\d+:\S+):x:\d+:(?P<Mitglieder>.*)", zeile)
    if m is not None:
        print "User", m.group('Mitglieder'), "ist/sind in der Gruppe", m.group('Group')

datei.close()
Align=none Ausgabe

user@localhost:~$ ./gruppen2.py
User mit Sekundärgruppen
User haldaemon,tandar ist/sind in der Gruppe cdrom
User haldaemon,tandar ist/sind in der Gruppe floppy
... weitere Ausgabe...

Der Ausdruck (?P<Group>\w+)(?=:x:\d+:\S+) besagt, dass nur dann wenn nach dem letzten Doppelpunkt noch Text folgt, der Gruppe mit dem Namen Group der Unix-Gruppenname zugewiesen werden soll. Es wird hier also eine Vorschau auf kommende Suchergebnisse betrieben. Diese Suchergebnisse werden jedoch nicht gespeichert. Anschließend wird der Zwischenbereich im Ausdruck :x:\d+: konsumiert und mit (?P<Mitglieder>.*) die Mitgliederliste aufgenommen.

Weiterführende Hinweise[Bearbeiten]

Reguläre Ausdrücke, wie wir sie in diesem Kapitel besprochen haben, sind recht kurz und wurden nur auf wenige Textzeilen angewendet. Hat man aber wirklich die Aufgabe, große Logdateien zu untersuchen, bietet es sich an, Ausdrücke vorzubereiten. Ausdrücke können mit der Methode compile(Ausdruck, Flags) vorbereitet werden, um dann mit match() und search() abgearbeitet zu werden. Solche kompilierten Ausdrücke sind schneller.

Folgendes Beispiel demonstriert das Vorgehen, wobei wir hier /var/log/messages (oder /var/log/syslog unter z.B. Ubuntu) auswerten:

#!/usr/bin/python
# -*- coding: utf-8 -*-
import re

datei = open("/var/log/messages", "r")
exp = re.compile(r"Jan 27 (?P<Zeit>\d+:\d+:\d+) .*? (?P<Ereignis>.*)")

print "Wann passierte heute was?"

for zeile in datei:
    m = exp.match(zeile)
    if m is not None:
        print m.group('Zeit'), m.group('Ereignis')
datei.close()

Anders als bei in vorherigen Abschnitten vorgestellten Methoden wird hier zuerst ein Objekt erzeugt, welches dem kompilierten Ausdruck entspricht. Dessen Methoden sind ebenfalls unter anderem match() und search(). exp.match(zeile) ist hier analog zu sehen zu re.match(Ausdruck, Zeile).

Zusammenfassung[Bearbeiten]

In diesem Kapitel wurden die Möglichkeiten regulärer Ausdrücke in Python vorgestellt. Im Kapitel Netzwerk lernen Sie weitere nützliche Anwendungen kennen.