01.01.03 – Quantifizierer, Subpattern, Gruppierung

Teil 4

Bislang, in den ersten beiden Teilen, war es eigentlich recht geruhsam. Dieses Kapitel allerdings hat es in sich. Arbeitet es langsam und mit Geduld durch. Es ist der anspruchsvollere Bereich, der aber auch der interessantere ist, da man nun endlich vernünftige Regexe bauen kann. Testet nach Möglichkeit selber mit dem Regex-Tester die Beispiele durch.

4.1. Quantifizierer

So, bislang konnten wir nach einzelnen Zeichen, Zeichengruppen oder Zeichenklassen und Bereichen suchen. Wir haben es geschafft, alternativ nach dem Einen oder Anderen zu suchen. Aber eines fehlt uns noch: nämlich nach Zeichenwiederholungen zu suchen.

Weiter oben gab es das Beispiel, mit dem nach einem deutschen Datum gesucht wurde:

„\d\d\.\d\d\.\d\d\d\d“

Für jede Ziffer musste man „\d“ schreiben. Gibt es da nichts einfacheres? Doch, klar! Natürlich kann man so etwas weniger aufwändig schreiben und mit Zeichenwiederholungen arbeiten. Aber fangen wir mal vorn bei den Quantifizierern an:

+ * ? sind die wichtigsten Quantifizierer.

Das „+“-Zeichen bedeutet, dass das Zeichen vor dem Plus-Zeichen mindestens einmal an der Stelle in der Zeichenkette vorkommen muss! „pa+r“ passt somit auf ‚paar‘, aber auch auf ‚par‘ oder ‚paaaaar‘.

„Re:\s+“ heißt zum Beispiel, dass mindestens ein Whitespace-Zeichen auf das ‚Re:‘ folgen muss, um gefunden zu werden. Ich höre da schon wieder Experten rufen, dass die Benutzung von Quantifizierern nicht nur auf Zeichen beschränkt ist. Stimmt, man kann sie auch auf Metazeichen, Zeichenklassen oder anderen Elementen anwenden, die wir aber erst noch lernen müssen.

Mit dem Stern wird festgelegt, dass das Zeichen vor dem Stern beliebig häufig oder gar nicht existieren muss! Ooops, wofür soll das denn gut sein: ‚gar nicht existieren muss‚?

Am besten schauen wir uns das an einem Beispiel an: „Re:\s*\w+“

Dies ist ein Regex, mit dem wir den Anfang von Betreffzeilen aus Mails erkennen wollen (wir werden später erkennen, wofür wir uns eigentlich so häufig um die Betreffzeile kümmern und was wir mit Regexen daraus machen wollen. Ihr müsst euch leider gedulden) und dabei können einige fehlerhafte Formatierungen vorliegen, die wir natürlich mit dem Regex erkennen müssen. Na, sieht ja schon langsam so aus, wie die kryptischen Teile der Profis. Was will das Regex?

Finde ein ‚Re‘ gefolgt von einem Doppelpunkt. Danach können beliebig viele Leerzeichen (oder Whitespaces) folgen oder auch gar keine. Warum das? Da gehört doch normalerweise sowieso ein Leerzeichen hin! Na, wir wollen auch manuell verhunzte Reply-Zähler finden und sollte der Schreiber das Leerzeichen gelöscht haben, so soll das Ganze trotzdem von der Regex-Maschine gefunden werden; es soll uns nicht stören. Danach aber muss mindestens ein alphanumerisches Zeichen folgen.

Achtung: hier wird gern ein Fehler gemacht. Nehmen wir mal die folgende Aufgabe: Es sollen nur Zeilen gefunden werden, in denen beliebig viele Ziffern stehen. Man kann folgendes hin und wieder als Lösung sehen: „^[0-9]*$“

Tatsächlich aber findet das Regex auch leere Zeilen, denn der Stern steht ja auch für ‚Häufigkeit 0‘. Will man also sicherstellen, dass mindestens eine Ziffer in der Zeile auftritt, so muss das „+“-Zeichen verwendet werden: „^[0-9]+$“.

Das Fragezeichen bedeutet dagegen, dass das Zeichen davor höchstens einmal vorkommen darf, aber nicht muss, also mit der ‚Häufigkeit 0‘. „H..?se“ findet ‚Hase‘, aber auch ‚Hirse‘.

Zeichenwiederholungen lassen sich aber auch anders angeben: „{x,y}“ Hiermit lassen sich detailliert Häufigkeiten definieren. „x“ steht für die Mindestanzahl und „y“ für die Maximalzahl, die ein Zeichen vorkommen darf. „\d{2,4}“ bedeutet, dass nur das gefunden wird, was mindestens zwei aber höchstens vier Ziffern hat.

Lässt man das „y“ weg, also „{x,}“, so muss das Zeichen mindestens x-mal vorkommen, ohne obere Grenze.

Lässt man „,y“ weg, also in der Form „{x}“, so wird dies als absolute Zahl gewertet: nicht mehr, aber auch nicht weniger häufig darf das Zeichen vorkommen. Nun können wir unser Datumssuchmuster umschreiben: „\d{2}\.\d{2}\.\d{4}“

Die zu Beginn beschriebenen Quantifizierer „?+*“ sind eigentlich Spezialschreibweisen der folgenden:

{0,1} = ?

{1,} = +

{0,} = *

Bevor wir zu einem weiteren Problem der Quantifizierer kommen, müssen wir erst die runden Klammern als Gruppierungsmöglichkeit einführen:

4.2. Gruppierung, Subpattern und noch mal Quantifizierer

Gruppierung

Im Abschnitt über Alternativen begegnete uns erstmalig die runde Klammer als Metazeichen. Sie wurde dort wie in der Mathematik verwendet: gleiche Elemente wurden ausgeklammert.

Sie kann aber auch dazu genutzt werden, Suchmuster zu gruppieren, zum Beispiel um darauf einen Quantifizierer anzuwenden: „foo(bar)?“ findet ‚foo‘ und auch ‚foobar‘.

Anderes Beispiel:

„Re\s*(\[\d+\])?:“ Schon wieder unsere Betreffzeile! Aber diesmal schon recht professionell. Wir suchen nach der Zeichenkette ‚Re‘. Dieser kann, nach beliebigen Whitespace-Zeichen, die eckige Klammer mit dem Zähler folgen, sie muss aber nicht. Die Whitespace-Zeichen haben wir mit dem Stern versehen, damit die Zeichenkette auch dann gefunden wird, wenn der Verfasser einfach das Leerzeichen manuell eingefügt hat, seine Leertaste prellt und mehr als ein Leerzeichen folgt oder er gar nichts gemacht hat und deswegen dort auch kein Leerzeichen steht. Normalerweise dürfte dort nämlich gar kein Leerzeichen stehen. Aber was heißt hier schon ’normalerweise‘? Zusätzlich erlauben wir sogar, dass der Zähler astronomisch groß werden darf, denn wir geben mit dem „+“-Zeichen an, dass mindestens eine Ziffer in der Klammer stehen muss, allerdings dürfen es auch beliebig viele sein.

Gefunden wird also:

‚Re:‘

‚Re [1]:‘

‚Re[123]:‘

Nicht gefunden wird dagegen ‚Re []:’ Aufgabe für zwischendurch: Was müsste am Regex geändert werden, damit auch diese Reply-Zähler gefunden werden?

Lösung: Das „+“-Zeichen müsste nur gegen einen Stern ausgetauscht werden: „Re\s*(\[\d*\])?:“

Aus der Zeichenkette ‚Re [1]: [3]:‘ findet das Regex allerdings ‚Re [1]:‘ Der zweite Teil passt nicht auf das gesamte Suchmuster. Wollen wir solche verkorksten Reply-Zähler finden, müssen wir uns noch mal an das Regex machen. Es soll nun zusätzlich beliebig viele oder keine eckige Klammern finden, die auch noch einen Doppelpunkt haben können „(\[\d+\]:\s*)*“. Am Schluß soll noch ein Doppelpunkt folgen „:?“

„Re\s*(\[\d+\]:\s*)*:?“

Damit werden nicht alle kaputten Reply-Zähler gefunden. Da gibt es eine Unmenge an denkbaren Kombinationen. Wer will, kann dies mal selbst erarbeiten. Aber immerhin haben wir jetzt den Vorspann eines Betreffs schon ganz gut im Griff und könnten uns auf das konzentrieren, was nach diesem Vorspann kommt: der eigentliche Betreff!

An dieser Stelle sei aber darauf hingewiesen, dass es nicht immer Sinn macht, das perfekte Regex zu suchen. Der Aufwand steigt sehr, nur um die unwahrscheinlichsten Zeichenkombinationen zu finden, die so gut wie nie auftreten. Man erkauft sich den Perfektionismus mit unlesbaren Regex-Code und einer größeren Fehleranfälligkeit, sollte man mal das Regex geringfügig ändern wollen. Wie immer im Leben, gilt auch hier: „Man sollte mal Fünfe gerade sein lassen!“

Als weiteres Beispiel soll wieder unsere Datumssuche dienen: „\d{2}\.\d{2}\.\d{4}“ Wie zu sehen ist, wiederholt sich der Anfang „\d{2}\.“ Genau das lässt sich mit Klammern verkürzen: „(\d{2}\.){2}\d{4}“ Das erste Element des Suchmusters – in den runden Klammern – soll nun mindestens zweimal vorkommen. Dies entspricht genau der Kombination ‚01.02.‘

Auch weiterhin gilt, dass dies nicht die optimale Version ist, da die Tages- und Monatsangaben zweistellig sein müssen und außerdem unsinnige Zahlenkombinationen als Datum erkannt werden könnten. Aber lasst uns noch ein paar weitere Sachen kennen lernen, dann werden wir das Problem in einer der Aufgaben angehen.

Subpattern

Die Klammersetzung hat noch einen ganz anderen Effekt, der in vielen Regexe zum Einsatz kommt. Zeichenketten, die von eingeklammerten Suchmustern gefunden wurden, werden in eine temporäre Variable (Subpattern) für spätere Zwecke zwischengespeichert. Das schaut man sich am besten in einigen Beispielen an:

‚erika.mustermann@beispiel.de‘

Wir verwenden das folgende Regex „(\w+)\.(\w+)@.*“. Die erste Klammer passt auf ‚erika‘, die zweite ‚mustermann‘. Und genau diese beiden Zeichenketten landen in den Subpattern1 bzw. 2.

Oder:

„(\d+\.)(\d+\.)“ Dieses Regex wenden wir auf die Zeichenkette ‚22.02.‘ an. Im ersten Subpattern wird ’22.‘ gespeichert, im zweiten ’02.‘.

Wie stellt man eigentlich fest, was das erste Subpattern ist? In obigen Fall ist das ja einfach, was aber, wenn das Regex so aussieht: „Re\s*(\[(\d+)\])*:“ Das was von dem ersten runden Klammerpaar umfasst wird, wird in Subpattern 1 gespeichert, das von dem zweiten im Subpattern 2 und so weiter. In unserem Beispiel würde aus dem String ‚Re [4]:‘

Subpattern 1 = ‚[4]‘

Subpattern 2 = ‚4‘

D.h.: mit jeder neuen geöffneten Klammer wird eine neue Variable erzeugt. Will man das verhindern, also eine Gruppierung vornehmen, ohne dass deren Inhalt in einem Subpattern landet, so kann dies durch Einfügen von „?:“ hinter der öffnenden Klammer erreicht werden: „(?:…)“

Was steht denn in der Subpattern-Variable, wenn eine Klammer mit einem Quantifizierer versehen ist? Nehmen wir mal wieder unser Datums-Regex: „(\d{2}\.){2}\d{4}“

Wird es nun auf das Datum ‚19.02.2001‘ angewendet, so wird das erste Subpattern bei der ’19.‘ gefunden. Die Regex-Maschine versucht nun einen weiteren Teil der Zeichenkette mit dem gleichen Muster (Subpattern) zu finden. Ist sie erfolgreich, dann legt sie diesen Wert erneut in das Subpattern ab. Mit anderen Worten: der zweite Fund überschreibt den ersten. Das Ergebnis ist, dass in unserem Beispiel ’02.‘ im Subpattern enthalten ist.

