in diesem Thread geht es um die Anwendung von regulären Ausdrücken in Python.
Am Ende dieser Lerneinheit solltet ihr verstanden haben, welche Eigenheiten Python im Kontext mit sich bringt und mit regulären Ausdrücken darin sinnvoll arbeiten können. Hilfreich sind grundlegende Python-Kenntnisse und natürlich die vorhergehenden Einheiten von Meillo.
Kurzeinstieg in Python
Für Neueinsteiger: Python ist eine sogenannte Interpretersprache, man kann also Code ausführen, ohne dass eine Kompilierung notwendig wäre (vergleichbar mit der Shell).
Im Folgenden werde ich längere Code-Snippets in Form eines kompletten ausführbaren Scripts halten, und einzelne Zeilen mit dem Interpreter-Prompt von Python markieren, ">>>" (analog zum Dollarzeichen der bash).
Getestet habe ich die Code-Teile übrigens mit Python 3.7, aber jede Python-Version ab 3.x aus den letzten Jahren sollte verwendbar sein.
Grundlegende Verwendung
Kurzer kommentierter Stoß ins kalte Wasser, um ein vollständiges Beispiel zu haben:
Code: Alles auswählen
# Die RegEx-Funktionalitäten stecken in diesem Modul der Python standard library
import re
text = "Ich weiß gar nicht, was ich alles nicht weiß, aber es ist sicher viel"
# re.findall sucht nach allen Vorkommen des Ausdrucks, ähnlich wie grep, und gibt sie als Liste zurück
kurze_woerter = re.findall(r"[A-Za-z]{1,4}", text)
print(kurze_woerter)
1. Zu Beginn importieren wir das Modul re aus der Standardbibliothek von Python. Die Doku von Python ist recht gut und alles, was ich hier über dieses Modul erwähne, wird auch hier erklärt: https://docs.python.org/3/library/re.html
2. Eine Variable mit etwas Beispieltext wird zugewiesen.
3. Die Funktion findall aus dem Modul re wird mit einem regulären Ausdruck und dem Beispieltext aufgerufen und gibt eine Liste mit allen Funden zurück, welche der Variable kurze_woerter zugewiesen wird.
4. Diese Liste wird (auf der Konsole) ausgegeben.
"raw-Strings"
Das erste auffällige Element in dem Script ist die Verwendung des Zeichen r vor dem in Anführungszeichen umschlossenen Ausdrucks. In Episode 2 des Kurs haben wir gelernt, dass wir abhängig von der Umgebung Zeichen escapen müssen. Hier trifft das ebenso zu, wenn auch nicht in diesem Beispiel. Möchte man aber in Python backslashes verwenden, muss man diese entweder mit einem weiteren backslash escapen oder den String mit dem r zu einem raw-String machen.
Code: Alles auswählen
>>> print("Hallo\nWelt")
Hallo
Welt
>>> print(r"Hallo\nWelt")
Hallo\nWelt
Kompilierte Ausdrücke
Im Beispiel verwende ich re.findall aus dem Modul heraus und übergebe einen raw-String. Das ist kompakt und in diesem Beispiel ohne Nachteile. Möchte ich aber den gleichen Ausdruck mehrfach verwenden (in einer Schleife oder an einer zweiten Stelle im Code), bietet es sich an, den Ausdruck zu "kompilieren":
Code: Alles auswählen
>>> hex_digits = re.compile(r"[0-9a-f]")
>>> hex_digits.findall("ff00ad")
['f', 'f', '0', '0', 'a', 'd']
Anstelle des Moduls verwendet man dann den kompilierten Ausdruck beim Aufruf, und der erste Parameter zu findall entfällt (logischerweise).
Dialekte: Python re, ERE, PCRE
Der Dialekt von Python orientiert sich an der Perl-Implementierung (PCRE), ist jedoch eine eigenständige Implementierung.
Es gibt also Konstrukte wie \w und eher abenteuerliche Gruppenmodifikatoren (Gruppen kommen noch), man kann sich aber nicht völlig auf bisheriges Wissen verlassen und sollte im Zweifel die Doku konsultieren und einen Test für seine Anwendung schreiben. Da ich mit verschiedenen Tools arbeite und mir nie merken kann, welches Tool genau welche Features unterstützt, mache ich das auch nicht anders.
Eine gute Zusammenfassung habe ich hier gefunden: https://stackoverflow.com/questions/339 ... bre-or-ere
Ein Vergleich mit anderen Dialekten hier: https://remram44.github.io/regex-cheatsheet/regex.html
Funktionen im Modul
Bisher habe ich mich auf findall beschränkt, es gibt aber noch einige weitere Funktionen. Die meisten davon arbeiten mit Match-Objekten, welche neben dem gematchten Text auch noch einige weitere Informationen enthalten. Den ersten vollständigen Treffer erhält man mit .group().
1. search: sucht an einer beliebigen Stelle im Suchtext nach einem Treffer und gibt diesen (ersten) als Match-Objekt zurück oder None, wenn nichts gefunden wurde.
Code: Alles auswählen
>>> re.search("Erd(apfel|birne)|Kartoffel", "Grumbeere Erdbirne Erdapfel").group()
'Erdbirne'
>>>
Code: Alles auswählen
>>> re.match("Erd(apfel|birne)|Kartoffel", "Grumbeere Erdbirne Erdapfel")
# Keine Ausgabe, Rückgabewert None - kein Treffer!
>>> re.match("Erd(apfel|birne)|Kartoffel", "Erdbirne Erdapfel").group()
'Erdbirne'
# Ohne group sehen wir nur das match-Objekt:
>>> re.match("Erd(apfel|birne)|Kartoffel", "Erdbirne Erdapfel")
<_sre.SRE_Match object at 0x7f6171b7e648>
>>>
Code: Alles auswählen
>>> re.match("Erd(apfel|birne)|Kartoffel", "Erdbirnen").group()
Erdbirne
>>> re.fullmatch("Erd(apfel|birne)|Kartoffel", "Erdbirnen")
# nichts
Code: Alles auswählen
>>> re.findall("Erd(apfel|birne)|Kartoffel", "Grumbeere Erdbirne Erdapfel")
['birne', 'apfel']
# "Entschärft", noncapturing group
>>> re.findall("Erd(?:apfel|birne)|Kartoffel", "Grumbeere Erdbirne Erdapfel")
['Erdbirne', 'Erdapfel']
>>>
Code: Alles auswählen
>>> food = re.finditer("Erd(apfel|birne)|Kartoffel", "Grumbeere Erdbirne Erdapfel")
>>> food
<callable-iterator object at 0x7f6171b85050>
# Ein Weg, um einen Iterator zu durchlaufen, ist next()
>>> next(food)
<_sre.SRE_Match object at 0x7f6171b7e6c0>
>>> next(food)
<_sre.SRE_Match object at 0x7f6171b7e738>
# Und das passiert, wenn alle Treffer durchlaufen wurden:
>>> next(food)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Code: Alles auswählen
>>> re.sub("Erd(apfel|birne)|Kartoffel", "Schokolade", "Grumbeere Erdbirne Erdapfel")
'Grumbeere Schokolade Schokolade'
>>>
Jede dieser Funktionen unterstützt Modifikatoren, wie beispielsweise das Ignorieren von Groß- und Kleinschreibung:
Code: Alles auswählen
>>> re.findall("debianforum.de", "Debianforum.de", re.IGNORECASE)
['Debianforum.de']
>>>
Code: Alles auswählen
>>> re.findall("^debianforum", "Debianforum.de\ndas beste Forum auf der ganzen Welt\ndebianforum ist toll", re.I)
['Debianforum']
>>>
Code: Alles auswählen
>>> re.findall("^debianforum", "Debianforum.de\ndas beste Forum auf der ganzen Welt\ndebianforum ist toll", re.I | re.M)
['Debianforum', 'debianforum']
>>>
Es gibt noch ein paar weitere Flags, die ihr gerne im verlinkten Abschnitt der Dokumentation nachschlagen könnt - brauchen werdet ihr die aber eher selten. re.VERBOSE ist für komplizierte Ausdrücke möglicherweise hilfreich und re.DEBUG könnte vielleicht auch irgendwann einmal helfen, einen "historisch gewachsenen" Ausdruck zu zerlegen.
Im Übrigen wäre es hier natürlich sinnvoll gewesen, die Ausdrücke zu kompilieren... aber dann wären die Einzelabschnitte nicht mehr separat les-/nutzbar gewesen.
Gruppen/Unterausdrücke
In Kapitel 4 haben wir "Unterausdrücke" kennengelernt. Python nennt sie "subgroups", also Untergruppen - die Benennung ist aber meiner bescheidenen Meinung nach nicht ganz einheitlich, wenn man auf die Funktionen schaut.
Aus dem vorigen Kapitel stammt folgender Ausdruck:
Code: Alles auswählen
>>> re.search("Erd(apfel|birne)|Kartoffel", "Erdbirne").groups()
('birne',)
Code: Alles auswählen
>>> re.search("Erd(apfel|birne)|Kartoffel", "Grumbeere Erdbirne Erdapfel").group()
'Erdbirne'
>>> re.search("Erd(apfel|birne)|Kartoffel", "Grumbeere Erdbirne Erdapfel").group(0)
'Erdbirne'
>>> re.search("Erd(apfel|birne)|Kartoffel", "Grumbeere Erdbirne Erdapfel").group(1)
'birne'
>>>
Code: Alles auswählen
>>> match = re.match(r"(?P<vorname>\w+) (?P<nachname>\w+)", "Linus Torvalds")
>>> match.groups()
('Linus', 'Torvalds')
>>> match.group()
'Linus Torvalds'
>>> match.groupdict()
{'nachname': 'Torvalds', 'vorname': 'Linus'}
>>> match.groupdict()["nachname"]
'Torvalds'
Code: Alles auswählen
>>> for match in re.finditer(r"(?P<vorname>\w+) (?P<nachname>\w+)", "Linus Torvalds, Sebastian Feltel, Meillo Meillo"):
... print(match.groupdict())
...
{'nachname': 'Torvalds', 'vorname': 'Linus'}
{'nachname': 'Feltel', 'vorname': 'Sebastian'}
{'nachname': 'Meillo', 'vorname': 'Meillo'}
>>>
Code: Alles auswählen
>>> re.sub("Erd(apfel|birne)|Kartoffel", r"Glueh\1", "Grumbeere Erdbirne Erdapfel")
'Grumbeere Gluehbirne Gluehapfel'
>>>
Man kann Gruppen auch beliebig verschachteln, die Lesbarkeit nimmt allerdings stark ab:
Code: Alles auswählen
>>> match = re.match(r"Meine (?P<what>(?P<howmany>[0-9]+) (\w+) Ausdruecke)", "Meine 5 stoerrischen Ausdruecke")
>>> match
<_sre.SRE_Match object at 0x7f6171c23880>
>>> match.groupdict()
{'what': '5 stoerrischen Ausdruecke', 'howmany': '5'}
>>> match.groups()
('5 stoerrischen Ausdruecke', '5', 'stoerrischen')
Die Verschachtelung von Gruppen mit Quantifizierung... das geht, es matcht, aber der Zugriff auf die Untergruppen ist weder numerisch noch benannt nur sehr beschränkt möglich - lasst es lieber (für den Fall, dass ich damit jemandem ein grausames Schicksal ersparen kann).
Typische Anwendungen
Ich kann mit curl und grep sehr viele Informationen aus Webseiten extrahieren, aber dann? Die Datenverarbeitung in der bash ist ziemlich limitiert. Mit Python könnt ihr vielerlei Quellen anzapfen (Datenbanken, Webseiten, spezielle Dateiformate) und die extrahierten Informationen direkt weiterverwenden, und dazwischen allerlei Hilfsmittel einsetzen, um die Daten zu validieren und beispielsweise wieder in strukturierter Form abzulegen.
Für gewöhnlich verwende ich das 3rd-Party-Modul requests, python3-requests, für HTTP-Requests. Auch die offizielle Doku empfiehlt dies, die Installation lohnt sich auf jeden Fall. Für das folgende Beispiel ist das notwendig.
Nehmen wir an, ihr hättet eine Liste von Youtube-Links. Ihr hättet gerne die Titel. Aus Erfahrung kann ich sagen, dass ein einfaches curl und die Suche nach dem title-Element bei Youtube nicht gut funktionieren. Dank eggy kenne ich aber noch einen Weg - wenn auch ein wenig umständlich. Für Python jedoch kein Problem. Man könnte damit nun durch den Musik-Thread oder die sqlite-DB von gajim iterieren - eine Fingerübung für interessierte, die dann aber nicht mehr mit Regex zu tun hat
Das Programm (herausgelöst aus anderem Code): 41710
Also aus einem beliebigen Text (hier: nur eine) Youtube-URL identifizieren (in verschiedenen Formaten), die ID extrahieren und über den kleinen Umweg den Titel aus einer HTTP-Antwort klauben.
Übung 2 bietet Gelegenheit, etwas ähnliches zusammenzubasteln.
Performance
Python ist nicht besonders schnell, wenn man es mit kompilierten Sprachen vergleicht. Das heißt aber nicht, dass man keinen Unterschied merkt zwischen der erstbesten Implementierung und einer optimierten.
Für reguläre Ausdrücke bedeutet das:
1. Ausdrücke kompilieren (bereits erwähnt)
2. Suchraum minimieren
3. Lieber zwei kleine Ausdrücke als ein großer über beide Bereiche, wenn es nicht notwendig ist
Die Liste ist sicher unvollständig.
Welchen Unterschied macht dies nun? Findet das in der Übung 3 heraus.
Übungen
1. Schreibe einen Ersatz für ein sehr einfaches (p)grep (nur stdin, keine Parameter) in Python. Hinweis:
Code: Alles auswählen
import sys
inputstream = sys.stdin
Nachtrag: mir ist aufgefallen, dass ich meine Gedanken bezüglich grep-Präfixe/Dialekte vermischt hab. Ich lass das mal für beide Varianten stehen:
a) pgrep (process grep)
b) grep (mit Perl-Dialekt, also wie Python eben arbeitet - darauf wollte ich eigentlich hinaus, dachte an egrep)
Werft eure Lösungen nicht über den Haufen, beides ist interessant. Sorry für die Verwirrung.
2. Suche aus einer Forenübersichtsseite alle Threads heraus und gib sie nach dem Erstellungsdatum sortiert aus, im Format "Titel; Datum; Threadstarter; Anzahl Antworten".
Hinweis für Einsteiger:
Code: Alles auswählen
import requests
import re
# HTML des "Netzwerk"-Unterforums
dfde = requests.get("https://debianforum.de/forum/viewforum.php?f=30").text
# nun hat man alles 💩 in der Hand und kann auf dfde matchen
3. Folgendes Programm ist nicht so schnell, wie es sein könnte. Seine Aufgabe ist es, aus man-pages die Sektionen auszugeben. Korrekt ist für man man also:
['NAME', 'SYNOPSIS', 'DESCRIPTION', 'EXAMPLES', 'OVERVIEW', 'DEFAULTS', 'OPTIONS', 'ENVIRONMENT', 'FILES', 'HISTORY', 'BUGS']
Das Programm: 41709
Die Funktion timeit berechnet die Laufzeit einer Funktion über x Durchläufe, hier 100. Finde heraus, wo hier die Performance hakt und wieso so viele andere Worte ausgegeben werden. Hinweis: es gibt große und kleine Versäumnisse, auch inhaltlich. Auf meinem Pi 3 beginnt die Laufzeit bei 22 Sekunden und beträgt nach Optimierung etwa 0,4 Sekunden (auf meinem Desktop-Rechner von 2 auf 0.025, der Faktor ist also nicht in allen Fällen vergleichbar).
(Es gibt sicher noch genug andere Fehler, die man machen kann - wenn ihr die Performance am Ausdruck selbst verschlechtern könnt, ohne es ins Lächerliche zu ziehen, bin ich auf jene gespannt!)
4. Welche kleine Korrektur hätte für das Eingangsbeispiel genügt, damit es inhaltlich korrekt ist (kurze_woerter)?
Und nun viel Spaß