∷ Skripte ∷ Reguläre Ausdrücke
Diese Seite wendet sich eher nicht an Neulinge, die in die Themen Formale Sprache
, Reguläre Grammatik
und Endlicher Automat
frisch einsteigen möchten.
Stattdessen werden nachfolgend komplexere Problemstellungen betrachtet und analysiert, um dann geeignete Lösungswege zu finden.
Die aufgezeigten Vorgehensweisen sind für Schulungs- oder Unterrichtszwecke gut verwendbar, allerdings sind Grundkenntnisse sowie ein bisschen Praxis hinsichtlich der erwähnten Wissensgebiete von Vorteil, um die einzelnen Schritte und zugrunde liegenden Ideen besser nachvollziehen zu können.
Alle letztlich hergeleiteten regulären Ausdrücke können online mittels kurzer, einfacher PowerShell-Skripte ausprobiert werden.
Inhaltsverzeichnis:
- Formale Definition
- Durch 3 glatt teilbare Binärzahlen
- Prüfung der Passwortkomplexität
- Parsen eines eMail-Headers
- Datum und Uhrzeit validieren
- Fazit
① Formale Definition
Reguläre Ausdrücke beschreiben Mengen von Wörtern und werden induktiv definiert:
- (Verankerung) Ist Σ ein endliches Alphabet von Literalen; dann sind jeweils Folgendes reguläre Ausdrücke:
- Das literale Zeichen
a
∈ Σ bezeichnet die Menge {a
}, die nur aus dem Worta
besteht. - Die leere Menge ∅ bezeichnet die Menge { }, die kein einziges Wort enthält.
- Das leere Wort ε bezeichnet die Menge { ε }, die nur eine leere Zeichenfolge enthält.
- Das literale Zeichen
- (Induktion) Sind R und S reguläre Ausdrücke, dann erzeugen die folgenden Operationen wiederum einen regulären Ausdruck:
- Die Alternative
(
R|
S)
bezeichnet die Menge von Zeichenfolgen, die durch Vereinigung der durch R und S spezifizierten Mengen entsteht, z. B. {ab
,c
,d
,ef
}, wenn R die Wörter {ab
,c
} und S die Wörter {ab
,d
,ef
} beschreiben. - Die Verkettung
(
RS)
bzw.(
R⋅S)
bezeichnet die Menge von Zeichenfolgen, die durch Aneinanderreihung je eines Worts aus R und S entsteht, z. B. {abd
,abef
,cd
,cef
}, wenn R die Wörter {ab
,c
} und S die Wörter {d
,ef
} beschreiben. - Die Kleenesche Hülle
(
R*)
bezeichnet die Menge von Zeichenfolgen, die durch beliebig häufige Wiederholung von R entsteht, z. B. { ε,ab
,abab
,abc
,c
,cab
,ccc
… }, wenn R die Wörter {ab
,c
} beschreibt.
- Die Alternative
Diese Definition legt zum einen fest, wie mittels nur 3 Operatoren ein komplexer regulärer Ausdruck zusammengesetzt werden kann, aber auch, wie und welche Wörter dann durch ihn abgedeckt werden.
Zusammen mit Festlegung der Rangfolge
* vor ⋅ vor |
– wodurch einige Klammern eingespart werden können – lässt sich beispielsweise zu den Ziffern 0
und 1
unter Anwendung der Operationen 4 bis 6 der Ausdruck
(0|1)*1
entwickeln, welcher für eine beliebig lange Sequenz von Nullen und Einsen steht, die mit einer 1
abschließt, also auf eine ungerade Zahl in Binärdarstellung zutrifft.
Das ist doch eigentlich recht intuitiv, oder?
Darüber hinaus hat sich in der Praxis eine erweiterte Syntax etabliert, u. a.
- steht R
+
für RR*
, - ist R
{2,4}
eine Abkürzung für(
RRRR|
RRR|
RR)
sowie R{3}
für RRR, - entsprechen R
?
und R{0,1}
und(
R|)
einander, - sind
[abcdef]
und[a-f]
äquivalent zu(a|b|c|d|e|f)
, - ist
[^abcdef]
oder[^a-f]
deren Negation (beschreibt also die Menge, welche alle Literale außera
bisf
enthält), - entsprechen
\d
und[0123456789]
und[0-9]
und(0|1|2|3|4|5|6|7|8|9)
einander, - steht der Punkt für ein beliebiges einzelnes Literal aus dem Alphabet,
- stehen die Anker
^
für den Anfang und$
für das Ende einer Sequenz/Eingabe, - handelt es sich beispielsweise bei
\n
und\r
um Steuerzeichen mit dem ASCII-Code 10 (Zeilenumbruch, englisch Line Feed (LF)) bzw. 13 (Wagenrücklauf, englisch Carriage Return (CR)).
② Durch 3 glatt teilbare Binärzahlen
Nun, im Gegensatz zum sehr einfachen Beispiel im vorangegangenen Abschnitt ist es dem folgenden regulären Ausdruck nicht unbedingt auf Anhieb anzusehen, dass er den prinzipiellen Aufbau aller glatt durch 3 teilbaren Binärzahlen beschreibt:
(0|1(01*0)*1)*
Um ihn besser zu begreifen, soll ein endlicher Automat konstruiert werden, der ziffernweise eine Binärzahl liest und durch seinen gerade aktuellen Zustand signalisiert, welchen Rest die bisherige Eingabe bei Division durch 3 ergibt.
Dafür wird dieser mit den drei Zuständen A
, B
und C
ausgestattet, die für die jeweilige Restklasse 0modulo 3, 1modulo 3 oder 2modulo 3 stehen.
Zu Beginn befinde sich der Automat im Zustand A
; genau in diesem muss er für eine glatt durch 3 teilbare Eingabe auch anhalten.
Nachfolgend sei angenommen, dass sich der Automat gerade im Zustand A
befinde, also die bisherige Eingabe glatt durch 3 teilbar sei.
Wird dann eine 0
gelesen, versetzt dies quasi alle bisherigen Ziffern um eine Stelle nach links, entspricht also bei Dezimalwerten deren Multiplikation mit 10 und im Dualsystem einer Verdopplung.
In diesem Fall muss der Automat im Zustand A
verbleiben, denn ist n glatt teilbar, dann sind es auch 2n oder 10n.
Dies kann durch einen von A
ausgehenden Pfeil symbolisiert werden, der für die Ziffer 0
wieder im Zustand A
endet.
Analog verbleibt eine Binärzahl auf jeden Fall in der Restklasse 2modulo 3 (bzw. im Zustand C
), solange ihr rechts einfach nur Einsen angefügt werden.
Durch entsprechende Überlegungen wird außerdem klar, dass eine vergleichbare Stabilität für den Zustand B
nicht gegeben sein kann und sich dieser durch das Anhängen einer beliebigen binären Ziffer zwangsläufig ändern muss, entweder nach A
(im Falle einer 1
) oder andernfalls nach C
.
Insgesamt lassen sich durch weitere Argumentationen gemäß diesem Schema 6 Transitionen begründen, u. a. auch dass eine zuvor glatt teilbare Zahl unweigerlich in die Restklasse 1modulo 3 fällt, wenn ihr ein Einselement nachgestellt wird.
Somit ergibt sich das folgende vollständige Schaubild:
An diesem Diagramm lässt sich am Beispiel der Binärzahl 1000100100010
(dezimal: 4386) gut nachverfolgen, wie die einzelnen Ziffern abgearbeitet werden, welche Statusübergänge dabei passieren und wie dann mit dem Lesen des leeren Worts ε am Zeilenende ein akzeptierter Zustand erreicht wird.
Auch kann erkannt werden, dass die um die vordere Stelle gekürzte Zeichenkette 000100100010
(dezimal: 290) letztlich im Endstatus C
hängenbleibt und welche Möglichkeiten es gibt, sie derart zu vervollständigen, dass sie doch noch durch 3 teilbar wird, u. a. durch das Anfügen von 01
, 111101
oder 0001
.
Nun soll das Verhalten der skizzierten Maschine in eine Transitionstabelle überführt werden:
0 | 1 | ε | ||
---|---|---|---|---|
A | A | B | ✔ | (0|1 ⬊ ⬈)* |
B | C | A | ✘ | (0 ⬊ ⬈)*1 |
C | B | C | ✘ | 1*0 |
Diese ist so zu interpretieren, dass ihre Zeilen jeweils für einen aktuellen Status stehen und sich dann an den Spalteninhalten der neue Zustand ersehen lässt, wenn als Nächstes eine 0
, 1
oder das Eingabeende ε gelesen wird; dabei markiert der grüne Haken einen akzeptierten Endstatus, welcher hier einzig und allein für A
gegeben ist.
Insgesamt wird dadurch also eine Abbildung
Zustandneu = ƒ(Zustandalt, Zeichen)
definiert.
Ein Verbleib im Zustand A
ist nur möglich, wenn als weitere Ziffer eine 0
kommt oder durch das Lesen einer 1
in den Zustand B
gewechselt und später von dort zurückgekehrt wird.
Analog wird im Zustand B
verblieben, solange durch Verarbeitung einer 0
nach C
gewechselt und daraus wieder zurückgekehrt wird; tritt dagegen die Ziffer 1
auf, wird er verlassen.
Und im Zustand C
wird nur während des Lesens von 1
verweilt, denn eine 0
bewirkt den sofortigen Rücksprung.
All dies lässt sich zu einem regulären Ausdruck zusammenfassen:
(0|1(01*0)*1)*
Im nachfolgenden PowerShell-Skript wird die obige Tabelle verwendet, um einen entsprechenden Algorithmus zu implementieren.
Dabei verdeutlicht der relativ einfach aufgebaute Quelltext mit gerade mal einer skalaren Variablen $s
, auf die innerhalb der Abarbeitungsschleife Schreibzugriffe erfolgen, dass ein endlicher Automat quasi nur in der Gegenwart lebt und über keinerlei Erinnerung an vergangene Zustände oder vormals Gelesenes verfügt.
# Finite-state machine for the following regular expression: (0|1(01*0)*1)* function Test-BinaryIsMultipleOf3([String]$b) { <# Literals: #> $l = '0', '1' <# Transitions: #> $t = @{ 'A' = 'A', 'B', $true # (0|1 )* 'B' = 'C', 'A', $false # `(0 )*1´ 'C' = 'B', 'C', $false } # `1*0´ <# Initial state: #> $s = 'A' foreach ($c in [String[]][Char[]]$b) { # Read... if ($l -eq $c) { $s = $t[$s][$c] } # ...literal else { return $false } } # ...non-literal $t[$s][-1] # ...end of line } Test-BinaryIsMultipleOf3 1000100100010 # True Test-BinaryIsMultipleOf3 000100100010 # False Test-BinaryIsMultipleOf3 00010010001001 # True
Die Funktion Test-BinaryIsMultipleOf3
signalisiert durch wahr oder falsch, ob eine Binärzahl glatt durch 3 teilbar ist.
Indem die Wahrheitswerte in der Transitionstabelle ausgetauscht werden, lassen sich andere Trefferbilder konfigurieren, beispielsweise alle nicht glatt durch 3 teilbaren Binärzahlen
oder alle Binärzahlen mit dem Rest 2 bei Division durch 3
.
Natürlich lässt sich die Funktion in einer Laufzeitumgebung, welche mit regulären Ausdrücken umgehen kann, auch sehr viel einfacher formulieren:
function Test-BinaryIsMultipleOf3([String]$b) { $b -match '^(0|1(01*0)*1)*$' } Test-BinaryIsMultipleOf3 1000100100010 # True Test-BinaryIsMultipleOf3 000100100010 # False Test-BinaryIsMultipleOf3 00010010001001 # True
Zugegeben, der betrachtete Anwendungsfall ist nicht besonders realitätsnah, da im täglichen Leben eher selten nach glatt teilbaren Binärzahlen gesucht wird, aber er zeigt recht eindrucksvoll, wie bei der Ausarbeitung regulärer Ausdrücke vorgegangen werden kann und was mit ihnen prinzipiell machbar ist.
Zum Abschluss dieses Abschnitts sei noch eine kleine Übungsaufgabe gestellt:
Wie könnte ein aus dem obigen Automatenmodell abgeleiteter regulärer Ausdruck aussehen, der alle Binärzahlen mit dem Rest 1 bei Division durch 3
beschreibt?
– Für die Einblendung der Lösung bewege den Mauszeiger hierhin!
③ Prüfung der Passwortkomplexität
Weitaus öfter als die zuvor behandelte Teilbarkeit durch 3 muss in der Praxis sichergestellt werden, dass Kennwörter gewisse Anforderungen erfüllen. Deshalb soll in diesem Abschnitt ein regulärer Ausdruck entwickelt werden, der alle Passwörter findet, welche nicht den folgenden Komplexitätsvoraussetzungen genügen:
- Das Kennwort muss eine Mindestlänge von 6 haben.
- Und es muss Elemente aus dreien der folgenden Kategorien enthalten:
- Dezimalziffern (
0
bis9
), - Großbuchstaben (
A
bisZ
), - Kleinbuchstaben (
a
bisz
), - nicht-alphanumerische Zeichen (z. B. Leer- oder Sonderzeichen wie
ä
,á
,!
,$
,_
,#
,%
usw.).
- Dezimalziffern (
Die notwendigen Zeichenklassen für die Punkte a bis d sind schnell erzeugt, wobei deren Kürzel für Ziffern, Großbuchstaben, Kleinbuchstaben und Sonstiges stehen:
Z:=[0-9] G:=[A-Z] K:=[a-z] S:=[^0-9A-Za-z]
Allerdings macht die Definition von S
nicht ganz glücklich, denn sie ist wegen der Negation und vielen Intervalle etwas komplizierter als die übrigen; aber der Einfachheit halber soll erst einmal mit diesem Makel gelebt werden.
Passwörter, die nicht den obigen Punkt 2 erfüllen, lassen sich daran erkennen, dass sie sich nur aus Zeichen von höchstens 2 der 4 Kategorien zusammensetzen.
Dieser Sachverhalt wird nachfolgend mit Hilfe eines regulären Ausdrucks formuliert:
[Z]+|[G]+|[K]+|[S]+|[ZG]+|[ZK]+|[GK]+|[KS]+|[GS]+|[ZS]+
Doch diese Zeile ist länger, als sie eigentlich zu sein bräuchte, denn wenn eine der 4 vorderen Alternativen zutrifft, liefern auch jeweils 3 der übrigen 6 Terme Treffer; insofern lässt sie sich vereinfachen zu:
[ZG]+|[ZK]+|[GK]+|[KS]+|[GS]+|[ZS]+
Per Definition sind die 4 Zeichenklassen Z
, G
, K
und S
nicht nur paarweise disjunkt, sondern vereinigen sich sogar zu einem Ganzen, welches alle potentiell in Kennwörtern erlaubten Zeichen umfasst.
Durch Differenzbildung ergeben sich die folgenden Äquivalenzen:
[KS]≡[^ZG] [GS]≡[^ZK] [ZS]≡[^GK]
Mittels entsprechender Ersetzungen gelingt es, nicht nur S
rückstandslos aus den Termen zu eliminieren, sondern letztlich auch den oben erwähnten Makel los zu werden, mit dem diese Zeichenklasse von Anfang an behaftet war, und den bisherigen Ausdruck umzuwandeln nach:
[ZG]+|[ZK]+|[GK]+|[^ZG]+|[^ZK]+|[^GK]+
Als Nächstes sollen nun auch Z
, G
und K
aufgelöst werden, und zwar durch ihre Definitionen:
[0-9A-Z]+|[0-9a-z]+|[A-Za-z]+|[^0-9A-Z]+|[^0-9a-z]+|[^A-Za-z]+
Dieser reguläre Ausdruck findet somit Wörter, die nicht den Punkt 2 der Komplexitätsvorgaben erfüllen.
Dass in ihm all die unter die Kategorie d fallenden nicht-alphanumerischen Zeichen nicht mehr explizit vorkommen, ist ein durchaus angenehmer und willkommener Nebeneffekt.
Nun auch noch den Punkt 1 zu kontrollieren, ob die Eingabe zu kurz ist, fällt relativ einfach, denn eine solche Prüfung ist schnell ergänzt:
.{0,5}|[0-9A-Z]+|[0-9a-z]+|[A-Za-z]+|[^0-9A-Z]+|[^0-9a-z]+|[^A-Za-z]+
Abschließend muss alles mit (?-i)^(?:
…)$
ummantelt werden, damit Teiltreffer verhindert sowie Groß- und Kleinschreibung beachtet werden:
(?-i)^(?:.{0,5}|[0-9A-Z]+|[0-9a-z]+|[A-Za-z]+|[^0-9A-Z]+|[^0-9a-z]+|[^A-Za-z]+)$
Voilà, damit ist ein handlicher regulärer Ausdruck gefunden, der beurteilen kann, ob ein Passwort das zu Beginn dieses Abschnitts aufgestellte Regelwerk verletzt. Im nachfolgenden PowerShell-Einzeiler kommt er dann auch zum Einsatz und listet sämtliche Zeichenfolgen mit ungenügender Komplexität auf:
'20.10.2010', 'Pa$$wort', 'g heim', ';-)', 'Sei 8sam!', '08/15', 'U-Bahn' -match '(?-i)^(.{0,5}|[0-9A-Z]+|[0-9a-z]+|[A-Za-z]+|[^0-9A-Z]+|[^0-9a-z]+|[^A-Za-z]+)$' # 20.10.2010 # g heim # ;-) # 08/15
Je nach Kontext kann auf den Inline-Modifier (?-i)
verzichtet, muss die Option m
(ultiline
) aktiviert oder sollten die 3 Negationen auf \n
ausgedehnt werden:
[Regex]::Matches( ('20.10.2010', 'Pa$$wort', 'g heim', ';-)', 'Sei 8sam!', '08/15', 'U-Bahn') -join "`n", '(?-i)^(.{0,5}|[0-9A-Z]+|[0-9a-z]+|[A-Za-z]+|[^0-9A-Z\n]+|[^0-9a-z\n]+|[^A-Za-z\n]+)$', 'Multiline') | Format-Table -Property Index, Length, Value # Index Length Value # ----- ------ ----- # 0 10 20.10.2010 # 20 6 g heim # 27 3 ;-) # 41 5 08/15
Sind arithmetische Operationen verfügbar, dann können statt eines einzelnen regulären Ausdrucks alternativ je ein separater für Ziffern, Groß-/
'20.10.2010', 'Pa$$wort', 'g heim', ';-)', 'Sei 8sam!', '08/15', 'U-Bahn' | Where-Object -FilterScript { $_.Length -lt 6 -or 3 -gt ($_ -match '\d') + ($_ -cmatch '[A-Z]') + ($_ -cmatch '[a-z]') + ($_ -match '[^0-9A-Z]') } # 20.10.2010 # g heim # ;-) # 08/15
④ Parsen eines eMail-Headers
In der Praxis sind reguläre Ausdrücke längst nicht mehr nur auf die formale Definition zu Beginn dieses Artikels beschränkt, sondern um kontextsensitive Fähigkeiten wie Rückwärtsreferenzen und (gar vorausschauende) Lookaround Assertions ergänzt. Von diesen Erweiterungen wird ab sofort in den Anwendungsbeispielen Gebrauch gemacht.
Gruppierungen (
…)
innerhalb eines regulären Ausdrucks werden – solange der öffnenden Klammer nicht unmittelbar ?:
folgt – implizit von links nach rechts mit 1 beginnend durchnummeriert.
Darüber lässt sich der entsprechende Teilausdruck referenzieren, beispielsweise innerhalb einer Bedingung der Form (?(
Nummer)
…|
…)
:
Hat die durch (
Nummer)
spezifizierte Gruppe einen Teiltreffer abbekommen, dann wird der Ausdruck links vom senkrechten Strich angewandt, andernfalls derjenige rechts davon.
Im Falle eines Gesamttreffers füllt PowerShell zudem automatisch eine Hashtabelle namens $Matches
, in der sich die jeweiligen Teiltreffer innerhalb der Gruppen über ihre Nummern indizieren lassen.
Ferner kann der Operator -split
Zeichenketten mittels eines regulären Ausdruck zerlegen.
Alles zusammen wird im nachfolgenden Quelltext kombiniert, um dem Kopfbereich einer eMail bestimmte Informationen wie Übermittlungszeitpunkt, Betreff, Absender und Empfänger zu entnehmen.
Um den Gebrauch von Bedingungen zu demonstrieren, werden dabei Zeilen mit ungültigen eMail-Adressen (hier Cc: aaaaaa@######ddd
) übergangen, wobei der dafür dienende reguläre Ausdruck (?:\w+@\w+\.[a-z]+(?:, *)?)+
bewusst sehr einfach gehalten ist:
Er akzeptiert durchaus mehrere, durch Kommas separierte eMail-Adressen, aber nur ohne Anzeigenamen.
Für den Praxiseinsatz sollte entweder dies verbessert oder auf die Validierung verzichtet werden.
[String]$header = @" Received: (qmail 12345 invoked from network); Wed, 20 Feb 2002 20:00:20 +0530 Date: Wed, 20 Feb 2002 20:00:02 +0530 Subject: Hello World! Message-ID: <123456BC.7891011@aaaaaa.ddd> From: xxxx@yyyyyy.zz To: zzzzzz@xxxxx.yyy, YYYYYY@zzzzz.xxx Cc: aaaaaa@######ddd Content-Type: text/plain; charset=UTF-8 "@ [Hashtable]$hashtable = @{} $header -split '\r?\n' | Where-Object -FilterScript { $_ -match '^((From|To|Cc)|(Date)|Subject): ((?(2)(?:\w+@\w+\.[a-z]+(?:, *)?)+|.*))' } | ForEach-Object -Process { $hashtable[$Matches.1] = if ($Matches.2) { $Matches.4 -split ', *' } elseif (!$Matches.3) { $Matches.4 } else { Get-Date -Date $Matches.4 -Format 'yyyy-MM-dd HH:mm:ss' } } $hashtable # Name Value # ---- ----- # Date 2002-02-20 15:30:02 # From xxxx@yyyyyy.zz # To {zzzzzz@xxxxx.yyy, YYYYYY@zzzzz.xxx} # Subject Hello World!
⑤ Datum und Uhrzeit validieren
Zusätzlich zu den im vorangegangenen Abschnitt eingeführten Bedingungen werden von nun an Lookaround Assertions benutzt, welche reguläre Ausdrücke um die Möglichkeit erweitern, kontextabhängige Bedingungen zu formulieren, ohne den Kontext selbst als passend zu finden. Zu einem Treffer identisch zum regulären Ausdruck R kommt es
für R(?= …) | nur unmittelbar vor … (Positive Lookahead), |
---|---|
für R(?! …) | nur, wenn nicht … direkt folgt (Negative Lookahead), |
für (?<= …) R | nur unmittelbar hinter … (Positive Lookbehind), |
für (?<! …) R | nur, wenn nicht … direkt voransteht (Negative Lookbehind). |
Dabei ist … zwar für das Umfeld von Bedeutung, aber nicht Teil des Treffers!
Doch nun zu Schaltjahren:
Sie sind ohne Rest durch 4 teilbar, aber nicht durch 100, es sei denn, durch 400; insofern ist 2000 ein Schaltjahr, jedoch nicht 1900.
Der nachstehende reguläre Ausdruck erkennt alle Jahreszahlen zwischen 1900 und 2099.
Dabei kommt es für Schaltjahre innerhalb der Gruppierung A
zu einem Teiltreffer.
(?:19|20)(?:(?:(?<=20)|(?!00))(?<A>[02468][048]|[13579][26])|\d\d)
Der folgende reguläre Ausdruck erkennt alle zweiziffrigen Monatsnummern zwischen 01 und 12; davon zieht die Gruppe B
die Zahl 02 an und die Gruppierung C 04, 06, 09 sowie 11:
(?:(?<B>02)|(?<C>0[469]|11)|0[13578]|10|12)
In Schaltjahren besitzt der Februar 29 Tage, sonst 28; von den übrigen Monaten haben Februar, April, Juni, September und November 30 Tage und alle sonstigen 31.
Unten beurteilen mehrere Bedingungen für den Februar mittels der Gruppierung A
, ob dieser über 29 oder nur 28 Tage verfügt, und für alle übrigen Monate mit Hilfe der Gruppe C
, ob sie 30 oder gar 31 Tage besitzen.
Die restlichen Teile des regulären Ausdrucks handhaben die anderen Werte zwischen 01 und 29.
(?:[01][1-9]|10|(?(B)2(?(A)[0-9]|[0-8])|(?:2\d|3(?(C)0|[01]))))
Somit kann der per Bindestrichen verbundene Gesamtausdruck sämtliche Datumsangaben zwischen 1900-01-01 und 2099-12-31 validieren:
[String]$r = @( '((?:19|20)(?:(?:(?<=20)|(?!00))(?<A>[02468][048]|[13579][26])|\d\d))' '((?<B>02)|(?<C>0[469]|11)|0[13578]|10|12)' '([01][1-9]|10|(?(B)2(?(A)[0-9]|[0-8])|(?:2\d|3(?(C)0|[01]))))' ) -join '-' -split '1900-02-29 1904-02-29 2000-02-29 2022-05-31 2022-06-31' -match $r # 1904-02-29 # 2000-02-29 # 2022-05-31 if ('2000-02-29' -match $r) { [String]$Matches[0, 3, 2, 1] } # 2000-02-29 29 02 2000
Dies funktioniert natürlich auch mit ausschließlich automatisch durchnummerierten statt benannten Gruppierungen, allerdings müssen dann die Referenzierungen neu justiert werden:
[String]$r = @( '((?:19|20)(?:(?:(?<=20)|(?!00))([02468][048]|[13579][26])|\d\d))' '((02)|(0[469]|11)|0[13578]|10|12)' '([01][1-9]|10|(?(4)2(?(2)[0-9]|[0-8])|(?:2\d|3(?(5)0|[01]))))' ) -join '-' -split '1900-02-29 1904-02-29 2000-02-29 2022-05-31 2022-06-31' -match $r # 1904-02-29 # 2000-02-29 # 2022-05-31 if ('2000-02-29' -match $r) { [String]$Matches[0, 6, 3, 1] } # 2000-02-29 29 02 2000
Oft gibt es nicht nur die eine
Lösung, sondern mehrere Möglichkeiten einer Formulierung.
Beispielsweise basieren die beiden folgenden Implementierungen auf Lookaround Assertions:
[String]$r = '((?:19|20)\d\d)-(0[1-9]|1[0-2])-(' + # Match YYYY-MM- '0[1-9]|1\d|2[0-8]|' + # Match days between 01 and 28 '(?<!1900....)(?<=(?:[02468][048]|[13579][26]).02.)29|' + # Match leap day in Feb '(?<=(?:0[469]|11).)(?:29|30)|' + # Match days 29 and 30 in Apr, Jun, Sep, Nov '(?<=(?:0[13578]|10|12).)(?:29|30|31)' + # Match days 29 to 31 in other months ')' '1900-02-29', '1904-02-29', '2000-02-29', '2022-05-31', '2022-06-31' | ForEach-Object -Process { if ($_ -match $r) { [String]$Matches[,0 + 3..1] } } # 1904-02-29 29 02 1904 # 2000-02-29 29 02 2000 # 2022-05-31 31 05 2022 [String]$r = '(' + '0[1-9]|1\d|2[0-8]|' + # Match days between 01 and 28 '29(?!....1900)(?=.02...(?:[02468][048]|[13579][26]))|' + # Match leap day in Feb '(?:29|30)(?=.(?:0[469]|11))|' + # Match days 29 and 30 in Apr, Jun, Sep, Nov '(?:29|30|31)(?=.(?:0[13578]|10|12))' + # Match days 29 to 31 in other months ')\.(0[1-9]|1[0-2])\.((?:19|20)\d\d)' # Match .MM.YYYY '29.02.1900', '29.02.1904', '29.02.2000', '31.05.2022', '31.06.2022' | ForEach-Object -Process { if ($_ -match $r) { Write-Host -Object $Matches[0..3] } } # 29.02.1904 29 02 1904 # 29.02.2000 29 02 2000 # 31.05.2022 31 05 2022
Die nachstehenden Varianten entsprechen eher der formalen Definition, weil sie keine kontextsensitiven Operationen wie Bedingungen, Referenzen oder Lookaround Assertions verwenden, aber im Falle eines Treffers werden nicht mehr Tag, Monat und Jahr unter gleichbleibenden Indizes bereitgestellt:
'1900-02-29', '1904-02-29', '2000-02-29', '2022-05-31', '2022-06-31' -match '(?:19|20)\d\d-(?:0[1-9]|1[0-2])-(?:0[1-9]|1\d|2[0-8])|' + # YYYY-MM-01 to YYYY-MM-28 '(?:2000|(?:19|20)(?:0[48]|[2468][048]|[13579][26]))-02-29|' + # Leap day YYYY-02-29 '(?:19|20)\d\d-(?:0[13-9]|1[0-2])-(?:29|30)|' + # YYYY-MM-29 else and YYYY-MM-30 '(?:19|20)\d\d-(?:0[13578]|10|12)-31' # YYYY-MM-31 # 1904-02-29 # 2000-02-29 # 2022-05-31 '29.02.1900', '29.02.1904', '29.02.2000', '31.05.2022', '31.06.2022' -match '(?:0[1-9]|1\d|2[0-8])\.(?:0[1-9]|1[0-2])\.(?:19|20)\d\d|' + # 01.MM.YYYY to 28.MM.YYYY '29\.02\.(?:2000|(?:19|20)(?:0[48]|[2468][048]|[13579][26]))|' + # Leap day 29.02.YYYY '(?:29|30)\.(?:0[13-9]|1[0-2])\.(?:19|20)\d\d|' + # 29.MM.YYYY else and 30.MM.YYYY '31\.(?:0[13578]|10|12)\.(?:19|20)\d\d' # 31.MM.YYYY # 29.02.1904 # 29.02.2000 # 31.05.2022
Alternativ könnte auch nur das Format JJJJ-MM-TT oder TT.MM.JJJJ mittels eines regulären Ausdrucks geprüft werden und in einem nachgelagerten Schritt die Gültigkeit des Datums per Operator (z. B. -as
in PowerShell) bzw. Funktion (u. a. checkdate(
MM,
TT,
JJJJ)
in PHP):
'1900-2-29', '1904-2-29', '2000-2-29', '2022-5-31', '2022-6-31' | Where-Object -FilterScript { $_ -match '\d{4}-\d?\d-\d?\d' -and $_ -as [DateTime] } # 1904-2-29 # 2000-2-29 # 2022-5-31 if (($s = '2000-2-29') -match '\d{4}-\d?\d-\d?\d' -and ($d = $s -as [DateTime])) { [String]($s, $d.Day, $d.Month, $d.Year) } # 2000-2-29 29 2 2000 '29.2.1900', '29.2.1904', '29.2.2000', '31.5.2022', '31.6.2022' | Where-Object -FilterScript { $_ -match '(\d?\d)\.(\d?\d)\.(\d{4})' -and $Matches.3 + '-' + $Matches.2 + '-' + $Matches.1 -as [DateTime] } # 29.2.1904 # 29.2.2000 # 31.5.2022 if ('29.2.2000' -match '(\d?\d)\.(\d?\d)\.(\d{4})' -and $Matches[3..1] -join '-' -as [DateTime]) { [String]$Matches[0..3] } # 29.2.2000 29 2 2000
Relativ einfach ist die Validierung und Zerlegung einer Uhrzeit:
[String]$r = '\b([01]?\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?\b' '6:78Uhr', '09:10', ' 9:10 Uhr', '09:1', '12:34:56', '23:45 Uhr', '65:43' | Where-Object -FilterScript { $_ -match $r } | ForEach-Object -Process { @{ $_ = $Matches[0..($Matches.Count - 1)] } } # Name Value # ---- ----- # 09:10 {09:10, 09, 10} # 9:10 Uhr {9:10, 9, 10} # 12:34:56 {12:34:56, 12, 34, 56} # 23:45 Uhr {23:45, 23, 45}
Dabei steht der Anker \b
für eine Wortgrenze, d. h. eine Stelle, die nur zu einer ihrer beiden Seiten ein Zeichen aus \w
:= [A-Za-z0-9_]
aufweist, entspricht also: (?:(?:^|(?<!\w))(?=\w)|(?<=\w)(?:(?!\w)|$))
⑥ Fazit
Reguläre Ausdrücke ermöglichen sehr mächtige, auf Mustern basierende Vergleiche oder Filterungen und können viel anderweitigen (Programmier-)Aufwand einsparen.
Vielleicht werden sie von manchen als schwer zu entwickeln/Wildcards
) wie
und ?
(für ein einzelnes bzw. mehrere beliebige Zeichen) hinaus geht.
*
Alle Angaben sind ohne jegliche Gewähr. Der Autor übernimmt keinerlei Verantwortung oder Haftung für Fehler oder Ungenauigkeiten, die in diesem Dokument auftreten können. Anmerkungen, Ergänzungen und Verbesserungsvorschläge zu diesem Artikel werden gerne entgegengenommen.