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 5:00–5:50
CASE-007
Grep & Co. Datendetektei
Fallakte
Auftraggeber
Reporter Oh, der über das Datenleck recherchiert
Beweismittel
Eine Kundenliste von einer anonymen Quelle. Sieht nach der Mitgliederliste von Markt Jeong aus. Jede Zeile benutzt andere Trennzeichen.
Zum Abschluss des Falls
Damit die Liste morgen früh in den Artikel kann: die mittleren Ziffern der Telefonnummern schwärzen, die Daten ins Format 2026-10-20 bringen und jede Zeile in Spalten aufteilen.
21 Uhr. Die Tür der Detektei fliegt auf. Reporter Oh klappt keuchend seinen Laptop auf. „Ich hab einen Tipp bekommen. Die Kundenliste von Markt Jeong. Mein Artikel erscheint morgen früh, aber persönliche Daten kann ich so nicht drucken.“
Die Direktorin beugt sich über den Bildschirm. Es ist genau die Mitgliederliste aus der Nacht um 3 Uhr, die wir in Lektion 6 gefunden haben. „Mit dem Suchen sind wir durch. Heute wird ersetzt und zerschnitten.“
id, name; phone | joined
1001,kim,010-0000-5678,20/10/2026
1002; lee; 010-0000-1234; 19/10/2026
1003 | park | 010-0000-9012 | 03/09/2026
1004, choi;010-0000-3456 |28/02/2026
# (a+)+$In der letzten Zeile stehen seltsame Zeichen, aber Reporter Oh sitzt die Deadline im Nacken, und er übersieht sie.
re.sub(muster, ersatz, text) gibt einen neuen String zurück, in dem alle Treffer des Musters ersetzt sind. Der ursprüngliche String bleibt unverändert, denn Python-Strings sind unveränderlich.
import re
re.sub(r"\d{4}", "****", "010-0000-5678") # '010-****-****'Es gibt zwei vierstellige Ziffernblöcke, also wurden auch die letzten Ziffern geschwärzt. Wenn die ersten drei und die letzten vier Ziffern stehen bleiben und nur die Mitte verschwinden soll, fängst du die Teile, die bleiben sollen, in Gruppen ein und rufst sie im Ersatz-String wieder auf. \1 steht für den Text, den Gruppe 1 gefangen hat.
re.sub(r"(\d{3})-\d{4}-(\d{4})", r"\1-****-\2", "010-0000-5678") # '010-****-5678'Auch der Ersatz-String enthält Backslashes, also schreibst du ihn wie das Muster als r"...".
Beweislabor
re.search(r"", text)
Rückgabewert
Die Daten in der Liste stehen als Tag/Monat/Jahr da, etwa 20/10/2026. Im Artikel sollen sie als 2026-10-20 erscheinen. Dafür setzt du die Gruppen einfach in anderer Reihenfolge wieder ein.
re.sub(r"(\d\d)/(\d\d)/(\d{4})", r"\3-\2-\1", "20/10/2026") # '2026-10-20'
re.sub(r"(\d\d)/(\d\d)/(\d{4})", r"\g<3>-\g<2>-\g<1>", "20/10/2026") # '2026-10-20'\3 und \g<3> bedeuten dasselbe. Benannte Gruppen aus Lektion 6 rufst du mit \g<name> auf. Dann musst du dir keine Reihenfolge merken, und es passieren weniger Fehler.
re.sub(r"(?P<d>\d\d)/(?P<m>\d\d)/(?P<y>\d{4})", r"\g<y>-\g<m>-\g<d>", "20/10/2026") # '2026-10-20'\g<0> ist der gesamte Treffer. Das brauchst du, wenn du das Gefundene nicht löschen, sondern nur einrahmen willst.
re.sub(r"\d{3}-\d{4}-\d{4}", r"[\g<0>]", "1001,kim,010-0000-5678") # '1001,kim,[010-0000-5678]'Vorsicht, wenn direkt nach einer Gruppennummer eine Ziffer folgen soll. \10 heißt nicht „Gruppe 1, dann eine 0“, sondern Gruppe 10. In solchen Fällen schließt du die Nummer mit spitzen Klammern ab, also \g<1>0.
re.sub(r"(\d+)", r"\g<1>0", "kim 5") # 'kim 50'
re.sub(r"(\d+)", r"\10", "kim 5") # re.PatternError: invalid group reference 10 at position 1Auch im Ersatz-String wird \n zu einem Zeilenumbruch. Ein Escape mit einem Buchstaben ohne Bedeutung wie \q ist dagegen ein Fehler.
re.sub(r",", r"\n", "a,b") # 'a\nb'
re.sub(r",", r"\q", "a,b") # re.PatternError: bad escape \q at position 0re.PatternError ist der Name ab Python 3.13. Es ist dieselbe Ausnahme wie das ältere re.error. Mehr dazu in Lektion 12.
Beweislabor
re.search(r"", text)
Rückgabewert
Statt eines Ersatz-Strings kannst du auch eine Funktion übergeben. re.sub reicht bei jedem Treffer das Match-Objekt an die Funktion weiter und ersetzt den Treffer durch den String, den sie zurückgibt. Lass uns vom Namen nur den ersten Buchstaben stehen lassen und den Rest durch genauso viele Sternchen ersetzen. Mit einer festen Vorlage geht das nicht.
def hide(m):
name = m.group(1)
return "," + name[0] + "*" * (len(name) - 1) + ","
re.sub(r",([a-z]+),", hide, "1003,park,010-0000-9012,03/09/2026") # '1003,p***,010-0000-9012,03/09/2026'
re.sub(r"[a-z]+", lambda m: m[0].upper(), "kim and lee") # 'KIM AND LEE'TypeError.count legt fest, wie viele Treffer von vorn ersetzt werden. Der Standardwert 0 heißt „alle“. re.subn gibt ein Tupel aus dem neuen String und der Anzahl der Ersetzungen zurück. Nehmen wir an, die Liste von oben steht im String leak.
re.sub(r"\d{3}-\d{4}-\d{4}", "***", leak, count=1).splitlines()[1:3]
# ['1001,kim,***,20/10/2026', '1002; lee; 010-0000-1234; 19/10/2026']
re.subn(r"\d{3}-\d{4}-\d{4}", "***", leak)[1] # 4So kann im Artikel die genaue Zahl stehen: „4 Telefonnummern geschwärzt.“
Jede Zeile der Liste hat andere Trennzeichen. Kommas, Semikolons und senkrechte Striche sind gemischt, und die Leerzeichen davor und danach sind auch mal so, mal so. str.split nimmt nur ein einziges Trennzeichen. re.split schneidet an einem Muster.
re.split(r"\s*[,;|]\s*", "a, b;c |d") # ['a', 'b', 'c', 'd']
re.split(r"\s*[,;|]\s*", "a, b;c |d", maxsplit=2) # ['a', 'b', 'c |d']\s* vor und nach dem Trennzeichen schluckt die Leerzeichen gleich mit, deshalb kommen die Spalten sauber heraus.maxsplit=2 schneidet höchstens zweimal. Der Rest bleibt unverändert in der letzten Spalte.Enthält das Muster eine einfangende Gruppe, landen auch die herausgeschnittenen Trennzeichen in der Ergebnisliste. Das ist praktisch, wenn du festhalten musst, welches Trennzeichen es war. Brauchst du sie nicht, schreib (?:...).
re.split(r"\s*([,;|])\s*", "a, b;c |d") # ['a', ',', 'b', ';', 'c', '|', 'd']Du kannst sogar an leeren Treffern schneiden, die kein Zeichen belegen. \b trifft nur an Wortgrenzen, also wird zwischen jedem Wort und jedem Leerzeichen geschnitten. Auch an beiden Enden liegt eine Grenze, deshalb entstehen dort leere Strings.
re.split(r"\b", "a b") # ['', 'a', ' ', 'b', '']Beweislabor
re.search(r"", text)
Rückgabewert
Die vierte Position von re.sub ist count. Wer dort gedankenlos re.I hinschreibt, um ein Flag zu setzen, übergibt keine Flags, sondern eine Anzahl. Denn der eigentliche Wert von re.I ist 2.
re.sub("a", "b", "aAaA", re.I) # 'bAbA' ← Groß-/Kleinschreibung wird nicht ignoriert, nur zwei Ersetzungen
re.sub("a", "b", "aAaA", flags=re.I) # 'bbbb'Dieser Fehler ist so verbreitet, dass Python ab 3.13 eine DeprecationWarning ausgibt, wenn du count, maxsplit oder flags als Positionsargumente übergibst. Die erste Zeile oben warnt mit 'count' is passed as positional argument. Für maxsplit und flags bei re.split gilt dasselbe. Schreib sie immer mit Namen: count=1, maxsplit=2, flags=re.I.
Dots Tipp
Wenn man „Sitz!“ zu mir sagt, setze ich mich. Wenn man aber einfach nur „Zwei!“ ruft, weiß ich nicht, was ich tun soll. Bei Funktionen ist das genauso. re.sub("a", "b", s, re.I) versteht „ersetz nur zweimal“. Ruf es beim Namen: flags=re.I.
Schwärzen, Daten umbauen, zerschneiden: alles der Reihe nach. Die Kopfzeile und die letzte Zeile lassen wir weg und zerschneiden nur die Kundenzeilen.
masked = re.sub(r"(\d{3})-\d{4}-(\d{4})", r"\1-****-\2", leak)
fixed = re.sub(r"(?P<d>\d\d)/(?P<m>\d\d)/(?P<y>\d{4})", r"\g<y>-\g<m>-\g<d>", masked)
for row in fixed.splitlines()[1:-1]:
print(re.split(r"\s*[,;|]\s*", row))
# ['1001', 'kim', '010-****-5678', '2026-10-20']
# ['1002', 'lee', '010-****-1234', '2026-10-19']
# ['1003', 'park', '010-****-9012', '2026-09-03']
# ['1004', 'choi', '010-****-3456', '2026-02-28']Reporter Oh nimmt die Tabelle und will schon aufstehen, da hebt die Direktorin die Hand. „Moment. Die letzte Zeile.“
print(fixed.splitlines()[-1]) # # (a+)+$Das sind keine Listendaten. Das ist ein regulärer Ausdruck, den jemand absichtlich hinterlassen hat. Um genau diese Zeichen zu finden, musst du, wie in Lektion 1 gelernt, jedes Metazeichen einzeln entschärfen.
print(re.escape("(a+)+$")) # \(a\+\)\+\$
re.search(r"\(a\+\)\+\$", leak) # <re.Match object; span=(177, 183), match='(a+)+$'>Beweislabor
re.search(r"", text)
Rückgabewert
„Was ist das?“, fragt Reporter Oh. Die Direktorin schweigt lange, dann antwortet sie: „Eine Backtracking-Falle. Eine Regex-Engine, die das liest, läuft denselben Weg unzählige Male zurück, bis sie stehen bleibt. Lass das morgen aus dem Artikel raus.“
Als Reporter Oh gegangen ist, holt die Direktorin einen verblassten Bilderrahmen aus der Schublade. Ein Foto aus dem ersten Jahr der Detektei. Sie betrachtet es lange, dann legt sie den Rahmen mit dem Bild nach unten auf den Schreibtisch.
Fall abgeschlossen — In den 4 Einträgen der geleakten Liste die mittleren Ziffern der Telefonnummern geschwärzt, die Daten einheitlich auf Jahr-Monat-Tag gebracht und dem Reporter übergeben. Am Ende der Liste einen absichtlich platzierten regulären Ausdruck (a+)+$ entdeckt. Herkunft unbekannt. Weitere Ermittlungen nötig.
1001 bis 1004 in der Liste so, dass ein # davorsteht, also #1001. Mit \g<0> geht das in einer Zeile.re.split(r"(\s*)([,;|])(\s*)", "a, b")? Rate zuerst. Bei drei Gruppen: Wie viele Einträge werden dazwischengeschoben?"price 1500 and 300". Denk daran, dass die Funktion einen String zurückgeben muss.Das Notizbuch der Direktorin
re.sub(muster, ersatz, text) gibt einen neuen String zurück. Das Original bleibt, wie es ist.
Im Ersatz-String: \1 = \g<1>, benannt \g<name>, alles \g<0>. Folgt eine Ziffer, dann \g<1>0 (\10 ist Gruppe 10).
Im Ersatz-String ist \n ein Zeilenumbruch, unbekannte Escapes wie \q sind ein Fehler.
Ersetzen per Funktion: Sie bekommt ein Match und gibt einen String zurück. count begrenzt die Anzahl, re.subn liefert (Ergebnis, Anzahl).
re.split schneidet an einem Muster. maxsplit begrenzt, mit einfangender Gruppe landen auch die Trennzeichen im Ergebnis.
count, maxsplit und flags als Schlüsselwortargumente. Positionsargumente geben ab 3.13 eine DeprecationWarning, sonst landet re.I als count=2.
← Zurück
Lektion 6 · Der Zugriff um 3 Uhr nachts: benannte Gruppen und Match-Objekte
Weiter →
Lektion 8 · Die Herausforderung des Backtrackers: gierig, genügsam, possessiv
Ü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.