Der Regex-Tester zeigt euch die Subpattern an. Der kleine Karteireiter unten mit der ‚0‘ zeigt den gesamten ‚Fund‘ an, der mit der ‚1‘ den ersten Subpattern usw. Tatsächlich werden bei den Programmiersprachen die gefundenen Elemente in ein Array abgelegt. Das Element mit dem Index 0 beinhaltet in der Tat den gesamten Match; die Subpattern sind in den Elementen mit der korrespondierenden Index-Ziffer.

Und noch mal Quantifizierer

Kommen wir zu einer Besonderheit der Quantifizierer. Einige von ihnen haben eine gewisse ‚menschliche‘ Neigung: sie sind gierig! Was das bedeutet? Schauen wir uns mal den folgenden String an:

„Die Abkürzung ‚ISP‘ heißt ‚Internet Service Provider‘.“

Wir wollen ein Regex, das beliebigen Text in Hochkommata findet und als Subpattern ablegt.

„(.*)'(.*)‘.*“

Und was steht im Subpattern 2? „Internet Service Provider“ Ooops, ich hätte eigentlich erwartet, dass „ISP“ darin steht, denn das erscheint schließlich auch als erstes im Text zwischen den Hochkommata. Aber hier wird schon deutlich, dass das erste (.*) sehr gierig alles genommen hat, was es finden konnte und nur soviel für die anderen Elemente des Regex ließ, wie unbedingt nötig sind. Damit blieb nur der zweite in Hochkommata befindliche Text für Subpattern 2 übrig. Dies hängt im gewählten Beispiel aber auch am letzten „.*“, denn dieser Teil ist auch dann erfüllt, wenn „nichts“ für diesen Teil bleibt.

Nehmen wir mal ein weiteres Beispiel:

Wir wollen aus einer beliebigen Mailadresse möglichst viele Elemente extrahieren.

Für den Namen hatten wir schon weiter oben eine Lösung, die aber nicht perfekt ist, da sie nur Wortzeichen akzeptierte. Wir werden das jetzt allgemeiner fassen. Wir nehmen (.*) für den ersten Teil. Der zweite Teil wird durch einen Punkt abgetrennt mit einem weiteren Textzusatz. Dieses kann mehrfach vorkommen bevor das @ erscheint, muss aber nicht. Dieses Regex sollte folgende Beispiele erfassen:

‚1234abc@mail.de‘

‚1234.abc@mail.de‘

’12-34.abc.def@mail.de‘

Das Regex beginnt mit „(.*)\.?(.*)*@“ Danach folgt beliebiger Text, möglicherweise getrennt von mehreren Punkten. Uns soll aber von diesem Rest nur das, was nach dem letzten Punkt kommt, wichtig sein, um das Beispiel nicht unnötig komplizierter zu gestalten. Das sollte man mit „(.*)\.(.*)“ erhalten können.

„(.*)\.(.*)*@(.*)\.(.*)“

Was erwarten wir als Ergebnis, wenn man als Zeichenkette ’12-34.abc.def@mail.de‘ eingeben?

Subpattern 1 = ’12-34′ ?

Subpattern 2 = ‚.abc‘ oder ‚.def‘ oder ‚abc.def‘ ?

Subpattern 3 = ‚mail‘ ?

Subpattern 4 = ‚de‘ ?

Lasst uns mal den Regextester fragen.

Subpattern 1 = ’12-34.abc‘

Subpattern 2 = ‚ def ‚

Subpattern 3 = ‚mail‘

Subpattern 4 = ‚de‘

Das Subpattern 1 hat fast den gesamten Vorspann erhalten, das Subpattern 2 dagegen nur die letzten drei Zeichen vor dem @! Klar, denn das * ist gierig und hat soviel wie möglich im ersten Subpattern „gefressen“.

Vorsicht: nicht nur das Sternchen ist gierig, Pluszeichen sind es auch!

