Reguläre Ausdrücke zum Ansehen
·
Kursüberblick
1
Die verschwundene Zahlung: Text wörtlich finden
2
Der Fluchtwagen: Zeichenklassen
3
Das zerrissene Telefonbuch: Quantoren
4
Die falschen Anmeldungen: Anker und Validierung
5
Daten in jedem Format: Gruppen und Alternativen
6
Der Zugriff um 3 Uhr nachts: benannte Gruppen und Match-Objekte
7
Die Nacht vor der Story: Ersetzen und Aufteilen
8
Die Herausforderung des Backtrackers: gierig, genügsam, possessiv
9
Die Regeln des Tresors: Lookaround
10
Gleiche Masche: Rückverweise und Bedingungen
11
Der unlesbare Regex: Flags und Kompilieren
12
Die Enttarnung des Backtrackers: Rückblick und ein Tokenizer
Zeit 4:10–5:00
CASE-006
Grep & Co. Datendetektei
Fallakte
Auftraggeber
Frau Jeong, Betreiberin des Onlineshops „Markt Jeong“
Beweismittel
Das Zugriffsprotokoll des Webservers von Markt Jeong, aus genau der Nacht, in der die falschen Anmeldungen hereinkamen.
Zum Abschluss des Falls
Zerleg eine Logzeile in benannte Felder (ein dict) und finde die IP, die um 3 Uhr nachts die Admin-Seite (/admin) aufgerufen hat.
Frau Jeong ist wieder da, diesmal gleich mit dem ganzen Laptop. „Die falschen Konten kamen doch nachts. Und genau um die Zeit soll jemand die Admin-Oberfläche geöffnet haben. Dabei hab ich da geschlafen.“
Die Chefin überfliegt den Bildschirm einmal. „Ein Webserver-Log. Rund sieben Felder pro Zeile. Mit Nummern kommst du da durcheinander. Gib ihnen Namen.“
203.0.113.7 - - [20/Oct/2026:02:58:40 +0900] "GET /products/42 HTTP/1.1" 200 5123
198.51.100.23 - - [20/Oct/2026:03:01:05 +0900] "POST /login HTTP/1.1" 302 0
203.0.113.66 - - [20/Oct/2026:03:02:11 +0900] "GET /admin HTTP/1.1" 200 8890
203.0.113.66 - - [20/Oct/2026:03:02:19 +0900] "GET /admin/users?page=2 HTTP/1.1" 200 12044
198.51.100.4 - - [20/Oct/2026:09:15:30 +0900] "GET /admin HTTP/1.1" 200 8890
203.0.113.7 - - [20/Oct/2026:09:16:02 +0900] "GET /cart HTTP/1.1" 200 2210Eine Zeile enthält von links nach rechts: die IP des Zugriffs, zwei leere Felder (-), die Uhrzeit in eckigen Klammern, die Anfrage in Anführungszeichen (Methode, Pfad, Protokoll), den Statuscode und die Anzahl gesendeter Bytes.
In Lektion 5 hast du Gruppen über Nummern wie group(1) und group(2) angesprochen. Bei mehr als sieben Gruppen kann sich niemand mehr merken, welche Nummer was ist. Um einer Gruppe einen Namen zu geben, schreibst du direkt hinter die öffnende Klammer ?P<name>.
import re
line = '203.0.113.66 - - [20/Oct/2026:03:02:11 +0900] "GET /admin HTTP/1.1" 200 8890'
m = re.search(r"(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\]", line)
print(m.group("ip")) # 203.0.113.66
print(m["time"]) # 20/Oct/2026:03:02:11 +0900
print(m.group("ip", "time")) # ('203.0.113.66', '20/Oct/2026:03:02:11 +0900')
print(m.groupdict()) # {'ip': '203.0.113.66', 'time': '20/Oct/2026:03:02:11 +0900'}m.group("ip") und m["ip"] sind dasselbe. Gibst du mehrere Namen an, kommt ein Tupel zurück.groupdict() sammelt nur die benannten Gruppen und liefert ein Dictionary {name: text}. Aus einer Logzeile wird so ein Dictionary.-, gibt es einen Fehler.Auch benannte Gruppen behalten ihre Nummer. Gezählt wird genau wie in Lektion 5 in der Reihenfolge der öffnenden Klammern. Der Name ist nur ein Spitzname, der auf der Nummer klebt.
print(m.group(1)) # 203.0.113.66
print(m[2]) # 20/Oct/2026:03:02:11 +0900
print(m.groups()) # ('203.0.113.66', '20/Oct/2026:03:02:11 +0900')Beweislabor
re.search(r"", text)
Rückgabewert
Dots Tipp
Vorsicht, wenn du aus anderen Sprachen (?<name>...) kennst. Lässt du in Python das P weg, gibt es den Fehler "unknown extension ?<n". Und vergibst du denselben Namen zweimal, heißt es "redefinition of group name". Ich hab ja auch nur einen Namen.
Jetzt bekommt jedes Feld der Zeile einen Namen. Die Leerzeichen, Anführungszeichen und eckigen Klammern zwischen den Feldern schreibst du wörtlich ins Muster.
LINE_RE = r'(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] "(?P<method>[A-Z]+) (?P<path>\S+) [^"]*" (?P<status>\d{3}) (?P<size>\d+)'
m = re.fullmatch(LINE_RE, line)
print(m.groupdict())
# {'ip': '203.0.113.66', 'time': '20/Oct/2026:03:02:11 +0900', 'method': 'GET', 'path': '/admin', 'status': '200', 'size': '8890'}r'...'.[^\]]+ heißt „Zeichen, die keine schließende eckige Klammer sind“, [^"]* heißt „Zeichen, die kein Anführungszeichen sind“. Die verneinten Klassen aus Lektion 2 markieren die Grenzen der Felder.fullmatch aus Lektion 4. Kaputte Zeilen ergeben None und fallen sofort auf.Ist die Zeile erst ein Dictionary, geht es mit ganz normalem Python weiter. Nimm an, die sechs Logzeilen von oben stecken im String access_log.
rows = [re.fullmatch(LINE_RE, l).groupdict() for l in access_log.splitlines()]
print(len(rows)) # 6
print(rows[1]["method"], rows[1]["path"], rows[1]["status"]) # POST /login 302Alle Werte sind Strings. Willst du die Bytes zusammenzählen, musst du sie selbst umwandeln, etwa mit int(row["size"]).
Beweislabor
re.search(r"", text)
Rückgabewert
Das Match-Objekt verrät dir für jede Gruppe nicht nur, was gefunden wurde, sondern auch wo. Gibst du eine Gruppennummer oder einen Namen als Argument an, bekommst du die Position dieser Gruppe.
m = re.fullmatch(LINE_RE, line)
print(m.start("path"), m.end("path")) # 51 57
print(m.span("status")) # (68, 71)
print(line[m.start("path"):m.end("path")]) # /adminOptionale Gruppen passen vielleicht gar nicht, etwa ein Query-String wie ?page=2 hinter dem Pfad. Bei einer Gruppe ohne Treffer ist der Text None und die Position -1.
P = r"(?P<path>/[^\s?]*)(?:\?(?P<query>\S+))?"
a = re.search(P, "GET /admin HTTP/1.1")
b = re.search(P, "GET /admin/users?page=2 HTTP/1.1")
print(a.group("query"), a.span("query")) # None (-1, -1)
print(b.group("query"), b.span("query")) # page=2 (17, 23)Mischt sich None unter die Werte, kracht es bei der Stringverarbeitung schnell. groups() und groupdict() nehmen deshalb ein Argument default, das statt None eingesetzt wird.
print(a.groups()) # ('/admin', None)
print(a.groups(default="")) # ('/admin', '')
print(a.groupdict(default="-")) # {'path': '/admin', 'query': '-'}Beweislabor
re.search(r"", text)
Rückgabewert
Diese Attribute brauchst du selten, aber beim Debuggen sind sie Gold wert.
m = re.search(r"(?P<method>[A-Z]+) (?P<path>\S+)", line)
print(m.lastindex, m.lastgroup) # 2 path
print(m.pos, m.endpos, len(line)) # 0 76 76
print(m.string is line) # True
print(m.re) # re.compile('(?P<method>[A-Z]+) (?P<path>\\S+)')lastindex ist die Nummer der zuletzt geschlossenen Gruppe, lastgroup ihr Name. Hat sie keinen Namen oder hat keine Gruppe gepasst, ist der Wert None.lastindex bei ((\d+)\.(\d+)) nicht 3, sondern 1.| wie in (?P<a>x)|(?P<b>y), verrät lastgroup, welche Seite gepasst hat. In Lektion 12 baust du mit dieser Eigenschaft einen Tokenizer.pos und endpos sind die Grenzen des Suchbereichs. Mit den Modulfunktionen sind sie immer 0 und die Länge des Strings. Wie du den Bereich selbst festlegst, lernst du in Lektion 11 bei re.compile.string ist der durchsuchte Originalstring, re das verwendete Musterobjekt.n = re.search(r"((\d+)\.(\d+))", "v3.14")
print(n.lastindex, n.lastgroup) # 1 None
n = re.search(r"(?P<a>x)|(?P<b>y)", "y")
print(n.lastindex, n.lastgroup) # 2 bMatch-Objekte sind unveränderlich, copy.copy(m) und copy.deepcopy(m) sind also erlaubt. Heraus kommt genau dasselbe Objekt.
Zum Schluss: expand(template) setzt das Ergebnis eines Treffers in eine Vorlage ein und baut daraus einen neuen String. Jedes \g<name> in der Vorlage wird durch den Text dieser Gruppe ersetzt. Das ist genau dieselbe Syntax wie bei den Ersetzungsstrings in Lektion 7. Im Beispiel unten steht 요청 für „Anfrage“.
print(m.expand(r"\g<method> 요청: \g<path>")) # GET 요청: /adminJetzt kommen die Bedingungen des Falls ins Muster. Im Zeitfeld [20/Oct/2026:03:02:11 +0900] sind die zwei Ziffern nach dem ersten : hinter dem Datum die Stunde. Mit \[[^:]+: springst du über das Datum, und schreibst du an diese Stelle 03, hast du die Bedingung „3 Uhr nachts“.
03 oder :03:, erwischst du auch Zeilen, in denen die Minute oder Sekunde 03 ist. Entscheidend ist, die Stelle festzunageln, an der die 03 stehen muss./admin\S*, damit alle Seiten erfasst werden, die mit /admin beginnen.Beweislabor
re.search(r"", text)
Rückgabewert
Eine mögliche Lösung:
(?P<ip>\S+) \S+ \S+ \[[^:]+:(?P<hour>03):[^\]]+\] "GET (?P<path>/admin\S*)Du zerlegst die Uhrzeit vollständig in Tag, Monat, Jahr, Stunde, Minute und Sekunde, gibst allem Namen und holst dir die Werte Zeile für Zeile wie aus einem Dictionary, um die Bedingungen zu prüfen. Dank der benannten Gruppen liest sich die Bedingung fast wie ein normaler Satz.
TIME_RE = (r'(?P<ip>\S+) \S+ \S+ '
r'\[(?P<day>\d\d)/(?P<mon>\w{3})/(?P<year>\d{4}):(?P<hour>\d\d):(?P<min>\d\d):(?P<sec>\d\d) [^\]]+\] '
r'"(?P<method>[A-Z]+) (?P<path>\S+)')
suspects = set()
for l in access_log.splitlines():
m = re.match(TIME_RE, l)
if m and m["hour"] == "03" and m["path"].startswith("/admin"):
print(m["hour"] + ":" + m["min"], m["ip"], m["path"])
suspects.add(m["ip"])
# 03:02 203.0.113.66 /admin
# 03:02 203.0.113.66 /admin/users?page=2
print(suspects) # {'203.0.113.66'}Um 3:02 Uhr nachts hat 203.0.113.66 die Admin-Oberfläche geöffnet und acht Sekunden später sogar Seite 2 der Mitgliederliste abgerufen. 198.51.100.4 um 9 Uhr morgens war die IP aus Frau Jeongs Büro. Frau Jeong wird kreidebleich. „Die Mitgliederliste? Dann sind die Kundendaten …“
Fall abgeschlossen — Am 20. Oktober um 03:02 hat die externe IP 203.0.113.66 auf die Admin-Seite zugegriffen und die Mitgliederliste eingesehen. Empfohlen: Admin-Passwort sofort ändern und Zugriffe auf bestimmte IPs beschränken. Ein Abfluss der Mitgliederdaten ist möglich.
LINE_RE ein und addiere size aller Zeilen mit dem Statuscode (status) 200. Vergiss int() nicht."2026-10-20" mit den benannten Gruppen (?P<y>\d{4})-(?P<m>\d\d)-(?P<d>\d\d) und überleg dir, was m.expand(r"\g<d>/\g<m>/\g<y>") ergibt.re.search(r"(?P<a>\d)(?P<b>x)?", "7") jeweils span("b"), groupdict(default="?") und lastgroup? Erst überlegen, dann prüfen.Das Notizbuch der Direktorin
(?P<name>...) gibt einer Gruppe einen Namen. m.group("name"), m["name"], m.groupdict().
Auch benannte Gruppen haben Nummern. Gezählt wird nach den öffnenden Klammern.
start(g), end(g), span(g). Gruppen ohne Treffer: Text None, Position -1.
groups(default=...) und groupdict(default=...) setzen statt None einen anderen Wert ein.
lastindex ist die Nummer der zuletzt geschlossenen Gruppe, lastgroup ihr Name. Verrät, welche der mit | verbundenen benannten Gruppen gepasst hat.
m.pos, m.endpos, m.string, m.re. Mit m.expand(r"\g<name>") in eine Vorlage einsetzen.
← Zurück
Lektion 5 · Daten in jedem Format: Gruppen und Alternativen
Weiter →
Lektion 7 · Die Nacht vor der Story: Ersetzen und Aufteilen
Über neue Lektionen informiert werden
Wir schicken dir eine E-Mail, wenn dieser Kurs neue Lektionen bekommt oder ein Folgekurs erscheint.
R
ReflexKR
Python und Reflex zum Anfassen. Alle Beispiele wurden mit Reflex 0.10.0 ausgeführt und geprüft.
ReflexKR ist kostenlos und ohne Werbebanner. Wenn es dir geholfen hat, kannst du es mit einem Kaffee unterstützen. ☕ Spendier uns einen Kaffee
Hinweis zu Affiliate-Links© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Fragen & Antworten
Frag alles zu dieser Lektion. Fragen aus allen Sprachversionen dieser Seite erscheinen hier gemeinsam.