regex in Python#

Erklär-Notebook zu 14: String Manipulation and Regular Expressions > flexible-pattern-matching-with-regular-expressions . Das Ausprobier-Notebook aus der Vorlesung 2023-12-06 wurde verschoben nach regex.

Vorbemerkung, Achtung, nicht verwechseln:

  • In der bash bedeutet der Stern: Null oder mehr Zeichen

!ls *AUTO*.ipynb
e_r1_quizz_AUTO.ipynb			     f_f8_2024-03-03_AUTO.ipynb
e_r2b_AUTO.ipynb			     f_f8_2024-03-03_quizz_AUTO.ipynb
e_r2_funktionsparameter_BMI_AUTO.ipynb	     json-beispiel42_AUTO.ipynb
f_f1_AUTO.ipynb				     python-101-ln2_AUTO.ipynb
f_f2_AUTO.ipynb				     python-101-ln3-AUTO.ipynb
f_f3_bibliotheken-kernfunktionen_AUTO.ipynb  python-101-ln4-AUTO.ipynb
f_f4_AUTO.ipynb				     q_r2b_AUTO.ipynb
f_f5_AUTO.ipynb				     quizz_mc_typen_AUTO.ipynb
f_f7_AUTO.ipynb				     zweiundvierzig_AUTO.ipynb

in RegEx bedeutet der Stern:

  • null mal oder öfters das vorausgehende Teilpattern

Bsp: ISO 8601 Datum suchen#

gegeben: ein Text mit Datumsangaben, z.B.

text = ("""Der 24.12.2023 ist in diesem Jahr ein Sonntag. 
Der Unterricht geht bis Freitag 2023-12-22, weiter dann 2024.01.08. 
In 2024 gibt es sogar einen Donnerstag 2024-02-29, aber nicht 2024-04-31. 
Auch klar: Amerikaner notieren den Anschlag nine-eleven 9/11 mit 09/11/2001; 
Europäer notieren den 11. September 2001 mit 11-09-2001.
und das gibt es nicht: 2024-13-98""")

Gesucht:

  • Identifiziere und markiere alle Datumsangaben, die korrekt im ISO-Format ISO_8601 angegeben sind,

  • und übersetze diese – und nur diese – auf DE, mit deutschen Monatsnamen.

Eine allgemeine regex für alle erlaubten Datumsangaben nach ISO 8601 ist schwierig, siehe z.B. https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch04s07.html.

Wir beschränken uns hier zunächst auf Angebaben der Art

  • YYYY-MM-TT, mit - als einzigem erlaubten Trennzeichen.

Hier geht es darum, wie man eine Regex in Python verwendet, die man schon konstruiert hat. Die Regex selbst wird man nicht hier im Quellcode konstruieren (geht nur für Experten) und testen (geht auch für Experten nicht), sondern mit einem interaktiven Frontend, wie z.B. mit https://regex101.com/.

In unserem Fall begnügen wir uns mit einem sehr einfachen Pattern, nämlich (\d{4})-(\d\d)-(\d\d)

import re
# regex ohne Gruppierungen
datum_regex_string = r"\d{4}-\d\d-\d\d"

# interessanter: regex mit Gruppierungen
datum_regex_string2 = r"(\d{4})-(\d\d)-(\d\d)"

datum_regex = re.compile(datum_regex_string2)
datum_regex
re.compile(r'(\d{4})-(\d\d)-(\d\d)', re.UNICODE)
type(datum_regex)
re.Pattern

re.findall()#

Wenn wir nur an einer Liste aller Datumsangaben – ggf. auch gleich nach Subgroups aufgeteilt – interessiert sind, können wir findall() benutzen:

datum_regex_findall = datum_regex.findall(text)
datum_regex_findall
[('2023', '12', '22'),
 ('2024', '02', '29'),
 ('2024', '04', '31'),
 ('2024', '13', '98')]

re.finditer()#

Wenn wir nicht nur an (hier: einer Liste von) allen Strings, sondern an (hier: einer Sequenz) von allen Match-Objekten interessiert sind, erzeugen wir einen Iterator:

datum_regex_iterator = datum_regex.finditer(text)
datum_regex_iterator
<callable_iterator at 0x706488a3f1f0>
for Treffer in datum_regex_iterator:
    print(Treffer)
<re.Match object; span=(80, 90), match='2023-12-22'>
<re.Match object; span=(156, 166), match='2024-02-29'>
<re.Match object; span=(179, 189), match='2024-04-31'>
<re.Match object; span=(350, 360), match='2024-13-98'>

Zur Vollständigkeit, siehe 10: Iterators: Wenn man durch einen Iterator durchgelaufen ist, ist er “verbraucht”. Wenn an ihn nochmal benutzt, liefert er keine Ergebnisse mehr:

for Treffer in datum_regex_iterator:
    print(Treffer)

Wenn man tatsächlich eine persistente Liste von Matchobjekten benötigt, kann sich ja leicht eine solche bauen:

datum_regex_matchlist = [ Treffer for Treffer in datum_regex.finditer(text) ]
datum_regex_matchlist
[<re.Match object; span=(80, 90), match='2023-12-22'>,
 <re.Match object; span=(156, 166), match='2024-02-29'>,
 <re.Match object; span=(179, 189), match='2024-04-31'>,
 <re.Match object; span=(350, 360), match='2024-13-98'>]

Eine solche Liste von Matches kann man nun z.B. in einer Schleife durchgehen, um mit den Match-Objekten etwas sinnvolles zu tun:

def iso_8601_nach_deutsch(Jahr, Monat, Tag):
    """empfängt ein Datum als 3 Strings 'JJJJ', 'MM', 'TT'; 
       gibt das Datum mit expamdierten Monatsnamen in DE zurück."""
    
    Monate = { 1: "Januar", 2: "Februar", 3: "März", 4: "April", 
          5: "Mai", 6: "Juni", 7: "Juli", 8: "August", 
          9: "September", 10: "Oktober", 11: "November", 12: "Dezember" }
    return f"{Tag}. {Monate.get(int(Monat), 'UNGÜLTIG')} {Jahr}"
iso_8601_nach_deutsch("2023", "13", "24")
'24. UNGÜLTIG 2023'
for Treffer in datum_regex_matchlist: 
    
    # Treffer ist ein Match-Objekt
    # print(f"{Treffer=}")
    j, m, t = Treffer.group(1), Treffer.group(2), Treffer.group(3)
    print(iso_8601_nach_deutsch(j, m, t))
22. Dezember 2023
29. Februar 2024
31. April 2024
98. UNGÜLTIG 2024

re.sub()#

re.sub() ist die Funktion Suchen und ersetzen. Damit erfüllen wir die ursprüngliche Aufgabe:

  • Markiere alle Datumsangaben im korrekten ISO 8601-Format

In der Funktion sub() können wir mit \1, \2 etc. auf unsere Subgroups zugreifen und diese im Ersetzungstext wiederverwenden.

print(datum_regex.sub(r"<\3.\2.\1>", text))
Der 24.12.2023 ist in diesem Jahr ein Sonntag. 
Der Unterricht geht bis Freitag <22.12.2023>, weiter dann 2024.01.08. 
In 2024 gibt es sogar einen Donnerstag <29.02.2024>, aber nicht <31.04.2024>. 
Auch klar: Amerikaner notieren den Anschlag nine-eleven 9/11 mit 09/11/2001; 
Europäer notieren den 11. September 2001 mit 11-09-2001.
und das gibt es nicht: <98.13.2024>