Erforschen Sie die Funktionsweise der Python-Regex-Engine. Dieser Leitfaden entmystifiziert Mustererkennungsalgorithmen wie NFA und Backtracking und hilft Ihnen, effiziente reguläre Ausdrücke zu schreiben.
Enthüllung der Engine: Ein tiefer Einblick in die Algorithmen der Python-Regex-Mustererkennung
Reguläre Ausdrücke, oder Regex, sind ein Eckpfeiler der modernen Softwareentwicklung. Für unzählige Programmierer auf der ganzen Welt sind sie das Werkzeug der Wahl für Textverarbeitung, Datenvalidierung und Log-Parsing. Wir verwenden sie, um Informationen zu finden, zu ersetzen und zu extrahieren, mit einer Präzision, die einfache String-Methoden nicht erreichen können. Doch für viele bleibt die Regex-Engine eine Blackbox – ein magisches Werkzeug, das ein kryptisches Muster und einen String akzeptiert und irgendwie ein Ergebnis liefert. Dieses mangelnde Verständnis kann zu ineffizientem Code und in einigen Fällen zu katastrophalen Leistungsproblemen führen.
Dieser Artikel lüftet den Vorhang für das Python-Modul re. Wir werden in den Kern seiner Mustererkennungs-Engine eintauchen und die grundlegenden Algorithmen erforschen, die sie antreiben. Indem Sie verstehen, wie die Engine funktioniert, werden Sie in die Lage versetzt, effizientere, robustere und vorhersehbarere reguläre Ausdrücke zu schreiben und Ihre Nutzung dieses leistungsstarken Werkzeugs von Ratespielen in eine Wissenschaft zu verwandeln.
Der Kern regulärer Ausdrücke: Was ist eine Regex-Engine?
Im Kern ist eine Regular Expression Engine eine Software, die zwei Eingaben entgegennimmt: ein Muster (die Regex) und eine Eingabezeichenfolge. Ihre Aufgabe ist es, festzustellen, ob das Muster innerhalb der Zeichenfolge gefunden werden kann. Wenn dies der Fall ist, meldet die Engine eine erfolgreiche Übereinstimmung und liefert oft Details wie die Start- und Endpositionen des übereinstimmenden Texts und alle erfassten Gruppen.
Obwohl das Ziel einfach ist, ist die Implementierung es nicht. Regex-Engines basieren im Allgemeinen auf einem von zwei grundlegenden algorithmischen Ansätzen, die in der theoretischen Informatik, insbesondere in der Theorie der endlichen Automaten, verwurzelt sind.
- Textgesteuerte Engines (DFA-basiert): Diese Engines, die auf Deterministic Finite Automata (DFA) basieren, verarbeiten die Eingabezeichenfolge Zeichen für Zeichen. Sie sind unglaublich schnell und bieten eine vorhersehbare Leistung in linearer Zeit. Sie müssen niemals Backtracking durchführen oder Teile der Zeichenfolge neu bewerten. Diese Geschwindigkeit geht jedoch auf Kosten von Funktionen; DFA-Engines können keine erweiterten Konstrukte wie Backreferences oder Lazy Quantifiers unterstützen. Tools wie `grep` und `lex` verwenden oft DFA-basierte Engines.
- Regex-gesteuerte Engines (NFA-basiert): Diese Engines, die auf Nondeterministic Finite Automata (NFA) basieren, sind mustergesteuert. Sie bewegen sich durch das Muster und versuchen, seine Komponenten mit der Zeichenfolge abzugleichen. Dieser Ansatz ist flexibler und leistungsfähiger und unterstützt eine breite Palette von Funktionen, darunter Erfassungsgruppen, Backreferences und Lookarounds. Die meisten modernen Programmiersprachen, darunter Python, Perl, Java und JavaScript, verwenden NFA-basierte Engines.
Das re Modul von Python verwendet eine traditionelle NFA-basierte Engine, die auf einem entscheidenden Mechanismus namens Backtracking basiert. Diese Designentscheidung ist der Schlüssel sowohl zu seiner Leistungsfähigkeit als auch zu seinen potenziellen Leistungsfallen.
Eine Geschichte von zwei Automaten: NFA vs. DFA
Um wirklich zu verstehen, wie die Regex-Engine von Python funktioniert, ist es hilfreich, die beiden dominanten Modelle zu vergleichen. Stellen Sie sie sich als zwei verschiedene Strategien für die Navigation in einem Labyrinth (der Eingabezeichenfolge) mithilfe einer Karte (dem Regex-Muster) vor.
Deterministic Finite Automata (DFA): Der unerschütterliche Pfad
Stellen Sie sich eine Maschine vor, die die Eingabezeichenfolge Zeichen für Zeichen liest. In jedem gegebenen Moment befindet sie sich in genau einem Zustand. Für jedes Zeichen, das sie liest, gibt es nur einen möglichen nächsten Zustand. Es gibt keine Mehrdeutigkeit, keine Wahl, kein Zurückgehen. Das ist ein DFA.
- Wie es funktioniert: Eine DFA-basierte Engine erstellt eine Zustandsmaschine, bei der jeder Zustand eine Menge möglicher Positionen im Regex-Muster darstellt. Sie verarbeitet die Eingabezeichenfolge von links nach rechts. Nach dem Lesen jedes Zeichens aktualisiert sie ihren aktuellen Zustand basierend auf einer deterministischen Übergangstabelle. Wenn sie das Ende der Zeichenfolge erreicht, während sie sich in einem "akzeptierenden" Zustand befindet, ist die Übereinstimmung erfolgreich.
- Stärken:
- Geschwindigkeit: DFAs verarbeiten Zeichenfolgen in linearer Zeit, O(n), wobei n die Länge der Zeichenfolge ist. Die Komplexität des Musters beeinflusst die Suchzeit nicht.
- Vorhersagbarkeit: Die Leistung ist konsistent und verschlechtert sich nie in exponentielle Zeit.
- Schwächen:
- Eingeschränkte Funktionen: Die deterministische Natur von DFAs macht es unmöglich, Funktionen zu implementieren, die sich an eine vorherige Übereinstimmung erinnern müssen, wie z. B. Backreferences (z. B.
(\w+)\s+\1). Lazy Quantifiers und Lookarounds werden im Allgemeinen ebenfalls nicht unterstützt. - Zustandsexplosion: Das Kompilieren eines komplexen Musters in einen DFA kann manchmal zu einer exponentiell großen Anzahl von Zuständen führen, die erheblichen Speicher verbrauchen.
- Eingeschränkte Funktionen: Die deterministische Natur von DFAs macht es unmöglich, Funktionen zu implementieren, die sich an eine vorherige Übereinstimmung erinnern müssen, wie z. B. Backreferences (z. B.
Nondeterministic Finite Automata (NFA): Der Pfad der Möglichkeiten
Stellen Sie sich nun eine andere Art von Maschine vor. Wenn sie ein Zeichen liest, hat sie möglicherweise mehrere mögliche nächste Zustände. Es ist, als ob die Maschine sich selbst klonen kann, um alle Pfade gleichzeitig zu erkunden. Eine NFA-Engine simuliert diesen Prozess, typischerweise indem sie jeweils einen Pfad ausprobiert und ein Backtracking durchführt, wenn er fehlschlägt. Dies ist ein NFA.
- Wie es funktioniert: Eine NFA-Engine durchläuft das Regex-Muster, und für jedes Token im Muster versucht sie, es mit der aktuellen Position in der Zeichenfolge abzugleichen. Wenn ein Token mehrere Möglichkeiten zulässt (wie die Alternation `|` oder ein Quantifizierer `*`), trifft die Engine eine Wahl und speichert die anderen Möglichkeiten für später. Wenn der gewählte Pfad keine vollständige Übereinstimmung ergibt, führt die Engine ein Backtracking zum letzten Wahlpunkt durch und versucht die nächste Alternative.
- Stärken:
- Leistungsstarke Funktionen: Dieses Modell unterstützt eine umfangreiche Funktionspalette, darunter Erfassungsgruppen, Backreferences, Lookaheads, Lookbehinds sowie Greedy- und Lazy Quantifiers.
- Ausdrucksstärke: NFA-Engines können eine größere Vielfalt komplexer Muster verarbeiten.
- Schwächen:
- Leistungsvariabilität: Im besten Fall sind NFA-Engines schnell. Im schlimmsten Fall kann der Backtracking-Mechanismus zu einer exponentiellen Zeitkomplexität O(2^n) führen, einem Phänomen, das als "katastrophales Backtracking" bekannt ist.
Das Herzstück des Python-Moduls `re`: Die Backtracking-NFA-Engine
Die Regex-Engine von Python ist ein klassisches Beispiel für einen Backtracking-NFA. Das Verständnis dieses Mechanismus ist das wichtigste Konzept zum Schreiben effizienter regulärer Ausdrücke in Python. Verwenden wir eine Analogie: Stellen Sie sich vor, Sie befinden sich in einem Labyrinth und haben eine Reihe von Anweisungen (das Muster). Sie folgen einem Pfad. Wenn Sie in einer Sackgasse landen, gehen Sie Ihre Schritte bis zur letzten Kreuzung zurück, an der Sie eine Wahl hatten, und versuchen einen anderen Pfad. Dieser "Zurückverfolgen und Wiederholen"-Prozess ist das Backtracking.
Ein Schritt-für-Schritt-Beispiel für Backtracking
Sehen wir uns an, wie die Engine ein scheinbar einfaches Muster verarbeitet. Dieses Beispiel demonstriert das Kernkonzept des Greedy Matching und Backtracking.
- Muster:
a.*b - Zeichenfolge:
axbyc_bzd
Das Ziel ist es, eine Teilzeichenfolge zu finden, die mit 'a' beginnt, mit 'b' endet und alles dazwischen hat.
- Die Engine beginnt mit dem ersten Zeichen der Zeichenfolge. Der erste Teil des Musters ist
a. Er entspricht dem 'a' am Anfang der Zeichenfolge. Die Position der Engine befindet sich nun hinter 'a'. - Als nächstes kommt
.*. Der*Quantifizierer ist standardmäßig greedy. Dies ist ein kritischer Punkt. Ein Greedy Quantifizierer findet so viele Zeichen wie möglich. Also verbraucht.*den gesamten Rest der Zeichenfolge: `xbyc_bzd`. - Die Engine hat nun das Ende des Musters
.*erreicht und versucht, das nächste Token abzugleichen:b. Sie befindet sich jedoch bereits am Ende der Zeichenfolge. Es sind keine Zeichen mehr zum Abgleichen vorhanden. Die Übereinstimmung schlägt an dieser Position fehl. - BACKTRACK! Die Engine muss ihre letzte Wahl überdenken. Die letzte wichtige Wahl war, wie viele Zeichen der Greedy
.*verbrauchen sollte. Er war zu gierig. Also führt er ein Backtracking durch:.*gibt ein Zeichen zurück.- Die von
.*verbrauchte Zeichenfolge ist nun `xbyc_bz`. - Das zurückgegebene Zeichen ist `d`.
- Die Engine versucht,
bmit `d` abzugleichen. Dies schlägt fehl.
- Die von
- NOCHMALS BACKTRACK! Die
.*gibt ein weiteres Zeichen zurück.- Die von
.*verbrauchte Zeichenfolge ist nun `xbyc_b`. - Das zurückgegebene Zeichen ist `z`.
- Die Engine versucht,
bmit `z` abzugleichen. Dies schlägt fehl.
- Die von
- NOCHMALS BACKTRACK! Die
.*gibt ein weiteres Zeichen zurück.- Die von
.*verbrauchte Zeichenfolge ist nun `xbyc_`. - Das zurückgegebene Zeichen ist `b`.
- Die Engine versucht,
bmit `b` abzugleichen. Erfolg!
- Die von
- Das gesamte Muster
a.*bwurde nun abgeglichen. Die endgültige Übereinstimmung istaxbyc_b.
Dieses einfache Beispiel zeigt die Trial-and-Error-Natur der Engine. Bei komplexen Mustern und langen Zeichenfolgen kann dieser Prozess des Verbrauchens und Zurückgebens Tausende oder sogar Millionen Mal erfolgen, was zu schwerwiegenden Leistungsproblemen führt.
Die Gefahr des Backtracking: Katastrophales Backtracking
Katastrophales Backtracking ist ein spezifisches Worst-Case-Szenario, in dem die Anzahl der Permutationen, die die Engine ausprobieren muss, exponentiell wächst. Dies kann dazu führen, dass sich ein Programm aufhängt und 100 % eines CPU-Kerns für Sekunden, Minuten oder sogar länger verbraucht, wodurch effektiv eine Regular Expression Denial of Service (ReDoS)-Schwachstelle entsteht.
Diese Situation entsteht typischerweise aus einem Muster, das verschachtelte Quantifizierer mit einem überlappenden Zeichensatz hat, das auf eine Zeichenfolge angewendet wird, die fast, aber nicht ganz, übereinstimmen kann.
Betrachten Sie das klassische pathologische Beispiel:
- Muster:
(a+)+z - Zeichenfolge:
aaaaaaaaaaaaaaaaaaaaaaaaaz(25 'a's und ein 'z')
Dies wird sehr schnell übereinstimmen. Das äußere `(a+)+` gleicht alle 'a's auf einmal ab, und dann gleicht `z` 'z' ab.
Betrachten Sie aber nun diese Zeichenfolge:
- Zeichenfolge:
aaaaaaaaaaaaaaaaaaaaaaaaab(25 'a's und ein 'b')
Hier ist, warum dies katastrophal ist:
- Das innere
a+kann ein oder mehrere 'a's abgleichen. - Der äußere
+Quantifizierer besagt, dass die Gruppe(a+)einmal oder mehrmals wiederholt werden kann. - Um die Zeichenfolge aus 25 'a's abzugleichen, hat die Engine viele, viele Möglichkeiten, sie zu partitionieren. Zum Beispiel:
- Die äußere Gruppe gleicht einmal ab, wobei das innere
a+alle 25 'a's abgleicht. - Die äußere Gruppe gleicht zweimal ab, wobei das innere
a+zuerst 1 'a' und dann 24 'a's abgleicht. - Oder 2 'a's dann 23 'a's.
- Oder die äußere Gruppe gleicht 25 Mal ab, wobei das innere
a+jedes Mal ein 'a' abgleicht.
- Die äußere Gruppe gleicht einmal ab, wobei das innere
Die Engine wird zuerst die gierigste Übereinstimmung ausprobieren: Die äußere Gruppe gleicht einmal ab, und das innere `a+` verbraucht alle 25 'a's. Dann versucht sie, `z` mit `b` abzugleichen. Sie schlägt fehl. Also führt sie ein Backtracking durch. Sie probiert die nächste mögliche Partition der 'a's aus. Und die nächste. Und die nächste. Die Anzahl der Möglichkeiten, eine Zeichenfolge aus 'a's zu partitionieren, ist exponentiell. Die Engine ist gezwungen, jede einzelne auszuprobieren, bevor sie zu dem Schluss kommen kann, dass die Zeichenfolge nicht übereinstimmt. Mit nur 25 'a's kann dies Millionen von Schritten dauern.
So identifizieren und verhindern Sie katastrophales Backtracking
Der Schlüssel zum Schreiben effizienter Regex besteht darin, die Engine zu führen und die Anzahl der Backtracking-Schritte zu reduzieren, die sie ausführen muss.
1. Vermeiden Sie verschachtelte Quantifizierer mit überlappenden Mustern
Die Hauptursache für katastrophales Backtracking ist ein Muster wie (a*)*, (a+|b+)* oder (a+)+. Überprüfen Sie Ihre Muster auf diese Struktur. Oft kann sie vereinfacht werden. Zum Beispiel ist (a+)+ funktional identisch mit dem viel sichereren a+. Das Muster (a|b)+ ist viel sicherer als (a+|b+)*.
2. Machen Sie Greedy Quantifiers Lazy (Nicht-Greedy)
Standardmäßig sind Quantifizierer (`*`, `+`, `{m,n}`) greedy. Sie können sie lazy machen, indem Sie ein `?` hinzufügen. Ein Lazy Quantifier gleicht so wenige Zeichen wie möglich ab und erweitert seine Übereinstimmung nur, wenn dies für den Erfolg des restlichen Musters erforderlich ist.
- Greedy:
<h1>.*</h1>auf der Zeichenfolge"<h1>Title 1</h1> <h1>Title 2</h1>"gleicht die gesamte Zeichenfolge vom ersten<h1>bis zum letzten</h1>ab. - Lazy:
<h1>.*?</h1>auf derselben Zeichenfolge gleicht zuerst"<h1>Title 1</h1>"ab. Dies ist oft das gewünschte Verhalten und kann das Backtracking erheblich reduzieren.
3. Verwenden Sie Possessive Quantifiers und Atomic Groups (wenn möglich)
Einige fortschrittliche Regex-Engines bieten Funktionen, die Backtracking explizit verbieten. Während das Standardmodul `re` von Python diese nicht unterstützt, tut dies das hervorragende Drittanbietermodul `regex`, und es ist ein lohnendes Werkzeug für komplexe Mustererkennung.
- Possessive Quantifiers (`*+`, `++`, `?+`): Diese sind wie Greedy Quantifiers, aber sobald sie übereinstimmen, geben sie niemals Zeichen zurück. Die Engine darf nicht in sie zurückverfolgen. Das Muster
(a++)+zwürde auf unserer problematischen Zeichenfolge fast sofort fehlschlagen, da `a++` alle 'a's verbrauchen und sich dann weigern würde, zurückzuverfolgen, wodurch die gesamte Übereinstimmung sofort fehlschlagen würde. - Atomic Groups `(?>...)`:** Eine Atomic Group ist eine nicht erfassende Gruppe, die, sobald sie verlassen wurde, alle Backtracking-Positionen innerhalb dieser Gruppe verwirft. Die Engine kann nicht in die Gruppe zurückverfolgen, um verschiedene Permutationen auszuprobieren. `(?>a+)z` verhält sich ähnlich wie `a++z`.
Wenn Sie in Python mit komplexen Regex-Herausforderungen konfrontiert sind, wird die Installation und Verwendung des Moduls `regex` anstelle von `re` dringend empfohlen.
Einblick: So kompiliert Python Regex-Muster
Wenn Sie einen regulären Ausdruck in Python verwenden, arbeitet die Engine nicht direkt mit der rohen Musterzeichenfolge. Sie führt zuerst einen Kompilierungsschritt durch, der das Muster in eine effizientere, Low-Level-Darstellung umwandelt – eine Sequenz von Bytecode-ähnlichen Anweisungen.
Dieser Prozess wird vom internen Modul `sre_compile` verwaltet. Die Schritte sind ungefähr:
- Parsing: Das String-Muster wird in eine baumartige Datenstruktur geparst, die seine logischen Komponenten (Literale, Quantifizierer, Gruppen usw.) darstellt.
- Kompilierung: Dieser Baum wird dann durchlaufen, und eine lineare Sequenz von Opcodes wird generiert. Jeder Opcode ist eine einfache Anweisung für die Matching-Engine, z. B. "gleiche dieses Literalzeichen ab", "springe zu dieser Position" oder "starte eine Erfassungsgruppe".
- Ausführung: Die virtuelle Maschine der `sre`-Engine führt dann diese Opcodes gegen die Eingabezeichenfolge aus.
Sie können einen Blick auf diese kompilierte Darstellung werfen, indem Sie das Flag `re.DEBUG` verwenden. Dies ist eine leistungsstarke Möglichkeit, zu verstehen, wie die Engine Ihr Muster interpretiert.
import re
# Analysieren wir das Muster 'a(b|c)+d'
re.compile('a(b|c)+d', re.DEBUG)
Die Ausgabe sieht in etwa so aus (Kommentare zur Verdeutlichung hinzugefügt):
LITERAL 97 # Gleiche das Zeichen 'a' ab
MAX_REPEAT 1 65535 # Starte einen Quantifizierer: Gleiche die folgende Gruppe 1 bis viele Male ab
SUBPATTERN 1 0 0 # Starte die Erfassungsgruppe 1
BRANCH # Starte eine Alternation (das '|'-Zeichen)
LITERAL 98 # Im ersten Zweig, gleiche 'b' ab
OR
LITERAL 99 # Im zweiten Zweig, gleiche 'c' ab
MARK 1 # Beende die Erfassungsgruppe 1
LITERAL 100 # Gleiche das Zeichen 'd' ab
SUCCESS # Das gesamte Muster wurde erfolgreich abgeglichen
Das Studium dieser Ausgabe zeigt Ihnen die exakte Low-Level-Logik, der die Engine folgen wird. Sie können den `BRANCH`-Opcode für die Alternation und den `MAX_REPEAT`-Opcode für den `+`-Quantifizierer sehen. Dies bestätigt, dass die Engine Entscheidungen und Schleifen sieht, die die Zutaten für Backtracking sind.
Praktische Auswirkungen auf die Leistung und Best Practices
Mit diesem Verständnis der Interna der Engine können wir eine Reihe von Best Practices für das Schreiben von leistungsstarken regulären Ausdrücken festlegen, die in jedem globalen Softwareprojekt effektiv sind.
Best Practices für das Schreiben effizienter regulärer Ausdrücke
- 1. Vorkompilieren Sie Ihre Muster: Wenn Sie dieselbe Regex mehrmals in Ihrem Code verwenden, kompilieren Sie sie einmal mit
re.compile()und verwenden Sie das resultierende Objekt wieder. Dies vermeidet den Overhead des Parsens und Kompilierens der Musterzeichenfolge bei jeder Verwendung.# Gute Praxis COMPILED_REGEX = re.compile(r'\d{4}-\d{2}-\d{2}') for line in data: COMPILED_REGEX.search(line) - 2. Seien Sie so spezifisch wie möglich: Ein spezifischeres Muster gibt der Engine weniger Auswahlmöglichkeiten und reduziert die Notwendigkeit, ein Backtracking durchzuführen. Vermeiden Sie übermäßig generische Muster wie `.*`, wenn ein präziseres Muster ausreicht.
- Weniger effizient: `key=.*`
- Effizienter: `key=[^;]+` (gleiche alles ab, was kein Semikolon ist)
- 3. Verankern Sie Ihre Muster: Wenn Sie wissen, dass sich Ihre Übereinstimmung am Anfang oder Ende einer Zeichenfolge befinden sollte, verwenden Sie die Anker `^` bzw. `$`. Dies ermöglicht es der Engine, bei Zeichenfolgen, die nicht an der erforderlichen Position übereinstimmen, sehr schnell zu scheitern.
- 4. Verwenden Sie nicht erfassende Gruppen `(?:...)`: Wenn Sie einen Teil eines Musters für einen Quantifizierer gruppieren müssen, aber den übereinstimmenden Text aus dieser Gruppe nicht abrufen müssen, verwenden Sie eine nicht erfassende Gruppe. Dies ist etwas effizienter, da die Engine keinen Speicher zuweisen und die erfasste Teilzeichenfolge speichern muss.
- Erfassen: `(https?|ftp)://...`
- Nicht erfassen: `(?:https?|ftp)://...`
- 5. Bevorzugen Sie Zeichenklassen gegenüber Alternation: Wenn Sie eines von mehreren einzelnen Zeichen abgleichen, ist eine Zeichenklasse `[...]` deutlich effizienter als eine Alternation `(...)`. Die Zeichenklasse ist ein einzelner Opcode, während die Alternation Verzweigungen und komplexere Logik beinhaltet.
- Weniger effizient: `(a|b|c|d)`
- Effizienter: `[abcd]`
- 6. Wissen Sie, wann Sie ein anderes Tool verwenden sollten: Reguläre Ausdrücke sind leistungsstark, aber sie sind nicht die Lösung für jedes Problem. Verwenden Sie für einfache Teilzeichenfolgenprüfungen `in` oder `str.startswith()`. Verwenden Sie zum Parsen strukturierter Formate wie HTML oder XML eine dedizierte Parserbibliothek. Die Verwendung von Regex für diese Aufgaben ist oft brüchig und ineffizient.
Fazit: Von der Black Box zu einem leistungsstarken Werkzeug
Die Regex-Engine von Python ist ein fein abgestimmtes Softwarestück, das auf Jahrzehnten der Informatiktheorie basiert. Durch die Wahl eines Backtracking-NFA-basierten Ansatzes bietet Python Entwicklern eine umfangreiche und ausdrucksstarke Mustererkennungssprache. Diese Leistung geht jedoch mit der Verantwortung einher, die zugrunde liegende Mechanik zu verstehen.
Sie sind jetzt mit dem Wissen ausgestattet, wie die Engine funktioniert. Sie verstehen den Trial-and-Error-Prozess des Backtracking, die immense Gefahr seines katastrophalen Worst-Case-Szenarios und die praktischen Techniken, um die Engine zu einer effizienten Übereinstimmung zu führen. Sie können jetzt ein Muster wie (a+)+ betrachten und sofort das Performance-Risiko erkennen, das es birgt. Sie können selbstbewusst zwischen einem Greedy .* und einem Lazy .*? wählen und genau wissen, wie sich jedes verhalten wird.
Wenn Sie das nächste Mal einen regulären Ausdruck schreiben, denken Sie nicht nur darüber nach, was Sie abgleichen möchten. Denken Sie darüber nach, wie die Engine dorthin gelangen wird. Indem Sie sich über die Black Box hinausbewegen, erschließen Sie das volle Potenzial regulärer Ausdrücke und verwandeln sie in ein vorhersehbares, effizientes und zuverlässiges Werkzeug in Ihrem Entwickler-Toolkit.