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 6

Zeit 4:10–5:00

Der Zugriff um 3 Uhr nachts: benannte Gruppen und Match-Objekte

CASE-006

Grep & Co. Datendetektei

Fallakte

Der Zugriff um 3 Uhr nachts

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 2210

Eine 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.

6-1. Namensschilder statt Nummern: (?P...) (10 Min.)

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.
  • Namen folgen den Regeln für Python-Bezeichner. Beginnen sie mit einer Ziffer oder enthalten sie ein -, 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

Wechsle zu finditer und sieh dir im Feld „Erster Treffer“ m.group("ip") und groupdict() an. Was fehlt im Dictionary, wenn du (?P<time>...) in ein schlichtes (...) verwandelst?

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.

6-2. Eine ganze Logzeile als Dictionary (10 Min.)

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'}
  • Weil im Muster doppelte Anführungszeichen vorkommen, steht der Raw-String in einfachen Anführungszeichen 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.
  • Die ganze Zeile muss diese Form haben, also nimmst du 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 302

Alle 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 Muster hört beim Pfad (path) auf. Häng Protokoll, Statuscode (status) und Bytes (size) an, damit die ganze Zeile per fullmatch passt. Die letzte Zeile, deren Ende abgeschnitten ist, darf nicht durchkommen.

6-3. Wo stand es? start, end, span (6 Min.)

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")])  # /admin

Optionale 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

finditer trennt in jeder Zeile Pfad und Query-String. Sieh im Feld „Erster Treffer“ nach, dass query None ist, und lösch dann die ersten zwei Zeilen des Textes, um zu sehen, ob page=2 gefangen wird. Entfernst du das [A-Z]+ ganz vorn, wird sogar /1.1 aus HTTP/1.1 als Pfad erkannt.

6-4. Die übrigen Attribute des Match-Objekts (6 Min.)

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.
  • Bei verschachtelten Gruppen schließt die äußere Klammer zuletzt. Deshalb ist lastindex bei ((\d+)\.(\d+)) nicht 3, sondern 1.
  • Verbindest du benannte Gruppen mit | 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 b

Match-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 요청: /admin

6-5. Die Admin-Seite um 3 Uhr nachts (8 Min.)

Jetzt 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“.

  • Suchst du einfach nach 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.
  • Den Pfad schreibst du als /admin\S*, damit alle Seiten erfasst werden, die mit /admin beginnen.

Beweislabor

re.search(r"", text)

Rückgabewert

Das jetzige Muster erwischt auch Frau Jeongs eigenen Zugriff um 9 Uhr morgens. Lass nur die /admin-Zugriffe übrig, bei denen die Stunde im Zeitfeld 03 ist. Noch besser: Fang ip und hour gleich mit benannten Gruppen.

Eine mögliche Lösung:

(?P<ip>\S+) \S+ \S+ \[[^:]+:(?P<hour>03):[^\]]+\] "GET (?P<path>/admin\S*)

Den Fall lösen (10 Min.)

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.

Übungen

  1. Lies das Log mit LINE_RE ein und addiere size aller Zeilen mit dem Statuscode (status) 200. Vergiss int() nicht.
  2. Finde "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.
  3. Was liefern bei 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.

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