Gebt mal ’12-34.abc.def@mail.test.de‘ ein. Auch hier ist das Suchmuster „(.*)“ im dritten Subpattern gierig. Es frisst sofort alles nach dem @-Zeichen bis zum letzten Punkt und speichert ‚mail.test‘ im Subpattern und nicht, wie man vielleicht vermuten könnte, nur ‚mail‘.

Wie lässt sich das vermeiden? Wir lernen wir eine neue Bedeutung des Fragezeichens kennen (immer mit der Ruhe: es ist erst die zweite Bedeutung, es kommen noch mehr. Aber das sollte erkären, warum es soviele Fragezeichen in Regexe gibt *gg*): durch Anfügen des ? an das gierige Suchmuster wird das Regex veranlasst, weniger gierig zu sein.

Konkret für das erste Subpattern:

„(.*?)\.(.*)@(.*)\.(.*)“

Subpattern 1= ’12-34′

Subpattern 2= ‚abc.def‘

Subpattern 3= ‚mail.test‘

Subpattern 4=’de‘

Ein kleiner Exkurs um die Vorgehensweise des Regex zu verstehen: nennen wir das, was die Regexe ausführt mal „Regex-Maschine“. Die Regex-Maschine lässt der Gier des (.*) normalerweise freien Lauf. In dem Augenblick, in dem es aber auf die Kombination (.*?) trifft, passiert folgendes: es wird soviel wie möglich von der Regex-Maschine in das Subpattern (.*?) gesteckt, als wäre das Fragezeichen gar nicht da und nun Zeichen für Zeichen rückwärts gehend wieder frei gegeben, bis das gesamte Regex insgesamt gerade wieder passt.

Wie sieht das praktisch aus:

„(.*?)\.(.*)*“ ist der betreffende Part, den wir auf den String ’12-34.abc.def‘ anwenden. Die Regex-Maschine frisst erst mal ’12-34.abc‘ für das erste Muster. Weil danach ein Punkt und noch Text folgt, ist dies das Maximum, denn damit wäre das Regex (ohne ?) erfüllbar. Es stellt aber fest, dass wir mit dem ? die Gier unterdrücken wollen. Also gibt es nun erst das ‚c‘ frei, was aber noch nicht ausreicht, denn nach dem gefundenen Muster folgt nun kein Punkt, sondern das ‚c‘. Dann das ‚b‘, was auch nicht reicht. Anschließend noch das ‚a‘. Nun stellt die Maschine fest, dass dies auch zu einem Treffer führt, da sich vor dem ‚a‘ noch ein Punkt befindet und somit das Minimum für einen Treffer erfüllt. In Realität geht die Regex-Maschine noch weiter zurück, um festzustellen, diese Position die letzte war, die mit einem Minimum an Zeichen einen positiven Match erlaubt. Ihr könnt euch denken, dass dies die Regex-Maschine erheblich beansprucht und es die Performance drückt, aber manchmal ist es halt nötig.

Wie funktioniert das beim ersten Beispiel mit den Hochkommata?

Das Regex war: „(.*)'(.*)‘.*“ und der Text: „Die Abkürzung ‚ISP‘ heißt ‚Internet Service Provider‘.“

Ändern wir es mal in „(.*?)'(.*?)‘.*“ Hier müssen beide geklammerten Suchmuster mit Fragezeichen versehen werden, damit nicht das zweite den Text “ ISP‘ heißt ‚Internet Service Provider“ findet! Nur das zweite Muster mit Fragezeichen zu versehen, nützt natürlich auch nichts, da schon das erste gefräßig ist!

4.3. Überblick über dieses Kapitel

Dies war ein sehr schwieriger Teil. Nicht nur für das Verständnis, sondern auch zum Schreiben. Allerdings ist es ein grundlegender Bereich der Regexe und wird häufig benötigt.

Wir haben folgende Elemente kennen gelernt:

  • Zeichen, mit denen vorangegangene Suchmusterzeichen wiederholt werden können, auch Quantifizierer genannt:
    • + das Zeichen davor muss mindestens einmal vorkommen, darf aber häufiger
    • ? das Zeichen davor darf höchstens einmal oder aber gar nicht vorkommen
    • * das Zeichen davor darf beliebig häufig oder auch gar nicht vorkommen.
  • Es gibt Quantifizierer, die Bereiche für Häufigkeiten angeben: {x,y} das Zeichen davor muss mindestens x-mal, aber höchstens y-mal vorkommen. Man kann dabei Teile des Bereiches weglassen: {x,} heißt beliebig häufig, aber mindestens x-mal. {x} heißt genau x-mal
  • Runde Klammern können zur Gruppierung von Suchmustern verwendet werden, um zum Beispiel Quantifizierer auf sie anzuwenden: „(ab)+“ heißt, die Buchstabenkombination ‚ab‘ muss mindestens einmal vorkommen.
  • Suchmuster in runden Klammern werden „gemerkt“ und in Variablen für spätere Verwendung gespeichert. Sie werden Subpattern genannt. Bei geschachtelten Klammern wird das gesamte Ergebnis eines vom Klammerpaar umgebenen Suchmusters in eine Variable gespeichert; die Ergebnisse der inneren Klammern sind dann Teilmengen der jeweils äußeren. Die erste öffnende Klammer erzeugt die erste Variable, die zweite erzeugt die zweite Variable usw…
  • Quantifizierer ohne obere Grenze sind in bestimmten Suchmustern gierig. + und * hinter einem Punkt veranlassen das Regex, alles zu finden, was möglich ist, damit das Suchmuster gerade soeben noch passt. Das Regex (.*)(.*) wird jede Zeichenkette komplett in das erste Subpattern legen.
  • Durch Anfügen eines Fragezeichens an gierige Suchmuster (.*?) wird das Regex veranlasst, nicht gierig zu sein. Zwar wird zunächst dennoch alles „gefressen“, aber dann Zeichen für Zeichen zurückgegeben, bis das Regex gerade noch passt.

Aufgaben

1: Das Regex für die Datumsangabe ist immer noch nicht perfekt, weil ja einstellige Tages- oder Monatszahlen und ggf. zweistellige Jahreszahlen nicht gefunden werden. Wäre doch mal ’ne Übung wert, oder?

2: So, die Lösung für das obige Problem ist ganz interessant, aber nun wollen wir ein Regex bauen, dass etwas besser nach dem Datum sucht. Es soll nach Möglichkeit nur etwas gefunden werden, was nach einem Datum aussieht. Es muss ja nicht übertrieben werden: wenn es laut Regex auch den 29.2. in einem Nicht-Schaltjahr gibt, ist das nicht so schlimm. Wichtig ist: TT.MM.JJJJ oder T.M.JJ sollen gefunden werden.

3: Über ein Online-Fehlermeldesystem kommen standardisierte Meldungen, die mit einem Regex in seine Bestandteile zerlegt werden soll. Die Meldungen haben die Form:

Absender: vorname.nachname@amt.de

Datum: TT.MM.JJJJ
Fehlernr.: xyz123

Bitte erstellt ein Regex, dass die einzelnen Felder (Vorname, Name, Amt, Datum, Fehlernummer) als Subpattern findet. Stellt euch vor, dass die Informationen in einem einzigen String stehen und die Zeilenumbrüche gar nicht existieren.

4. Erstellt ein Regex, das Uhrzeiten in der Form hh:mm:ss findet. Dabei sollen nach Möglichkeit nur gültige Kombinationen gefunden werden.

Zu 1:
„\d{1,2}\.\d{1,2}\.(\d{4}|\d{2})“

Ihr habt etwas anderes? Macht nichts, muss nicht falsch sein. Es gibt immer mehrere Wege, es richtig zu machen:

„(\d?\d\.){2}(\d{4}|\d{2})“

wäre die elegante Lösung. Was ich weniger gelungen fände, wäre „\d{2,4}“ für die Jahreszahl, da dann auch dreistellige Jahreszahlen akzeptiert würden.

Zu 2:
Dies ist sicherlich der kompliziertere Fall. Zerlegen wir das mal in Teilprobleme. Die möglichen Tage sind:

a)01-09, wobei die führende Null fehlen könnte.

b)10-29, alle Monate haben 29 Tage. Ok, gewollter Fehler: der Februar hat eben nur alle 4 Jahre 29 Tage, aber damit wollen wir jetzt mal leben, sonst wird es nahezu unmöglich.

c)30, haben alle Monate außer Februar

d)31, haben nur die Monate Januar, März, Mai, Juli, August, Oktober, Dezember

Die möglichen Monate sind 01-10, wobei die Null fehlen darf und 11, 12.

Wir wollen nur beliebige zweistellige oder vierstellige Jahreszahlen zulassen. Im letzteren Fall sollte die Jahresangabe aber schon mit 19xx oder 20xx beginnen.

Na, dann wollen wir mal:
Fall a) und b) mit den Monaten
„(0?[1-9]|[12][0-9])\.(0?[1-9]|1[0-2])\.“

Fall c) mit den Monaten
„30\.((0?[13-9])|(1[0-2]))\.“

und dann noch Fall d) mit seinen Monaten
„31\.(0?[13578]|1[02])\.“

Zu den Jahren:
„(\d{2}|(19|20)\d{2})“

Die ersten drei Elemente sind Alternativen, die Jahreszahl ist obligatorisch. Damit nicht aus längeren Ziffernfolgen zufällige Datumsangaben gefunden werden, umgeben wir das Regex noch mit dem \b-Operator. Dann muss das also ungefähr so aussehen:

„\b(((0?[1-9]|[12][0-9])\.(0?[1-9]|1[0-2])\.)|(30\.((0?[13-9])|(1[0-2]))\.)
|(31\.(0?[13578]|1[02])\.))(\d{2}|(19|20)\d{2})\b“

[Anm.: Das Regex wird aus Layout-Gründen umgebrochen. Tatsächlich muss sich alles in einer Zeile befinden!]

Wahnsinn: ganz schön lang. Ihr habt was anderes? Sogar was besseres? Das würde ich als „normal“ bezeichnen. Das obige Regex lässt sich natürlich bei gleichem Ergebnis anders schreiben. Und „besser“ geht fast immer 😉 Meine Lösung oben zeigt nur, wie ich das Problem angegangen habe. Ich hoffe, dies ist nachvollziehbar gewesen.

Zu 3.

Das sieht gar nicht so wild aus. Auch hier wieder in Teilen:

Vorname und Name sowie Amt ergeben sich aus der Mailadresse. Damit sollte
„Absender:\s*(.*?)\.(.*?)@(.*?)\.\w+\s*“
ausreichen. Das ? im zweiten Subpattern ist vielleicht gar nicht nötig, weil danach ohnehin das @-Zeichen folgen muss. Aber es schadet auch nix.

Datum: welch Glück, das Format ist fest. Man muss also nicht das mörderische Regex aus Aufgabe 2 verwenden:
„Datum:\s*((\d{1,2}\.){2}\d{4})\s*“

Und nun noch Fehlernummer:
„Fehlernr.:\s*(.*)“

Um sicher zu gehen, dass auch der ganze String abgefragt wird, veranlassen wir mit \A und \Z, dass der ganze Text ausgewertet wird.

„\AAbsender:\s*(.*?)\.(.*?)@(.*?)\.\w+\s*Datum:\s*
((\d{1,2}\.){2}\d{4})\s*Fehlernr.:\s*(.*)\Z“
[Anm.: Das Regex wird aus Layout-Gründen umgebrochen. Tatsächlich muss sich alles in einer Zeile befinden!]

Die Subpattern 1,2,3,4 und 6 sollten die gewünschten Felder liefern.

Zu 4.

Nun haben wir ja schon gewisse Übung darin, Probleme in Teilprobleme zu zerlegen und das Uhrzeit-Problem ist erneut so eines. Es sollte uns also eigentlich leicht von der Hand gehen. Es macht es geringfügig einfacher, dass das Format feststeht!

Stunden gibt es von 00-19 Uhr und von 20-23 (24 Uhr ist 00 Uhr!):
„([01][0-9]|2[0-3]):“

Minuten und Sekunden verwenden die gleichen Ziffernkombinationen, nämlich 00 bis 59:
„([0-5][0-9]:){2}“

Zusammen, umgeben von Wortbegrenzern:
„\b([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]\b“

weiter