Zum Inhalt
wannacode

Reguläre Ausdrücke: Gruppen und Ersetzen

Kurz gesagt: Gruppen in einem regulären Ausdruck, Teile in Klammern, holen Stücke aus jedem Treffer; in Python findest du sie mit re.finditer und verwendest sie beim Ersetzen mit \1.

Problem

Der Kontoauszug

Deine Bank schickt dir einen Text mit den Zahlungen des Monats. Du willst jeden Betrag einzeln, um sie zusammenzuzählen:

import re
statement = "Miete 450,00 € am 01.10, Strom 87,35 € am 05.10, Wasser 23,10 € am 12.10"
print(re.fullmatch(r".*\d+,\d{2} €.*", statement) is not None)
Ausgabe
True
fullmatch sagt nur „ja, irgendwo steht ein Betrag“. Das .* auf beiden Seiten ist da, weil fullmatch will, dass das Muster den ganzen Text abdeckt. re.sub würde die Beträge ändern, aber es gibt sie dir nicht. Du brauchst einen Weg, jeden Treffer zu finden und ihn zu nehmen.

Die Idee

re.compile und finditer

Zum Suchen hat das Modul re ein Muster-Objekt und ein Treffer-Objekt:

import re
statement = "Miete 450,00 € am 01.10, Strom 87,35 € am 05.10, Wasser 23,10 € am 12.10"
amount = re.compile(r"\d+,\d{2} €")
for m in amount.finditer(statement):
print(m.group())
Ausgabe
450,00 €
87,35 €
23,10 €
  • re.compile(...) macht aus dem Muster ein Pattern-Objekt. Mach das einmal und verwende es so oft du willst.
  • amount.finditer(statement) liefert die Treffer einen nach dem anderen, von links nach rechts. Deshalb steht es in einem for: Jeder Durchlauf bekommt den nächsten, bis keine mehr da sind.
  • m ist ein Match-Objekt: ein gefundener Treffer. m.group() ist sein Text.
Genauer, für Neugierige
  • re.findall liefert dir direkt eine Liste mit den Texten der Treffer, ohne die Match-Objekte.
  • re.search liefert nur den ersten Treffer, egal wo er steht, oder None. re.match prüft nur, ob der Text mit einem Treffer beginnt: Für „Tisch 12“ liefert es None, obwohl 12 im Text steht.
  • re.sub, re.split und re.fullmatch kompilieren das Muster selbst und halten die letzten Muster bereit. re.compile ist praktisch, wenn ein Muster an vielen Stellen verwendet wird.
import re
statement = "Miete 450,00 € am 01.10, Strom 87,35 € am 05.10, Wasser 23,10 € am 12.10"
print(re.findall(r"\d+,\d{2} €", statement))
print(re.match(r"\d+", "Tisch 12"))
print(re.match(r"\d+", "12 Tische").group())
Ausgabe
['450,00 €', '87,35 €', '23,10 €']
None
12

Sieh es dir an

Wo der Treffer steht

Außer dem Text weiß der Treffer auch, wo er gefunden wurde:

import re
log = "ERROR Festplatte; INFO Start; ERROR Netzwerk"
count = 0
for m in re.finditer("ERROR", log):
count += 1
print(f"{m.group()} von {m.start()} bis {m.end()}")
print(f"Gesamt: {count}")
Ausgabe
ERROR von 0 bis 5
ERROR von 30 bis 35
Gesamt: 2
m.start() ist der Index des ersten Zeichens des Treffers, und m.end() ist der Index nach dem letzten, wie bei einem Slice. Deshalb ist log[m.start():m.end()] dasselbe wie m.group().

Probier es aus

Eine Ziffer oder eine Zahl?

Was gibt das Programm aus?

import re
for m in re.finditer(r"\d+", "Tisch 12, 4 Personen, 2 Flaschen"):
print(f"[{m.group()}]", end="")
print()
Antwort

Die Idee

Gruppen: Teile des Treffers

Klammern im Muster bilden eine Gruppe. Der Treffer merkt sich für jede Gruppe einzeln, was auf sie gepasst hat:

import re
date = re.compile(r"(\d{2})\.(\d{2})\.(\d{4})")
m = date.search("Fällig: 15.11.2026")
if m:
print(m.group())
print(m.group(1) + " / " + m.group(2) + " / " + m.group(3))
print(m.group(3) + "-" + m.group(2) + "-" + m.group(1))
Ausgabe
15.11.2026
15 / 11 / 2026
2026-11-15
  • Die Gruppen werden nach ihren öffnenden Klammern nummeriert, von links nach rechts, ab 1.
  • m.group(1) ist Gruppe 1. group(0) oder group() ist der ganze Treffer.
  • search ist für den Fall, dass du nur den ersten Treffer brauchst. Gibt es keinen, ist m gleich None, und if m: überspringt ihn.

So nimmst du aus einem Datum Tag, Monat und Jahr einzeln heraus und ordnest sie so an, wie du sie brauchst.

import re
date = re.compile(r"(\d{2})\.(\d{2})\.(\d{4})")
m = date.search("Fällig: 15.11.2026")
if m:
print(m.group())
print(m.group(1) + " / " + m.group(2) + " / " + m.group(3))
print(m.group(3) + "-" + m.group(2) + "-" + m.group(1))
Schritt für Schritt, in Worten
  1. Das Datumsmuster hat drei Klammerpaare: drei Gruppen.
  2. search sucht das Muster im Text. Findet search es, ist m der Treffer (Match), sonst None.
  3. search findet das Datum: von Index 8 bis 18.
  4. Jede Gruppe merkt sich ihren Teil: Gruppe 1 ist 15, Gruppe 2 ist 11, Gruppe 3 ist 2026.
  5. Die Teile liegen in m, jeder unter seiner Nummer. group(0) ist der ganze Treffer.
  6. group() ist dasselbe wie group(0): 15.11.2026.
  7. Die drei Gruppen, eine nach der anderen.
  8. In umgekehrter Reihenfolge ergeben die Teile 2026-11-15.
Genauer, für Neugierige
  • (?:...) ist eine Gruppe, die die Teile nur zusammenhält, ohne sie sich zu merken. Zum Beispiel bildet (?:\+359|0)8[789]\d{7} keine Gruppe 1.
  • date.groups liefert die Anzahl der Gruppen im Muster.
  • Wenn eine Gruppe am Treffer nicht beteiligt war (zum Beispiel, weil (,\d{2})? fehlte), ist die Gruppe None.
import re
date = re.compile(r"(\d{2})\.(\d{2})\.(\d{4})")
print(date.groups)
m = re.search(r"(\d+)(,\d{2})? €", "Trinkgeld 5 €")
print(m.group(1), m.group(2))
Ausgabe
3
5 None

Die Idee

Benannte Gruppen und Ersetzen

Wenn es viele Gruppen gibt, verwechselt man die Nummern. Du kannst ihnen Namen geben: (?P<name>...), und dann m.group("name"). In Python steht ein P nach dem Fragezeichen: Die Form (?<name>...) aus Java und JavaScript ist in Python 3.12 ein Fehler. Hier ist eine Zeile von einer Café-Rechnung:
import re
item = re.compile(r"(?P<qty>\d+) x (?P<name>[A-Za-z ]+) zu (?P<price>\d+\.\d{2})")
for m in item.finditer("2 x Kaffee mit Milch zu 3.20; 1 x Croissant zu 2.90"):
qty = int(m.group("qty"))
price = float(m.group("price"))
print(f"{m.group('name')}: {qty * price} €")
Ausgabe
Kaffee mit Milch: 6.4 €
Croissant: 2.9 €
Gruppen funktionieren auch in re.sub: Im Ersetzungstext ist \1 die Gruppe 1, und \g<name> ist die Gruppe mit diesem Namen. Auch der Ersetzungstext ist ein Raw-String, wegen des \. Hier die Daten von der deutschen Form in die ISO-Form und in die amerikanische:
import re
text = "Frist: 15.11.2026, Erinnerung: 08.11.2026"
print(re.sub(r"(\d{2})\.(\d{2})\.(\d{4})", r"\3-\2-\1", text))
print(re.sub(r"(?P<d>\d{2})\.(?P<m>\d{2})\.(?P<y>\d{4})", r"\g<m>/\g<d>/\g<y>", text))
Ausgabe
Frist: 2026-11-15, Erinnerung: 2026-11-08
Frist: 11/15/2026, Erinnerung: 11/08/2026
Hier brauchst du re.compile und finditer nicht: re.sub versteht Gruppen von selbst.
Genauer, für Neugierige
  • \ ist im Ersetzungstext ein Sonderzeichen. Wenn der Text vom Benutzer kommt, zum Beispiel ein Pfad zu einem Ordner, kann re.sub mit einem Fehler abbrechen. Verdopple die Backslashes darin mit replace("\\", "\\\\"):
import re
user = "C:\\Temp"
try:
re.sub("ORDNER", user, "Kopie in ORDNER")
except re.error as e:
print(e)
print(re.sub("ORDNER", user.replace("\\", "\\\\"), "Kopie in ORDNER"))
Ausgabe
bad escape \T at position 2
Kopie in C:\Temp
  • Du kannst eine Gruppe auch im Muster selbst verwenden: \1 heißt „dasselbe wie Gruppe 1“. So fängst du wiederholte Wörter. Sieh dir die Karte zu den Flags an.

Probier es aus

Wie viel nimmt +?

Du willst alle HTML-Tags aus der Zeile:

Was gibt das Programm aus?

import re
html = "<b>Preis:</b> <i>12 €</i>"
for m in re.finditer(r"<.+>", html):
print(m.group())
Antwort

Die Idee

Gierig und genügsam

Ein ? nach einem Quantifizierer macht ihn genügsam: Er nimmt so wenig, wie er kann, und hält bei der ersten Gelegenheit an.

import re
html = "<b>Preis:</b> <i>12 €</i>"
for m in re.finditer(r"<.+?>", html):
print(m.group())
print(re.sub(r"<[^>]+>", "", html))
Ausgabe
<b>
</b>
<i>
</i>
Preis: 12 €
  • <.+?> heißt „<, mindestens ein Zeichen und das erste > danach“.
  • <[^>]+> macht dasselbe auf andere Weise: „<, Zeichen, die nicht > sind, und >“. Es kann kein > überspringen, deshalb ist es auch schneller. Damit entfernst du die Tags, und nur der Text bleibt übrig.
Gierig Genügsam
* *?
+ +?
? ??
{2,5} {2,5}?

Probier es aus

Finde den Fehler

Das Programm soll die drei Bestellnummern ausgeben, gibt aber nur #1042 aus. In welcher Zeile ist der Fehler?

Wähle die Zeile mit dem Fehler.

Die Idee

Flags und Zeilen

Die Flags gibst du als zweites Argument von re.compile an, verbunden mit |. Aus einer Logdatei mit mehreren Zeilen holst du die Fehlermeldungen, egal ob groß oder klein geschrieben:

import re
log = "INFO: Start\nERROR: Keine Verbindung zur Datenbank\nINFO: Neuer Versuch\nerror: Festplatte 95%"
p = re.compile(r"^error: (.+)$", re.IGNORECASE | re.MULTILINE)
for m in p.finditer(log):
print(m.group(1))
Ausgabe
Keine Verbindung zur Datenbank
Festplatte 95%
  • re.IGNORECASE ist dasselbe wie (?i): Groß- und Kleinbuchstaben gelten als gleich.
  • re.MULTILINE macht ^ und $ zum Anfang und Ende jeder Zeile. Ohne es sind sie nur Anfang und Ende des ganzen Textes, und das Muster findet nichts.
  • re.DOTALL lässt . auch einen Zeilenumbruch fangen.
Genauer, für Neugierige

Zwei Dinge, denen du in fremden Mustern begegnen wirst:

  • Eine Rückreferenz (backreference) \1: „derselbe Text wie Gruppe 1“. So fängst du wiederholte Wörter.
  • Ein Lookahead (?=...): „irgendwo ab hier kommt ...“, ohne Zeichen zu verbrauchen. Mehrere davon hintereinander sind praktisch für Passwörter: „hat eine Ziffer“ und „hat einen Großbuchstaben“, und mindestens 8 Zeichen.
