R

ReflexKR

Reflex-Dokumentation

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

Reguläre Ausdrücke zum Ansehen
Lektion 11

Zeit 8:20–9:10

Der unlesbare Regex: Flags und Kompilieren

CASE-011

Grep & Co. Datendetektei

Fallakte

Der unlesbare Regex

Auftraggeber

Direktorin Han Byeol

Beweismittel

Die letzten Zeilen der drei Drohbriefe und das Zutrittsprotokoll der Tür im 4. Stock der Detektei.

Zum Abschluss des Falls

Die drei Zeilen zu einem einzeiligen Regex zusammenfügen, ihn lesbar aufschreiben und herausfinden, wonach er sucht.

23 Uhr. In der Detektei brennt nur noch eine Lampe. Die Direktorin sitzt an ihrer alten mechanischen Tastatur und tippt die letzten Zeilen der drei Briefe ab. „Einen Regex in einer Zeile kann nur lesen, wer ihn geschrieben hat. Und nach drei Monaten nicht einmal der.“

Hier die Datei mit den drei Briefenden der Reihe nach und das Zutrittsprotokoll der Tür im 4. Stock, das die Direktorin wortlos herausgesucht hat. In den Regex-Bruchstücken tauchen nämlich door, 4f und badge auf.

<<(?mi)^(\d{4})([-./])(\d\d)\2(\d\d)[ T]
(0[0-4]):(\d\d)\s+(?:door|gate)=4f\s+
badge=(f-0[12])\b(?!.*denied)>>
2026-10-19 22:41 door=4F badge=F-01 result=ok
2026-10-20 02:13 door=4F badge=S-07 result=ok
2026-10-20 02:58 door=4F badge=F-02 result=denied
2026-10-20 03:02 door=4F badge=F-02 result=ok
2026.10.20 03:05 gate=4F badge=f-02 result=ok
2026-10-20 09:00 door=4F badge=F-01 result=ok

Der erste Hinweis ist das (?mi) ganz vorn. Das ist ein Flag, ein Schalter, der von außerhalb des Musters festlegt, wie das Muster gelesen wird. Heute legst du ein Flag nach dem anderen um und entwirrst damit diese eine Zeile.

11-1. Groß-/Kleinschreibung und Zeilen: re.I, re.M (8 Min.)

Im Zutrittsprotokoll schreibt nur das Gerät an der Hintertür (gate) die Ausweisnummer klein: f-02. Mit re.I (re.IGNORECASE) wird Groß- und Kleinschreibung ignoriert.

import re

re.findall(r"badge=F-02", door_log)          # ['badge=F-02', 'badge=F-02']
re.findall(r"badge=F-02", door_log, re.I)    # ['badge=F-02', 'badge=F-02', 'badge=f-02']

re.M (re.MULTILINE) hast du in Lektion 4 nur kurz probiert. Normalerweise passt ^ nur ganz am Anfang des Strings und $ nur ganz am Ende. Mit re.M passen sie am Anfang und Ende jeder Zeile. door_log sind hier die sechs Zeilen des Zutrittsprotokolls von oben.

re.findall(r"^\S+ 0[0-4]:\d\d", door_log)          # []
re.findall(r"^\S+ 0[0-4]:\d\d", door_log, re.M)
# ['2026-10-20 02:13', '2026-10-20 02:58', '2026-10-20 03:02', '2026.10.20 03:05']

re.findall(r"\w+$", door_log)          # ['ok']
re.findall(r"\w+$", door_log, re.M)    # ['ok', 'ok', 'denied', 'ok', 'ok', 'ok']

Ohne re.M kommt kein einziger Eintrag aus der Nacht heraus, weil die erste Zeile um 22 Uhr ist. Bei zeilenweisen Logs ist re.M praktisch Standard.

Beweislabor

re.search(r"", text)

Flags

Rückgabewert

Ändere nicht das Muster, sondern drück nur die Flag-Knöpfe. Mit re.M passen ^ und $ in jeder Zeile, mit re.I kommt auch die Zeile mit dem kleinen f-02 dazu.

11-2. re.S öffnet das Fenster (6 Min.)

Jetzt müssen die drei Briefenden zu einer Zeile werden. Alles von << bis >> zu fangen scheint zu reichen, aber es kommt nichts heraus.

m = re.search(r"<<(.*)>>", tails)
print(m)    # None

Dot sagt es seit Lektion 1: . kommt nicht über einen Zeilenumbruch. Das .* nach << bleibt am Ende der ersten Zeile stehen, und dort gibt es kein >>.

