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 3:20–4:10
CASE-005
Grep & Co. Datendetektei
Fallakte
Auftraggeber
Frau Jeong, Betreiberin des Onlineshops „Markt Jeong“
Beweismittel
Die Bestellprotokolle von Markt Jeong. Jede Aushilfe hat das Datum in einem anderen Format eingetragen.
Zum Abschluss des Falls
Zieh aus jeder Bestellung Jahr, Monat und Tag einzeln heraus und bring alles in dasselbe Format.
Am Tag nach dem Fall mit den falschen Anmeldungen steht Frau Jeong wieder in der Tür. Diesmal seufzt sie zuerst. „Die Anmeldungen sind dicht. Aber jetzt will ich die Bestellungen nach Datum sortieren, und meine drei Aushilfen haben das Datum jede anders eingetragen.“ Die Chefin nimmt einen Schluck Kaffee. „Finden hast du in Teil 1 gelernt. Jetzt lernst du, aus dem Gefundenen etwas herauszuholen.“
So sehen die Bestellungen aus:
#1001 2026-10-20 ship=done
#1002 2026.10.20 ship=wait
#1003 2026/10/19 ship=done
#1004 20261019 ship=done
#1005 2026-10/20 ship=waitDas Datum nur zu finden, reicht nicht. Um aufzuräumen, musst du „2026“, „10“ und „20“ einzeln herausholen. Das Werkzeug dafür sind heute runde Klammern, also Gruppen.
In Lektion 3 hast du gelernt, dass ein Quantor nur am Baustein direkt davor hängt. Mit runden Klammern ( ) werden mehrere Zeichen zu einem Baustein.
re.fullmatch(r"ha+", "hahaha") # None
re.fullmatch(r"(ha)+", "hahaha") # <re.Match object; span=(0, 6), match='hahaha'>Auch Formen, in denen sich „Zahl und Punkt“ dreimal wiederholen, wie bei einer IP-Adresse, schreibst du mit einem Quantor an einer Gruppe.
re.search(r"(\d{1,3}\.){3}\d{1,3}", "ip=203.0.113.66")
# <re.Match object; span=(3, 15), match='203.0.113.66'>(\d{1,3}\.){3} heißt dreimal „1 bis 3 Ziffern und ein Punkt“. Hinter der letzten Zahl steht kein Punkt, deshalb kommt \d{1,3} noch extra dazu.
| heißt „links oder rechts“. Allerdings hat | im Regex den niedrigsten Vorrang von allen. Wenn du es nicht mit Klammern eingrenzt, teilt es das ganze Muster in zwei Hälften.
re.fullmatch(r"ship=done|wait", "wait") # <re.Match object; span=(0, 4), match='wait'>
re.fullmatch(r"ship=(done|wait)", "wait") # None
re.fullmatch(r"ship=(done|wait)", "ship=wait") # <re.Match object; span=(0, 9), match='ship=wait'>ship=done|wait heißt „ship=done oder wait“. Willst du „ship=, gefolgt von done oder wait“, setzt du nur die Auswahl in Klammern.
Besonders aufpassen musst du in Kombination mit Ankern. ^a|b$ heißt nicht „ein String, der nur aus a oder b besteht“, sondern „^a oder b$“.
re.findall(r"^a|b$", "a.b") # ['a', 'b']
re.search(r"^ok|no$", "say no") # <re.Match object; span=(4, 6), match='no'>say no beginnt nicht mit ok und passt trotzdem. Gemeint war vermutlich ^(ok|no)$.
Die Alternativen werden von links nach rechts ausprobiert, und die erste, die klappt, gewinnt. Die längste wird nicht gesucht.
re.search(r"10|100", "100") # <re.Match object; span=(0, 2), match='10'>
re.search(r"100|10", "100") # <re.Match object; span=(0, 3), match='100'>-|\.|/ ist dasselbe wie die Klasse [-./]. In solchen Fällen ist die Klasse kürzer und schneller.Beweislabor
re.search(r"", text)
Rückgabewert
Klammern haben neben dem Zusammenfassen noch eine zweite Aufgabe: Sie merken sich, was in ihnen gefunden wurde. Das nennt man eine Capture-Gruppe (fangende Gruppe), und sie werden in der Reihenfolge der öffnenden Klammern mit 1, 2, 3 … nummeriert.
Nimm an, die Bestellungen von oben stecken im String orders.
m = re.search(r"(\d{4})-(\d\d)-(\d\d)", orders)
print(m.group(0)) # 2026-10-20
print(m.group(1)) # 2026
print(m.group(2), m.group(3)) # 10 20
print(m.groups()) # ('2026', '10', '20')
print(m.group(1, 2)) # ('2026', '10')
print(m[1], m[0]) # 2026 2026-10-20group(0) (oder einfach group()) ist der ganze Treffer. Ab Nummer 1 kommen die Gruppen.groups() liefert alle Gruppen ab Nummer 1 als Tupel.group(1, 2) mehrere Nummern an, kommt ein Tupel zurück.m[1] ist die Kurzform von m.group(1).Auch bei verschachtelten Klammern gilt dieselbe Regel: Gezählt wird in der Reihenfolge der öffnenden Klammern (.
((\d{4})-(\d\d))-(\d\d)
12 3 4m = re.search(r"((\d{4})-(\d\d))-(\d\d)", "2026-10-20")
print(m.groups()) # ('2026-10', '2026', '10', '20')Beweislabor
re.search(r"", text)
Rückgabewert
Oft willst du nur zusammenfassen, ohne dir etwas zu merken, etwa um die Reichweite von | festzulegen oder einen Quantor anzuhängen. Dafür gibt es (?:...). Es fasst zusammen, bekommt aber keine Nummer.
m = re.search(r"(?:\d{4})-(\d\d)", "2026-10")
print(m.group(1)) # 10
print(re.fullmatch(r"(?:ab)+", "abab").groups()) # ()Weil die erste Klammer keine Nummer bekommt, ist (\d\d) jetzt Gruppe 1. Fängst du nur, was du brauchst, bleiben die Nummern übersichtlich, und du umgehst die findall-Falle aus dem nächsten Abschnitt.
Hat das Muster Capture-Gruppen, verhält sich findall anders.
| Anzahl Capture-Gruppen | findall liefert |
|---|---|
| 0 | eine Liste der ganzen Treffer |
| 1 | eine Liste der Strings dieser Gruppe |
| 2 oder mehr | eine Liste von Tupeln der Gruppen |
re.findall(r"\d{4}[-./]\d\d[-./]\d\d", orders)
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']
re.findall(r"(\d{4})[-./]\d\d[-./]\d\d", orders)
# ['2026', '2026', '2026', '2026']
re.findall(r"(\d{4})[-./](\d\d)[-./](\d\d)", orders)
# [('2026', '10', '20'), ('2026', '10', '20'), ('2026', '10', '19'), ('2026', '10', '20')]Brauchst du Jahr, Monat und Tag, ist die Tupelliste praktisch. Das Problem: Auch Klammern, die du nur zum Zusammenfassen gesetzt hast, werden zu Gruppen.
re.findall(r"\d{4}(-|\.|/)\d\d(-|\.|/)\d\d", orders)
# [('-', '-'), ('.', '.'), ('/', '/'), ('-', '/')]Gewollt war das Datum, zurück kamen nur die Trennzeichen. Brauchst du den ganzen Treffer, mach aus den Klammern (?:...) oder hol dir mit finditer die Match-Objekte und nimm group().
re.findall(r"\d{4}(?:-|\.|/)\d\d(?:-|\.|/)\d\d", orders)
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']
[m.group() for m in re.finditer(r"\d{4}(-|\.|/)\d\d(-|\.|/)\d\d", orders)]
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']Dots Tipp
Wenn findall seltsam kurze Ergebnisse oder Tupel liefert, zähl zuerst die Klammern im Muster. Ganz oft hat sich eine Klammer, die nur zusammenfassen sollte, heimlich in eine Gruppe verwandelt. Darum kam in Lektion 3 bei (ha)+ mit findall auch nur 'ha' heraus.
Beweislabor
re.search(r"", text)
Rückgabewert
Nimmt eine Gruppe am Treffer nicht teil, steht an ihrer Stelle None. Das passiert, wenn eine Gruppe mit ? null-mal gepasst hat oder bei | die andere Seite gewählt wurde.
m = re.fullmatch(r"(\d{4})-(\d\d)(-(\d\d))?", "2026-10")
print(m.groups()) # ('2026', '10', None, None)
print(m.group(3)) # None
print(re.search(r"(a)|(b)", "b").groups()) # (None, 'b')Hängt an einer Gruppe ein Quantor und passt sie mehrmals, bleibt in der Gruppe nur der letzte Durchgang übrig.
m = re.fullmatch(r"(\d)+", "2026")
print(m.group(0), m.group(1)) # 2026 6
re.findall(r"(\d{1,3}\.){3}\d{1,3}", "a=203.0.113.66 b=198.51.100.7")
# ['113.', '100.']
re.findall(r"(?:\d{1,3}\.){3}\d{1,3}", "a=203.0.113.66 b=198.51.100.7")
# ['203.0.113.66', '198.51.100.7'](\d)+ hat viermal gepasst, aber in Gruppe 1 ist nur die letzte 6 geblieben. Auch bei der IP-Adresse blieb in der Gruppe nur der dritte Durchgang 113. übrig, und findall hat genau diese Gruppe zurückgegeben. Willst du das IP-Muster aus 5-1 mit findall benutzen, ist (?:...) die richtige Wahl.
Die drei Trennzeichen -, . und / deckst du mit [-./] ab, das trennzeichenlose 20261019 mit ?. Jahr, Monat und Tag fängst du jeweils einzeln.
Die Ausgabe ist koreanisch formatiert: 년 heißt Jahr, 월 Monat und 일 Tag.
DATE = r"(\d{4})[-./]?(\d\d)[-./]?(\d\d)"
for m in re.finditer(DATE, orders):
y, mo, d = m.groups()
print(f"{y}년 {int(mo)}월 {int(d)}일")
# 2026년 10월 20일
# 2026년 10월 20일
# 2026년 10월 19일
# 2026년 10월 19일
# 2026년 10월 20일Willst du die Bestellnummer gleich mit herausholen, kommt eine weitere Gruppe dazu. Bei vier Gruppen liefert findall Tupel mit vier Feldern.
re.findall(r"#(\d+) " + DATE, orders)
# [('1001', '2026', '10', '20'), ('1002', '2026', '10', '20'), ('1003', '2026', '10', '19'), ('1004', '2026', '10', '19'), ('1005', '2026', '10', '20')]Beweislabor
re.search(r"", text)
Rückgabewert
Fall abgeschlossen — Die Daten aller 5 Bestellungen wurden in Jahr, Monat und Tag zerlegt und vereinheitlicht. Bei #1005 wurden allerdings gemischte Trennzeichen eingegeben, das muss das Personal noch prüfen.
Frau Jeong nimmt die aufgeräumte Tabelle entgegen und zeigt auf die letzte Zeile. „#1005 ist 2026-10/20. Das ist auch einfach durchgerutscht, oder?“
re.fullmatch(DATE, "2026-10/20")
# <re.Match object; span=(0, 10), match='2026-10/20'>Stimmt. Die beiden [-./]? wissen nichts voneinander. Im jetzigen Muster gibt es keine Möglichkeit zu sagen: „Wenn vorn ein - stand, muss hinten auch ein - stehen.“ Die Chefin notiert eine Zeile in ihr Notizbuch. „Es gibt einen Weg, etwas Gefangenes weiter hinten noch einmal aufzurufen. Der kommt in Lektion 10.“ Für heute bleibt es dabei, dass ein Mensch diesen einen Eintrag mit eigenen Augen prüft.
re.findall(r"ship=(done|wait)", orders)? Was ändert sich, wenn du (?:done|wait) daraus machst?groups() bei re.search(r"(\d{4})-(\d\d)(-(\d\d))?", "2026-10 Versand")?03:11:02 Stunde, Minute und Sekunde als Gruppen herausholt, und sieh dir das Ergebnis von m.group(1, 3) an.Das Notizbuch der Direktorin
( ) fasst zu einem Baustein zusammen und merkt sich den gefundenen Text. Quantoren anhängen wie bei (ab)+.
| hat den niedrigsten Vorrang. ^a|b$ heißt '^a oder b$'. Alternativen werden von links probiert.
Gruppennummern folgen den öffnenden Klammern. group(0) ist alles, groups() ein Tupel, m[1] ist group(1).
(?:...) fasst nur zusammen und bekommt keine Nummer.
findall: 0 Gruppen der ganze Treffer, 1 Gruppe diese Gruppe, ab 2 Gruppen Tupel. Für den ganzen Treffer (?:...) oder finditer.
Gruppen ohne Treffer sind None. Bei wiederholten Gruppen wie (\d)+ bleibt nur der letzte Durchgang.
← Zurück
Lektion 4 · Die falschen Anmeldungen: Anker und Validierung
Weiter →
Lektion 6 · Der Zugriff um 3 Uhr nachts: benannte Gruppen und Match-Objekte
Ü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.