import re
twice = re.compile(r"\b(\w+) \1\b")
for m in twice.finditer("Die Zahlung für für März ist eingegangen, bitte bitte prüfe sie."):
print("Wiederholt: " + m.group(1))
strong = r"(?=.*\d)(?=.*[A-Z]).{8,}"
print(re.fullmatch(strong, "geheim12") is not None, re.fullmatch(strong, "Geheim12") is not None)
Ausgabe
Wiederholt: für
Wiederholt: bitte
False True

(?!...) ist das Gegenteil: „ab hier kommt kein ...“. Es gibt auch Lookbehind: (?<=...) und (?<!...).

Übung

Finden, herausholen, ersetzen

Löse die Aufgaben in deinem Editor. Der Text ist eine Zeile von der Tastatur. In der Lektion „Dateien“ machst du sie auch mit einer Datei.

1. Suchen und ersetzen. Ersetze das Wort „alt“ überall, wo es ein ganzes Wort ist, durch „neu“, und sag, wie viele Ersetzungen du gemacht hast. Teile anderer Wörter („altmodisch“) bleiben unverändert. re.subn macht dasselbe wie re.sub, gibt aber ein Paar zurück: den neuen Text und die Anzahl der Ersetzungen.

Eingabe
Das Gerät ist alt. Auch das Sofa ist alt, aber nicht altmodisch.
Ausgabe
Das Gerät ist neu. Auch das Sofa ist neu, aber nicht altmodisch.
Ersetzt: 2

2. Wörter mit einem Präfix. In der ersten Zeile steht das Präfix, in der zweiten der Text. Gib jedes Wort aus, das mit dem Präfix beginnt.

Eingabe
auf
aufräumen aufstehen Laufband ablaufen aufgeben
Ausgabe
aufräumen
aufstehen
aufgeben

3. Adressen im Text. Hol alle Webadressen heraus, die mit http:// oder https:// beginnen. Der Punkt am Satzende gehört nicht zur Adresse.

Eingabe
Siehe die Seite https://www.example.com und diese: http://www.test.com. Die Dokumente liegen unter https://docs.example.com/guide/start.
Ausgabe
https://www.example.com
http://www.test.com
https://docs.example.com/guide/start

4. Daten nach ISO. Mach aus jedem Datum tt.mm.jjjj im Text die Form jjjj-mm-tt, mit einem einzigen re.sub.

Eingabe
Vertrag vom 03.10.2026, Zahlung bis 15.10.2026.
Ausgabe
Vertrag vom 2026-10-03, Zahlung bis 2026-10-15.

Weitere Aufgaben

  • Summe des Kontoauszugs. Nimm die Beträge aus der ersten Karte mit einer Gruppe für die Euro und einer Gruppe für die Cent, zähl sie zusammen und gib „Summe: 560,45 €“ aus.
  • Hashtags. Hol alle Wörter, die mit # beginnen, aus einem Beitrag, in dem Wörter mit und ohne Umlaute (ä, ö, ü, ß) gemischt sind. Überleg, was mit dem Flag re.ASCII passieren würde.
  • Ohne Tags. Entferne die HTML-Tags aus <p>Die Frist ist der <b>15.11</b>.</p> und gib den reinen Text aus.
  • Telefonnummern im Text. Finde alle Handynummern in einem Text und gib sie in der Form +359 88 812 3456 aus, mit Gruppen und re.sub.

Test

Teste dich

1. Was liefert re.finditer(muster, text)?
2. Das Muster ist (\d{2})\.(\d{2})\.(\d{4}), der Text ist 15.11.2026. Was ist m.group(2)?
3. Was ergibt re.sub(r"(\d+)\.(\d+)\.(\d+)", r"\2/\1", "05.03.2026")?
4. Warum fängt "<.+>" den ganzen Text "<b>Preis</b>" und nicht nur "<b>"?
5. Was macht re.MULTILINE?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·