Die Direktorin dreht ihren Stuhl und öffnet das Fenster. „Dot, raus mit dir.“ Mit re.S (re.DOTALL) wird . zu wirklich jedem Zeichen, Zeilenumbrüche eingeschlossen.

m = re.search(r"<<(.*)>>", tails, re.S)
one_line = m.group(1).replace("\n", "")
print(one_line)
# (?mi)^(\d{4})([-./])(\d\d)\2(\d\d)[ T](0[0-4]):(\d\d)\s+(?:door|gate)=4f\s+badge=(f-0[12])\b(?!.*denied)

Dot springt über das Fensterbrett nach draußen, kommt durchs Nachbarfenster wieder herein und setzt sich auf den Schreibtisch. Aus drei Zeilen ist eine geworden.

Dots Tipp

Das Fenster ist offen! Mit re.S komme ich auch über Zeilenumbrüche, bis in die nächste Zeile und die übernächste. Aber Vorsicht: .* ist von Natur aus gierig und rennt bei offenem Fenster in einem Satz bis ans Ende des Textes. Wenn du über mehrere Zeilen nur kurz fangen willst, nimm .*?.

Beweislabor

re.search(r"", text)

Flags

Rückgabewert

Drück den Knopf re.S und öffne das Fenster. Sobald . über Zeilenumbrüche kommt, werden alle drei Zeilen auf einmal gefangen.

re.M und re.S klingen ähnlich und werden oft verwechselt. Sie tun aber etwas völlig anderes.

  • re.M ändert, wo ^ und $ passen (in jeder Zeile).
  • re.S ändert, welche Zeichen . fressen darf (auch den Zeilenumbruch).
re.findall(r"^.+$", "a\nb", re.M)    # ['a', 'b']
re.findall(r"^.+$", "a\nb", re.S)    # ['a\nb']

Dass der Regex des Backtrackers nur (?mi) einschaltet und s nicht, hat seinen Grund. Das (?!.*denied) am Ende heißt: „Weiter hinten in dieser Zeile darf kein denied stehen.“ Mit s würde .* in die nächsten Zeilen weiterlaufen und den Eintrag schon verwerfen, wenn irgendeine Zeile weiter unten denied enthält.

