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 9:10–10:00
CASE-012
Grep & Co. Datendetektei
Fallakte
Auftraggeber
Die ganze Detektei
Beweismittel
Eine verschlüsselte Nachricht, die um 9 Uhr morgens im Postfach landete. Sie besteht nur aus Backslashes, Ziffern und lateinischen Buchstaben.
Zum Abschluss des Falls
Die Nachricht in Tokens zerlegen, entschlüsseln und herausfinden, wer der Backtracker ist.
9 Uhr morgens. Die Detektei ist voll. Kommissar Kang steht an der Tür, Frau Jeong hat ihr Notizbuch aufgeschlagen, Reporter Oh balanciert seinen Laptop auf den Knien. Die Direktorin dreht den Monitor so, dass alle ihn sehen. „Heute lesen wir gemeinsam. Zeichen für Zeichen.“
Das ist die verschlüsselte Nachricht aus dem Postfach:
\x46\N{HYPHEN-MINUS}\060\x32 \x3d \uc11c\uc9c4
\x34\x46 \060\063\x3a\060\x32
(a+)+$Reporter Oh schüttelt den Kopf. „Das sind doch keine Buchstaben, das sind nur Symbole.“ Die Direktorin antwortet: „Auch Symbole folgen Regeln. Schreib die Regeln eine nach der anderen auf, dann liest die Maschine für dich.“
Ein Programm, das langen Text in sinnvolle Stücke (Tokens) zerlegt, heißt Tokenizer. Heute baust du aus allem, was du bisher gelernt hast, einen Tokenizer und knackst damit diese Nachricht.
Zum Üben zerlegen wir erst einmal einen Rechenausdruck wie 3.5*(2 + x). Der Aufbau entspricht dem Beispiel „Writing a Tokenizer“ aus der offiziellen Python-Dokumentation.
import re
TOKEN_SPEC = [
("NUMBER", r"\d+(?:\.\d*)?"), # Ganzzahl oder Dezimalzahl
("ID", r"[A-Za-z_]\w*"), # Name
("OP", r"[+\-*/]"), # Operator
("LPAR", r"\("),
("RPAR", r"\)"),
("SKIP", r"[ \t]+"), # Leerraum wird verworfen
("MISMATCH", r"."), # alles andere: unbekanntes Zeichen
]
TOKEN = re.compile("|".join(f"(?P<{name}>{rx})" for name, rx in TOKEN_SPEC))
def tokenize(code):
for m in TOKEN.finditer(code):
kind, value = m.lastgroup, m.group()
if kind == "SKIP":
continue
if kind == "MISMATCH":
raise ValueError(f"모르는 글자 {value!r} (위치 {m.start()})")
yield kind, value
print(list(tokenize("3.5*(2 + x)")))
# [('NUMBER', '3.5'), ('OP', '*'), ('LPAR', '('), ('NUMBER', '2'), ('OP', '+'), ('ID', 'x'), ('RPAR', ')')]Die Fehlermeldung im Code ist koreanisch: 모르는 글자 heißt „unbekanntes Zeichen“, 위치 heißt „Position“. Der ganze Aufbau besteht aus drei Dingen:
(?P<name>regel), verbunden mit |. Weil die Regeln als Liste dastehen, sind sie leicht zu lesen und zu ändern.finditer fängt von vorn ein Token nach dem anderen, und m.lastgroup (Lektion 6) liefert den Namen der Regel, die getroffen hat.MISMATCH am Ende ist ein einzelner .. Hier bleibt jedes Zeichen hängen, auf das keine andere Regel passt.list(tokenize("3.5 $ 2"))
# ValueError: 모르는 글자 '$' (위치 4)Was passiert ohne MISMATCH? finditer überspringt Zeichen, die nicht passen, stillschweigend. Steckt ein merkwürdiges Zeichen in der Nachricht, merkst du es nicht einmal.
NO_GUARD = re.compile(r"(?P<NUMBER>\d+)|(?P<OP>[+*])")
[m.group() for m in NO_GUARD.finditer("3 $ 2")] # ['3', '2']| probiert von links nach rechts, also ist die Reihenfolge die Priorität. Steht eine breite Regel vorn, kommt eine engere nie zum Zug.
BAD = re.compile(r"(?P<ID>\w+)|(?P<NUMBER>\d+)")
[m.lastgroup for m in BAD.finditer("x 3")] # ['ID', 'ID']Beweislabor
re.search(r"", text)
Rückgabewert
Dinge wie \x46 oder \uc11c in der Nachricht heißen Zeichen-Escapes. Die meisten Escapes aus Python-Strings versteht auch re direkt im Muster. Deshalb findest du einen Tab auch mit dem Raw-String r"\t".
print(re.findall(r"\t|\n", "F-02\t03:02\n")) # ['\t', '\n']
print(re.fullmatch(r"\\", "\\")) # <re.Match object; span=(0, 1), match='\\'>\t (Tab), \n (Zeilenumbruch), \r, \f, \v, \a (Klingel), und der Backslash selbst ist \\.\x41 (zwei Stellen), \u00e9 (vier Stellen), \U0001F600 (acht Stellen).\N{EM DASH}.print(re.findall(r"\x46-\x30\x32", "badge=F-02")) # ['F-02']
print(re.fullmatch(r"caf\u00e9", "café")) # <re.Match object; span=(0, 4), match='café'>
print(re.fullmatch(r"\N{EM DASH}", "—")) # <re.Match object; span=(0, 1), match='—'>
print(len("\U0001F600"), "\U0001F600" == "\N{GRINNING FACE}") # 1 TrueEs gibt auch Oktalzahlen. Beginnt ein Escape mit \0 oder hat es drei Ziffern, wird es zu einem Oktalzeichen (wie in Lektion 10 gesehen, ist \10 eine Gruppennummer). Außerdem ist \b nur innerhalb einer Klasse ein Backspace-Zeichen.
print(re.findall(r"\101|\0", "A\0B")) # ['A', '\x00']
print(re.findall(r"[\b]", "F\b-02")) # ['\x08']Umgekehrt ist ein Escape mit einem Buchstaben ohne festgelegte Bedeutung ein Fehler. Diese Plätze sind freigehalten, damit sie später eine neue Bedeutung bekommen können.
re.compile(r"\q")
# re.PatternError: bad escape \q at position 0Dots Tipp
Jedes Mal, wenn ich \b sehe, wackelt mein Schwanz dreimal. Außerhalb von Regex (im normalen String "\b") ist es ein Backspace, im Muster eine Wortgrenze, und in [ ] im Muster wieder ein Backspace. Deshalb schreibe ich Muster immer als r"..." und nehme [\b] nur, wenn ich wirklich einen Backspace brauche.
Beweislabor
re.search(r"", text)
Rückgabewert
Die Meldung im roten Fehlerfeld des Labors stammt von einer Ausnahme namens re.error. Seit 3.13 gibt es auch den Namen re.PatternError, beides ist dieselbe Ausnahme. Das Ausnahmeobjekt sagt dir, was falsch ist und wo.
print(re.PatternError is re.error) # True
try:
re.compile(r"[a-Z]")
except re.error as e:
print(e.msg) # bad character range a-Z
print(e.pattern) # [a-Z]
print(e.pos) # 1Bei mehrzeiligen Mustern sind lineno (welche Zeile) und colno (welche Spalte) hilfreich. Gezählt wird ab 1.
user_pattern = r'''(?x)
(?P<badge> F-\d+ )
(?P<time \d\d:\d\d )
'''
try:
re.compile(user_pattern)
except re.PatternError as e:
print(e.msg) # missing >, unterminated name
print(e.pos, e.lineno, e.colno) # 36 3 9Von Menschen eingegebene Muster können jederzeit falsch sein. Programme, die Muster entgegennehmen, etwa ein Suchfeld, packen das immer in try/except.
def safe_compile(text):
try:
return re.compile(text)
except re.error as e:
print(f"패턴 오류: {e.msg} (위치 {e.pos})")
return None
safe_compile(r"(?P<SIGN>(a+)+$") # 패턴 오류: missing ), unterminated subpattern (위치 0)(패턴 오류 heißt „Musterfehler“.) try/except fängt allerdings nur Syntaxfehler ab. Ein Muster wie (a+)+$, dessen Syntax stimmt, das aber langsam ist, bleibt ohne jede Ausnahme einfach hängen (Lektion 8). Deshalb lässt das Beweislabor Muster von einem separaten Arbeiter ausführen und tauscht diesen Arbeiter aus, wenn die Zeit abläuft.
Beweislabor
re.search(r"", text)
Rückgabewert
Daten aus Dateien oder dem Netzwerk sind manchmal nicht str, sondern bytes. Für bytes nimmst du bytes-Muster wie rb"...". str und bytes lassen sich nicht mischen.
data = "badge=F-02 서진".encode()
print(re.findall(rb"F-\d+", data)) # [b'F-02']
re.findall(r"F-\d+", data)
# TypeError: cannot use a string pattern on a bytes-like object
re.findall(rb"F-\d+", "badge=F-02")
# TypeError: cannot use a bytes pattern on a string-like object\w, \d und \s in bytes-Mustern beachten nur ASCII. Hangul ist auf mehrere Bytes verteilt und wird darum nicht als Buchstabe erkannt. Um mit Hangul zu arbeiten, machst du zuerst mit decode() einen str daraus.
print(re.findall(rb"\w+", data)) # [b'badge', b'F', b'02']
print(re.findall(r"\w+", data.decode())) # ['badge', 'F', '02', '서진']Auch bei den Flags ist es umgekehrt. re.L geht nur mit bytes-Mustern (Lektion 11), und re.U ist bei bytes-Mustern ein Fehler.
print(re.compile(rb"\w", re.L).flags) # 4
re.compile(rb"\w", re.U)
# ValueError: cannot use UNICODE flag with a bytes patternIn Lektion 2 hieß es, du sollst [[, --, &&, ~~ und || in Klassen meiden. Diese Stellen sind für künftige „Mengenoperationen“ reserviert, deshalb gibt es heute eine FutureWarning. Meinst du die Zeichen wörtlich, escape sie.
re.compile(r"[[a-z]")
# FutureWarning: Possible nested set at position 1
re.compile(r"[a-z--]")
# FutureWarning: Possible set difference at position 4
re.compile(r"[\[a-z]") # keine WarnungIm Modul re gibt es auch interne Werkzeuge, die nicht in der offiziellen Dokumentation stehen: re.Scanner (eine Klasse, die dir einen Tokenizer baut), Pattern.scanner() und Match.regs (die spans aller Gruppen). Es reicht, dass du weißt, dass es sie gibt. Was nicht dokumentiert ist, kann sich jederzeit ändern. In echtem Code nimmst du den Weg über finditer + lastgroup, den du heute gebaut hast.
m = re.search(r"(F)-(\d+)", "badge=F-02")
print(m.regs) # ((6, 10), (6, 7), (8, 10))Jetzt schreibst du die Regeln der Nachricht auf: vier Arten von Zeichen-Escapes, die Signatur, Leerraum und das MISMATCH, das alle Lücken schließt. Wie in Lektion 11 gelernt, mit re.X aufgeschrieben und kommentiert.
import unicodedata
CIPHER = re.compile(r'''
(?P<HEX> \\x [0-9a-fA-F]{2} ) # \x46 zwei Hexadezimalstellen
| (?P<UNI> \\u [0-9a-fA-F]{4} ) # \uc11c vier Hexadezimalstellen
| (?P<NAME> \\N \{ [A-Z -]+ \} ) # \N{...} Zeichenname
| (?P<OCT> \\ [0-7]{3} ) # \060 drei Oktalstellen
| (?P<SIGN> \(a\+\)\+\$ ) # Signatur, wie re.escape("(a+)+$")
| (?P<SPACE> [ \n]+ ) # Leerzeichen und Zeilenumbrüche bleiben
| (?P<MISMATCH> . ) # alle anderen Zeichen sind verdächtig
''', re.X)
def decode(message):
out = []
for m in CIPHER.finditer(message):
kind, value = m.lastgroup, m.group()
if kind in ("HEX", "UNI"):
out.append(chr(int(value[2:], 16)))
elif kind == "OCT":
out.append(chr(int(value[1:], 8)))
elif kind == "NAME":
out.append(unicodedata.lookup(value[3:-1]))
elif kind == "SPACE":
out.append(value)
elif kind == "MISMATCH":
raise ValueError(f"모르는 토큰 {value!r} (위치 {m.start()})")
return "".join(out).strip()
print(decode(message))
# F-02 = 서진
# 4F 03:02Denk daran: Auch unter re.X bleiben die Leerzeichen in den Klassen [A-Z -] und [ \n] erhalten. Die Signatur SIGN gibt nichts aus und wird verworfen. (모르는 토큰 heißt „unbekanntes Token“.)
Beweislabor
re.search(r"", text)
Rückgabewert
Ob die entschlüsselten Zeichen stimmen, kannst du auch re direkt fragen. Die erste Zeile der Nachricht ist selbst ein gültiger regulärer Ausdruck, denn re versteht dieselben Escapes.
first = message.splitlines()[0]
print(re.fullmatch(first, "F-02 = 서진")) # <re.Match object; span=(0, 9), match='F-02 = 서진'>F-02 = 서진. 서진 ist ein koreanischer Name: Seo Jin. 4. Stock, 3:02 Uhr nachts. Genau die Uhrzeit aus dem Zutrittsprotokoll in Lektion 11.
Kommissar Kang fragt: „Wer ist Seo Jin?“ Die Direktorin holt den Bilderrahmen, der mit dem Bild nach unten in der Schublade lag, und stellt ihn auf den Schreibtisch. Zwei junge Menschen stehen auf dem Foto unter dem Schild dieser Detektei.
„Der Mann, mit dem ich Grep & Co. gegründet habe. F-02 war sein Ausweis.“ Mit der Fingerspitze tippt die Direktorin auf den Mann im Bild. „Er war ein Genie. Alles schrieb er als einzeiligen Regex. Niemand konnte seinen Code lesen, niemand konnte ihn reparieren. Also haben wir ihn Stück für Stück lesbar aufgeschrieben … und er hat das wohl so verstanden, als würden wir ihn auslöschen. Eines Tages ließ er nur seinen Ausweis zurück und ging.“
„Und warum haben Sie den Ausweis nicht gesperrt?“ Statt zu antworten, sieht die Direktorin auf die letzte Zeile auf dem Bildschirm, (a+)+$. „Ausgerechnet der, für den ein unlesbarer Regex der beste war, schickt seinen Namen so, dass man ihn lesen kann. Er wollte von Anfang an, dass ihn jemand liest.“
Fall abgeschlossen — Der Backtracker ist Seo Jin, Gründungsmitglied von Grep & Co. Mit dem nie gesperrten Ausweis F-02 ging er in der Detektei ein und aus und hinterließ die Herausforderung und die Briefe. Alle seine regulären Ausdrücke wurden lesbar aufgeschrieben, kommentiert und ins Fallnotizbuch geklebt. Der Ausweis F-02 ist gesperrt.
Am Nachmittag blättern die Direktorin und du alle zwölf Fallakten noch einmal von vorn durch. „Wie würden wir es heute schreiben?“
re.escape("$4.99").\d{2,3}[가-힣]\d{4}, prüfen mit fullmatch.search, sondern re.fullmatch(r"[a-z][a-z0-9_]{3,15}", user_id).(?P<y>\d{4})(?P<sep>[-./])(?P<m>\d\d)(?P=sep)(?P<d>\d\d). Mit Namen muss man keine Nummern zählen.groupdict(), um eine Zeile in ein dict zu verwandeln.count=.^a+$ statt ^(a|aa)+$. Keine überlappenden Wiederholungen.re.X aufschreiben und mit re.compile benennen.MISMATCH.Spät in der Nacht kommt noch eine Mail. Diesmal ist sie nicht verschlüsselt.
Han Byeol. Danke, dass du bis zum Ende gelesen hast. Es war nicht so, dass damals niemand meinen Code lesen konnte. Ich habe ihn nur nicht so geschrieben, dass man ihn lesen kann. Nächstes Mal schreibe ich Kommentare.
(?x) a+ $ # ich gehe nicht mehr zurück
Die Signatur hat sich geändert. Aus Gewohnheit steckst du sie ins Labor. Ohne die äußere Wiederholung zögert das Muster nicht einmal, wenn es scheitert.
sign = r"(?x) a+ $ # ich gehe nicht mehr zurück"
print(re.fullmatch(sign, "a" * 30 + "b")) # NoneDie Direktorin legt ihr altes Notizbuch auf deinen Schreibtisch. Von der ersten Seite an steht zu jedem Fall ihre Handschrift, dicht an dicht. „Das gehört jetzt dir. Nicht der kürzeste Regex gewinnt. Der lesbare gewinnt. Vergiss das nie.“
= als ASSIGN und den Zeilenumbruch als NEWLINE, und zerlege damit zweizeiligen Code in Tokens: x = 3.5 und darunter y = x * 2.\U mit acht Hexadezimalstellen und entschlüssle \U0000C11C\U0000C9C4.e.pos ein ^ anzeigt. Genau das macht das Beweislabor.Das Notizbuch der Direktorin
Tokenizer: (?P<name>regel) mit | verbinden, dazu finditer + m.lastgroup. Die Reihenfolge ist die Priorität, ganz am Ende MISMATCH.
Escapes im Muster: \t \n \r \f \v \a \\, \x41 \u00e9 \U0001F600 \N{EM DASH}, oktal \0 \101. [\b] ist ein Backspace.
Buchstaben-Escapes ohne Bedeutung (\q) sind ein Fehler. Benutzermuster mit try/except re.error, e.msg e.pattern e.pos e.lineno e.colno.
Für bytes rb-Muster. Mit str gemischt gibt es einen TypeError. \w ist bei bytes ASCII, re.L nur für bytes, re.U bei bytes ein Fehler.
[[ -- && ~~ || in Klassen geben eine FutureWarning. re.Scanner, Pattern.scanner und Match.regs sind undokumentierte interne Werkzeuge.
Nicht der kürzeste Regex gewinnt, sondern der lesbare.
Ü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.