11-3. Lesbar aufschreiben: re.X und (?#...) (9 Min.)

Jetzt ist es eine Zeile, aber lesbar ist sie immer noch nicht. Mit re.X (re.VERBOSE) werden Leerzeichen und Zeilenumbrüche im Muster ignoriert, und alles von # bis zum Zeilenende wird zum Kommentar. So kannst du ein Muster auf mehrere Zeilen verteilen und erklären.

Die Direktorin tippt es Stück für Stück neu. Die nummerierten Gruppen bekommen Namen, und \2 wird zu (?P=sep) aus Lektion 10.

DOOR = re.compile(r'''
    ^
    (?P<year>\d{4}) (?P<sep>[-./]) (?P<month>\d\d) (?P=sep) (?P<day>\d\d)   # Datum, nur eine Art Trennzeichen
    [ T]                                   # zwischen Datum und Uhrzeit: Leerzeichen oder T
    (?P<hour>0[0-4]) : (?P<minute>\d\d)    # nachts, von 0 bis 4:59 Uhr
    \s+ (?:door|gate) = 4f                 # Vorder- oder Hintertür im 4. Stock
    \s+ badge = (?P<badge>f-0[12]) \b      # Ausweis F-01 oder F-02
    (?! .* denied )                        # abgelehnte Einträge raus
''', re.I | re.M | re.X)

for m in DOOR.finditer(door_log):
    print(m["hour"], m["minute"], m["badge"])
# 03 02 F-02
# 03 05 f-02

Derselbe Regex, aber jetzt lässt er sich Zeile für Zeile lesen. Er sucht Einträge, bei denen nachts zwischen 0 und 4:59 Uhr ein Ausweis F-01 oder F-02 durch eine Tür im 4. Stock tatsächlich hereingekommen ist.

Auch re.X hat Ausnahmen. Willst du ein echtes Leerzeichen oder # finden, escapest du es oder steckst es in eine Klasse. Leerzeichen in einer Klasse werden nicht entfernt. Das [ T] im Muster oben ist so ein Fall.

re.fullmatch(r"\d{4} - \d\d", "2026-10", re.X)   # <re.Match object; span=(0, 7), match='2026-10'>
re.fullmatch(r"a b", "a b", re.X)                # None
re.fullmatch(r"a\ b", "a b", re.X)               # <re.Match object; span=(0, 3), match='a b'>
re.fullmatch(r"a[ ]b", "a b", re.X)              # <re.Match object; span=(0, 3), match='a b'>
re.fullmatch(r"a\#b", "a#b", re.X)               # <re.Match object; span=(0, 3), match='a#b'>

Willst du ohne re.X nur einen kurzen Kommentar mitten ins Muster setzen, nimm (?#...). Der Inhalt der Klammer wird komplett ignoriert. Der Kommentar unten ist koreanisch und heißt „Ausweise der Gründungsmitglieder“.

re.findall(r"F-0[12](?#창립 멤버 출입증)", door_log, re.I)
# ['F-01', 'F-02', 'F-02', 'f-02', 'F-01']

Beweislabor

re.search(r"", text)

Flags

Rückgabewert

Das Muster ist mit Leerzeichen aufgelockert. Ohne re.X sind die Leerzeichen echte Zeichen, und nichts wird gefunden. Schalte re.X ein. Das Leerzeichen in [ ] bleibt auch unter re.X erhalten.

Die Direktorin betrachtet das aufgeschriebene Muster lange. „Kein einziger Variablenname, alles in einer Zeile. Diese Angewohnheit mit den Kommentaren … nein, die Angewohnheit, keine Kommentare zu schreiben. Der hat das auch immer …“ Sie bricht ab. Den Bilderrahmen, der mit dem Bild nach unten auf dem Schreibtisch liegt, schiebt sie in die Schublade und legt die Hände wieder auf die Tastatur. „Weiter.“

11-4. Welche Zeichen zählen: re.A, re.U, re.L (5 Min.)

In Lektion 2 hieß es, dass \w in str-Mustern auch Hangul umfasst und \d auch die arabisch-indische Ziffer ٣. Denn Unicode ist der Standard. Mit re.A (re.ASCII) beachten \w \W \d \D \s \S \b \B nur ASCII-Zeichen.

re.findall(r"\w+", "café 한글 F_02")          # ['café', '한글', 'F_02']
re.findall(r"\w+", "café 한글 F_02", re.A)    # ['caf', 'F_02']
re.findall(r"\d", "٣3")                       # ['٣', '3']
re.findall(r"\d", "٣3", re.A)                 # ['3']

(한글 ist das koreanische Wort für die koreanische Schrift, Hangul.)

  • re.U (re.UNICODE) ist bei str-Mustern ohnehin Standard, du musst es nicht extra angeben. Es gibt es noch aus Kompatibilitätsgründen mit altem Code.
  • re.L (re.LOCALE) funktioniert nur mit bytes-Mustern, und weil das Ergebnis von den Regionseinstellungen des Rechners abhängt, wird davon abgeraten. Bei einem str-Muster ist es ein Fehler.
re.compile(r"\w", re.L)
# ValueError: cannot use LOCALE flag with a str pattern

Beweislabor

re.search(r"", text)

Flags

Rückgabewert

Schalte re.A ein und aus und sieh dir an, was \w+ dabei verpasst. Ändere das Muster zu \d, um auch die arabisch-indische Ziffer ٣ zu prüfen.

11-5. Flags kombinieren, ein- und ausschalten (6 Min.)

Flags kombinierst du mit |. Ihr Wert ist ein Enum namens re.RegexFlag, mit dem du wie mit einer Ganzzahl rechnen kannst. Für „gar kein Flag“ gibt es re.NOFLAG (seit 3.11, Wert 0), das sich gut als Standardwert in Funktionen macht.

flags = re.I | re.M
print(flags)          # re.IGNORECASE|re.MULTILINE
print(type(flags))    # <flag 'RegexFlag'>
print(int(flags))     # 10

def find_all(pattern, text, flags=re.NOFLAG):
    return re.findall(pattern, text, flags)

Du kannst die nötigen Buchstaben aus (?aiLmsux) auch wie der Backtracker ganz vorn ins Muster schreiben. (?im) ist dasselbe wie re.I | re.M. Steht es nicht ganz vorn, gibt es einen Fehler.

re.findall(r"(?im)^\S+ 0\d:\d\d(?=.*f-02)", door_log)
# ['2026-10-20 02:58', '2026-10-20 03:02', '2026.10.20 03:05']

re.compile(r"badge=(?i)f-02")
# re.PatternError: global flags not at the start of the expression at position 6

Soll ein Flag nur für einen Teil des Musters gelten, nimmst du einen Klammerbereich wie (?i:...). (?-i:...) schaltet es nur in diesem Bereich aus, (?i-s:...) schaltet i ein und s aus.

re.findall(r"badge=(?i:f)-02", door_log)        # ['badge=F-02', 'badge=F-02', 'badge=f-02']
re.findall(r"(?i)BADGE=(?-i:F)-02", door_log)   # ['badge=F-02', 'badge=F-02']
re.fullmatch(r"(?s)a.(?i-s:b.)", "a\nB\n")      # None
re.fullmatch(r"(?s)a.(?i-s:b.)", "a\nBc")       # <re.Match object; span=(0, 4), match='a\nBc'>

a, L und u legen fest, welche Zeichen zählen. Deshalb kannst du sie in einem Bereich ein-, aber nicht ausschalten.

re.findall(r"(?a:\w+)", "café")    # ['caf']
re.compile(r"(?-a:\w+)")
# re.PatternError: bad inline flags: cannot turn off flags 'a', 'u' and 'L' at position 4

Zum Schluss re.DEBUG: Es druckt aus, wie re das Muster verstanden hat. Vergleichen wir die Signatur des Backtrackers mit demselben Muster ohne die äußere Wiederholung. Unten steht jeweils der Anfang der Ausgabe, danach folgt noch eine Liste von Ausführungsbefehlen.

re.compile(r"(a+)+$", re.DEBUG)
# MAX_REPEAT 1 MAXREPEAT
#   SUBPATTERN 1 0 0
#     MAX_REPEAT 1 MAXREPEAT
#       LITERAL 97
# AT AT_END

re.compile(r"a+$", re.DEBUG)
# MAX_REPEAT 1 MAXREPEAT
#   LITERAL 97
# AT AT_END

In einer Wiederholung (MAX_REPEAT) steckt noch eine Wiederholung. Genau diese Form hatte das langsame Muster aus der Herausforderung in Lektion 8.

11-6. re.compile und das Pattern-Objekt (6 Min.)

So wie die Direktorin das aufgeschriebene Muster unter dem Namen DOOR abgelegt hat, macht re.compile aus einem Muster ein Pattern-Objekt. Es hat dieselben Methoden wie die Modulfunktionen, search match fullmatch findall finditer sub split, und nimmt zusätzlich den Suchbereich pos und endpos an.

p = re.compile(r"\d\d:\d\d")
line = "2026-10-20 03:02 door=4F"
print(p.search(line))              # <re.Match object; span=(11, 16), match='03:02'>
print(p.match(line))               # None
print(p.match(line, 11))           # <re.Match object; span=(11, 16), match='03:02'>
print(p.fullmatch(line, 11, 16))   # <re.Match object; span=(11, 16), match='03:02'>
print(p.findall(line, 0, 15))      # []
print(len(list(p.finditer(line, 11))))   # 1

pos ist nicht dasselbe wie den String abzuschneiden. ^ passt nicht an der Stelle pos, sondern nur am echten Anfang des Strings (mit re.M am Zeilenanfang).

q = re.compile(r"^\d\d")
print(q.match(line, 11))     # None
print(q.match(line[11:]))    # <re.Match object; span=(0, 2), match='03'>

Ein Pattern-Objekt kennt auch Informationen über sich selbst. Bei str-Mustern kommt zu flags automatisch re.UNICODE (32) dazu.

print(DOOR.groups)       # 7
print(DOOR.groupindex)
# {'year': 1, 'sep': 2, 'month': 3, 'day': 4, 'hour': 5, 'minute': 6, 'badge': 7}
print(DOOR.flags)        # 106
print(re.RegexFlag(DOOR.flags))   # re.IGNORECASE|re.UNICODE|re.MULTILINE|re.VERBOSE
print(re.compile("x").flags)      # 32
print(re.compile(one_line).pattern == one_line)   # True
  • Auch die Modulfunktion re.search(muster, ...) kompiliert intern. Kürzlich benutzte Muster bleiben in einem internen Cache und werden nicht neu kompiliert. re.purge() leert diesen Cache (das brauchst du selten).
  • Der eigentliche Vorteil von re.compile ist also weniger die Geschwindigkeit als der Name. Ein Muster namens DOOR lässt sich im Code lesen.
  • Typhinweise schreibst du als re.Pattern[str] und re.Match[str] (bei bytes-Mustern [bytes]).
def night_entries(p: re.Pattern[str], log: str) -> list[re.Match[str]]:
    return list(p.finditer(log))

print(len(night_entries(DOOR, door_log)))   # 2

Den Fall lösen (10 Min.)

Mit dem aufgeschriebenen Muster liest du das Zutrittsprotokoll noch einmal.

for m in DOOR.finditer(door_log):
    print(f"{m['year']}-{m['month']}-{m['day']} {m['hour']}:{m['minute']} {m['badge'].upper()}")
# 2026-10-20 03:02 F-02
# 2026-10-20 03:05 F-02

Auch die ursprüngliche eine Zeile aus den Briefen findet genau dieselben zwei Einträge. Die Bedeutung ist dieselbe, nur die Lesbarkeit ist eine andere.

[m.group(7) for m in re.finditer(one_line, door_log)]   # ['F-02', 'f-02']

In der Nacht zum 20. Oktober wurde der Ausweis F-02 um 02:58 an der Vordertür einmal abgelehnt, vier Minuten später erneut vorgehalten und hereingelassen, und drei Minuten danach ging er durch die Hintertür. Der Backtracker hat diesen Regex geschickt, um zu sagen: „Lies doch, wann ich hier war, wenn du kannst.“

Die Direktorin schlägt das Ausweisregister auf. „F-01 ist meiner. F-02 …“ In der Zeile steht statt eines Namens nur ein doppelter Strich. „Den hätte ich vor zehn Jahren sperren sollen. Ich hab’s nicht übers Herz gebracht.“

Fall abgeschlossen — Die Enden der drei Briefe ergaben zusammen einen einzigen Regex. Lesbar aufgeschrieben bestätigte er, dass der Ausweis F-02 am 2026-10-20 zwischen 03:02 und 03:05 den 4. Stock der Detektei betreten hat. F-02 wurde bei der Gründung ausgestellt und ist ab heute gesperrt.

Zum Schluss holt die Direktorin die ursprüngliche eine Zeile auf den Bildschirm. „Es ist derselbe Regex. Den einen kann nur lesen, wer ihn geschrieben hat, den anderen jeder. Wir schreiben so, dass jeder ihn lesen kann.“

Übungen

  1. Schreib die Benutzernamen-Regel [a-z][a-z0-9_]{3,15} aus Lektion 4 mit re.X auseinander und kommentiere jedes Stück.
  2. Finde mit dem pos von re.compile(r"\d+") in "no.17 at 03:02" nur die Zahlen, die nach at kommen.
  3. Sieh dir den Fehler von re.compile(r"badge=(?i)f-02") an und repariere ihn dann mit dem Bereichsflag (?i:...), sodass die Bedeutung dieselbe ist.

Die Spur des Backtrackers

(a+)+$

Ihr habt ihn aufgeschrieben. Den nächsten werdet ihr nicht aufschreiben können. Morgen früh um neun. Öffnet euer Postfach.

Das Notizbuch der Direktorin

  • re.I ignoriert Groß-/Kleinschreibung, re.M lässt ^ $ in jeder Zeile passen, re.S lässt . auch Zeilenumbrüche fressen. M und S tun Verschiedenes.

  • re.X: Leerzeichen und #-Kommentare werden ignoriert. Leerzeichen in Klassen und \ (Backslash + Leerzeichen) bleiben. (?#...) ist überall ein Kommentar.

  • re.A: \w \d \s \b nur ASCII. Bei str ist re.U Standard, re.L nur für bytes und nicht empfohlen.

  • Kombinieren mit re.I | re.M (re.RegexFlag), kein Flag ist re.NOFLAG. Inline (?im) nur ganz vorn.

  • Bereichsflags (?i:...) (?-i:...) (?i-s:...). a L u lassen sich nicht ausschalten. Mit re.DEBUG schaust du ins Innere eines Musters.

  • re.compile ist ein Werkzeug, um Mustern Namen zu geben. Nimmt pos endpos, aber ^ passt nur am echten Anfang. Attribute: flags groups groupindex pattern.

← Zurück

Lektion 10 · Gleiche Masche: Rückverweise und Bedingungen

Weiter →

Lektion 12 · Die Enttarnung des Backtrackers: Rückblick und ein Tokenizer

Über neue Lektionen informiert werden

Wir schicken dir eine E-Mail, wenn dieser Kurs neue Lektionen bekommt oder ein Folgekurs erscheint.

Erhoben: deine E-Mail-Adresse · Zweck: Hinweise auf neue Lektionen oder den Start dieses Kurses · Speicherdauer: bis du die Löschung verlangst. Keine andere Nutzung, keine Weitergabe. Antworte einfach auf eine Benachrichtigung, dann löschen wir sie sofort.

Fragen & Antworten

Frag alles zu dieser Lektion. Fragen aus allen Sprachversionen dieser Seite erscheinen hier gemeinsam.

Wird als reiner Text angezeigt, ohne Links oder Formatierung.

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
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex