RegEx Advanced Guide

    • C#
    • .NET (FX) 3.0–3.5

    Es gibt 7 Antworten in diesem Thema. Der letzte Beitrag () ist von nafets.

      RegEx Advanced Guide

      Hi,

      ich möchte mit diesem Tutorial einen Einblick in die fortgeschrittenen Techniken von RegEx geben und denen, die bereits
      grundlegende RegEx-Kenntnisse haben, mehr Möglichkeiten eröffnen.

      Dieses Tutorial setzt das vorhandene Tutorial von @Link vorraus, dies ist hier zu finden:
      RegEx Tutorial - Blutige Anfänger und Fortgeschrittene
      Es ist möglich, dass sich beide Tutorials an manchen Stellen überschneiden, da ich nicht das gesamte Tutorial von Link mit
      meinem abgleichen werde. Sollte das passieren, dann sind die behandelten Inhalte hier trotzdem gut aufgehoben :)

      Dieses Tutorial arbeitet aktuell abstrakt - es gibt noch keinen direkten .NET Code, dieser folgt aber mit Sicherheit noch.
      Die Interessierten werden sich trotzdem zurecht finden.

      Los gehts!


      Übersicht
      • Grundlagen
      • Wie funktioniert RegEx?
      • Unterschiede von RegEx in verschiedenen Programmiersprachen
      • RegEx Advanced Patterns
      • Code-Snippets
      • Nützliche Tools zur Verwendung von RegEx


      Grundlagen

      Dieses Tutorial setzt gewisse Kenntnisse und Grundlagen vorraus, wie in der Einleitung beschrieben. Man sollte beispielsweise wissen was ein Pattern ist, was ein Token
      ist und wie man RegEx im .NET Framework verwendet. Ebenfalls wichtig ist es, die Grundlagen von RegEx zu kennen und anwenden zu können. Dass ein . jeden Character
      außer NewLine matcht sollte genauso klar sein wie die Nutzung von Gruppen und Quantifiern. Alle weiteren Grundkenntnisse von RegEx würde ich empfehlen, aber auch mit
      dem o.g. Wissen sollte man sich in diesem Tutorial noch zurecht finden.


      Wie funktioniert RegEx?

      Erstmal möchte ich mich mit der Funktionsweise von RegEx befassen, denn die kann oft helfen, das eigene Pattern besser zu verstehen.
      Die RegEx-Engine setzt 2 Cursors - ich nenne sie hier jetzt mal Cursors - einen an den Anfang des Patterns und einen an den Anfang des zu überprüfenden Strings.
      Man kann sich diese Cursors wie die blinkende Linie in einem Textfeld vorstellen.

      Nun beginnt RegEx damit, die Inhalte zu vergleichen. Es wird versucht, den ersten Character im String mit dem ersten Token des Patterns abzugleichen.
      Natürlich gibt es hier noch Quantifier, so wie + oder *, die überspringe ich hier der Einfachheit halber aber mal. Wenn der erste Character zum ersten Token
      im Pattern passt, dann springt der Cursor im Pattern und im Inhalt eins weiter, und versucht wieder, den Character nach dem Cursor mit dem Token nach dem Cursor im Pattern
      abzugleichen. Schlägt dieser Test nun fehl, dann springt der Cursor im Inhalt wieder eine Stelle nach vorn, der Cursor im Pattern setzt sich jedoch zurück und beginnt wieder von
      vorn. Anschaulich gestaltet:

      String: ac abc
      Pattern: a.c


      Schritt 1:

      Der Cursor sitzt sowohl im Pattern als auch im String auf Position 0, daher vor jeglichem Inhalt.

      String: |ac abc
      Pattern: |a.c


      Schritt 2:

      Die Engine versucht, den ersten Character im String mit dem ersten Token im Pattern abzugleichen. Dieser Test ist erfolgreich: Das erste Token im Pattern ist der Character a,
      der erste Character im String ist ebenfalls ein a. Beide Cursor springen in der Position eins nach vorn.

      String: a|c abc
      Pattern: a|.c


      Schritt 3:

      Die Engine versucht erneut, den nächsten Character im String mit dem nächsten Token im Pattern abzugleichen. Wieder ist der Test erfolgreich: Das folgende Token . steht für
      alle Character außer NewLine, der folgende Character ist ein c, beides stimmt also überein, die Position des Cursors sprint wieder eins nach vorn.

      String: ac| abc
      Pattern: a.|c

      Schritt 4:

      Die Engine versucht beide Schritte erneut. Diesmal scheitert der Abgleich allerdings, wenn das folgende Token c, welches den Character c genau matcht, versucht, den im String
      auf das c folgende WhiteSpace zu matchen. Der Cursor im Pattern springt wieder auf Position 0, der Cursor im String wiederum eins weiter.

      String: ac |abc
      Pattern: |a.c


      Weitere Schritte:

      Der Rest sollte klar sein: Die Engine matcht wieder Token für Token und findet die darauffolgende Zeichenkette "abc", weil sie dem Pattern a.c entspricht.


      Unterschiede von RegEx in verschiedenen Programmiersprachen

      Ein weiteres Thema, das oft Probleme bereitet, ist die Unterstützung von RegEx in verschiedenen Programmiersprachen. Die Ursache hierfür sind die verschiedenen RegEx-Engines, die jeweils
      eben auch unterschiedliche Funktionalität bereitstellen. (siehe en.wikipedia.org/wiki/Comparis…egular_expression_engines)
      Folglich kann ein und das selbe RegEx-Pattern in unterschiedlichen Programmiersprachen zu unterschiedlichen Ergebnissen führen. Auf die Gefahr hin, nicht völlig aktuelle Informationen zu verbreiten,
      ziehe ich als Beispiel "possessive Quantifiers" heran, die meines Wissens nach von .NET nicht untersützt werden.

      Abhilfe schaffen hier diverse Online-Tools, die beim Konvertieren helfen oder zumindest das Testen erleichtern, sodass man erkennt, welche Unterschiede das Pattern
      in verschiedenen Programmiersprachen aufweist.


      Greedy, Lazy, Backtracking, Posessive

      Bevor ich zu den Advanced Patterns komme ist es wichtig, erst einmal zu verstehen was Quantifiers sind und wie sie funktionieren.
      Grundsätzlich gibt es drei Varianten von Quantifiern, welche ich erst einmal erkläre. @'Link''s Tutorial beschreibt das Token .*? als
      "bis zum nächsten Vorkommen von", was so nicht völlig korrekt ist, bzw. einen falschen Eindruck vermitteln kann. Für Anfänger ist das auch eine sehr gute Beschreibung, ich möchte
      das Ganze hier allerdings näher erläutern.


      Greedy:

      Greedy ist das Standardverhalten eines Quantifiers. Greedy heißt, dass der vorangehende Quantifier, also beispielsweise ein *, so viele Vorkommen matcht wie irgend möglich,
      sodass das Pattern ein Match ergibt. Nehmen wir also ein einfaches Beispiel:

      String: Hallo
      Pattern: .*

      Das Pattern beschreibt ein beliebiges Zeichen, welches 0 oder mehrmals wiederholt wird. Der *-Quantifier ist standardmäßig greedy, was dazu führt, dass das gesamte Wort "Hallo"
      als Match des Patterns erkannt wird, denn wie oben erwähnt bewirkt ein "Greedy"-Quantifier, dass so viele Vorkommen wie möglich gematcht werden.


      Lazy:

      Lazy ist das genaue Gegenteil zum oben genannten "Greedy"-Quantifier. Es wird durch ein ? gekennzeichnet. Um dies direkt zu verdeutlichen ziehen wir das selbe Beispiel erneut heran, verwenden diesmal aber ein ?,
      um dem *-Quantifier mitzuteilen, dass er doch bitte "lazy" vorgehe. Trotz aller Erwartungen möchte ich hier anmerken, dass "Lazy" aus dem Englischen kommt und "faul" bedeutet, was auf
      das Ergebnis des folgenden Tests schließen lässt:

      String: Hallo
      Pattern: .*?

      Was wird nun also gematcht? Erstmal garnichts. Durch die Spezifizierung des Quantifiers geht dieser nun "lazy" vor, das heißt, er matcht so wenig wie möglich, achtet aber trotzdem
      darauf, dass das Pattern gematcht werden kann
      . Hierdurch entsteht auch die Darstellung in @'Link''s Tutorial, "bis zum nächsten Vorkommen von", denn nehmen wir nun

      Pattern: .*?o,

      so wird das gesamte Wort "Hallo" gematcht. Die Engine hat bemerkt, dass ein Match zustande kommt, wenn die ersten vier Buchstaben vom "Lazy"-Quantifier gematcht werden und somit
      das Wort gematcht. Lazy matcht also so wenige Vorkommen wie möglich, beachtet aber das Pattern.


      Backtracking:

      Ein nicht weniger wichtiger Bestandteil von RegEx ist das Backtracking, was die Grundlage für "Posessive"-Quantifier bildet (folgen gleich).
      Backtracking heißt, dass, wie bereits oben bei "Lazy" angedeutet, die Engine auf das Pattern an sich achtet. Man betrachte folgendes Beispiel:

      String: Hallo
      Pattern: .+o

      Ich bitte dich - den Leser - einmal direkt darüber nachzudenken was die Engine in diesem Fall machen würde, und ob ein Match vorliegt oder nicht.
      Wenn eine Meinung herrscht, dann kommt hier das Ergebnis:
      Es existiert ein Match. Wenn jemand aber tatsächlich darüber nachgedacht hat, dann wird demjenigen aufgefallen sein, dass das laut obiger Definition
      doch gar nicht passieren darf, denn der +-Quantifier ist doch standardmäßig "Greedy", und matcht so viele Zeichen wie möglich. Somit müsste er doch
      bis ans Ende matchen, um danach kein passendes "o" mehr zu finden, was eigentlich darin resultieren sollte, dass RegEx kein Match findet. Warum findet RegEx
      also ein Match?

      Des Rätsels Lösung nennt sich Backtracking und ist wesentlicher Bestandteil von RegEx. In der Tat matcht RegEx durch den "Greedy"-Quantifier bis zum Ende durch
      und findet dann kein "o" mehr. Allerdings ist es ebenfalls intelligent genug zu bemerken, dass, wenn man ein Zeichen weniger durch das .+ gematcht hätte,
      ein Match gefunden worden wäre. Die Engine springt also einen Character zurück und matcht dann noch das "o". Dieses Vehalten nennt sich Backtracking, und wird hier gebraucht:


      Posessive:

      Posessive Quantifiers stellen sich über das o.g. Backtracking und "verbieten" es. Das heißt, ist etwas einmal gematcht, dann war es das vorerst, egal was dem Token noch folgt. Ein posessive
      Quantifier wird durch ein auf den Quantifier folgendes + gekennzeichnet (nicht zu verwechseln mit dem eigentlichen Quantifier +, der ist standardmäßig immernoch greedy). Ein Beispiel:

      String: Hallo
      Pattern: .++o

      Es fällt auf: Wir haben 2 Mal das +-Token verwendet? Warum? Das erste von beiden ist der normale Quantifier, welcher besagt, dass ein oder mehr Vorkommen des vorrangehenden Tokens gematcht
      werden sollen. Das zweite bezieht sich auf diesen Quantifier und macht ihn "posessive" - es verhindert Backtracking. Lässt obige Kombination nun also ein Match zu oder nicht? Nein.
      Das Beispiel entspricht exakt dem der Backtracking-Section, wir haben nur ein einzelnes + hinzugefügt - die Engine kann nicht so vorgehen wie oben und findet das "o" nicht - kein Match.
      Anzumerken ist, dass Posessive Quantifiers von .NET nicht unterstützt werden.


      Zusammenfassend:

      Greedy ist das Standardverhalten eines Quantifiers, matcht so viele Vorkommen wie möglich und beachtet das Pattern (Backtracking).
      Lazy matcht so wenige Vorkommen wie möglich und beachtet ebenfalls das Pattern (Backtracking), wird mit ? verwendet.
      Posessive matcht so viele Vorkommen wie möglich, ignoriert dabei das Pattern (kein Backtracking), wird mit + verwendet.
      Backtracking nennt man das Verhalten von RegEx, bei einem kein Match erzeugenden Pattern im String zurückzugehen um ein Match zu finden.


      RegEx Advanced Patterns

      In diesem Abschnitt möchte ich gezielt auf Patterns und Tokens eingehen, die man - gerade als RegEx-Anfänger - nicht zwingend kennt, die aber doch ganz nützlich sein können.
      Dies sind keine ganzen Code-Snippets, mehr wichtige Grundbestandteile von RegEx. Ich werde die einzelnen Patterns auflisten und kurz erklären, ab und an möglicherweise an Beispielen,
      ich möchte aktuell allerdings (noch) nicht auf jedes davon spezifisch eingehen. Das kommt dann vielleicht noch ;)

      (?:Hallo) - Non-Capture-Group: Wird nicht als Capture-Group gewertet.
      (?=Hallo) - LookAhead: Prüft, ob die nachfolgende Zeichenfolge dem Pattern entspricht.
      (?<=Hallo) - LookBehind: Prüft, ob die vorrangehende Zeichenfolge dem Pattern entspricht.
      (?!Hallo) - Negativer LookAhead: Prüft, ob die nachfolgende Zeichenfolge nicht dem Pattern entspricht.
      (?<!Hallo) - Negativer LookBehind: Prüft, ob die vorrangehende Zeichenfolge nicht dem Pattern entspricht.
      (?>A+) - Atomic Group: Wenn die Engine die schließende Klammer verlassen hat ist ein Backtrack hier nicht mehr möglich.
      (?<named>Hallo) - Named Group: Kann in den Groups per Name abgerufen werden.
      (.{2})\s\1) - Backreference: \GroupNumber matcht den Inhalt, der von der Gruppe mit der Nummer 1 gecaptured wurde, hier zum Beispiel "Hi Hi", nicht jedoch "Hi Yo".
      (?(condition)then|else) - Condition: If-Then-Else-Konstrukt, Verwendung meist im Zusammenhang mit anderen Gruppen (Gruppe 1 gesetzt? (?(1)then)). Else ist optional.
      (?# Hi) - Comments: Inline-Kommentare im RegEx.


      Code Snippets

      - folgen


      Nützliche Tools zur Verwendung von RegEx

      Hier noch ein paar Tools, deren Verwendung ich jedem nur ans Herz legen kann. Grundsätzlich ist es für RegEx wichtig, einen RegEx-Tester zu haben. Der beste nicht-.NET-Tester
      ist meiner Meinung nach eindeutig regex101.com, der beste Online-.NET-Tester regexstorm.net/tester.
      Ebenfalls wichtig ist die Verwendung von Online-Resourcen zum nachstöbern, teils also CheatSheets.


      Was folgt
      • Cheat Sheets
      • Beispiel-Projekt
      • Code-Snippets
      • Weitere Advanced Patterns
      • Genauere Unterscheidungen zwischen .NET und anderen Sprachen
      • Tipps
      • .NET Code


      Auch wenn dieses Tutorial noch nicht komplett vollständig ist hoffe ich, dass ich einen kleinen Einblick in die tieferen Ebenen von
      RegEx geben konnte. Ich werde dieses Tutorial in den nächsten Wochen Stück für Stück erweitern und es hoffentlich zu einer vertrauenswürdigen
      und informativen Quelle für "Advanced RegEx" machen können.

      Konstruktive Kritik ist immer erwünscht.

      Grüße,
      Nikx
      "Life isn't about winning the race. Life is about finishing the race and how many people we can help finish the race." ~Marc Mero

      Nun bin ich also auch soweit: Keine VB-Fragen per PM! Es gibt hier ein Forum, verdammt!

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „Nikx“ ()

      Hi,

      nettes Tutorial. Wenn du es eh erweitern willst, empfehl ich dir, dass du dem Thema Backreferences einen eigenen Bereich im Tutorial widmest, und wie man beim Ersetzen auf die Gruppen zugreifen kann (numerisch oder assoziativ). Beispiele dazu wären dann natürlich auch super (z.B. \b(.)[a-z]+\1\b matcht alle Wörter die mit dem gleichen Buchstaben enden mit dem sie beginnen und so Zeugs......)

      Einen winzigen Fehler hab ich noch entdeckt:
      - Pattern: .?*o, solltest du umschreiben in .*?o. Schließlich is der vorangehende Token nicht quantifizierbar.

      Ja der Satz "bis zum nächsten Vorkommen von" wurde schon mehrmals bemängelt, damals ist mir nix besseres eingefallen wie ich es ohne die Spezialbegriffe hätte beschreiben können^^

      Was den Inhalt angeht hätte ich hier zwar äußerst mehr und auch tiefergehende Erläuterungen und Techniken gewünscht, aber ansonsten nice :) Bis dahin müssen wir halt warten bis der Rest folgt.


      Link :thumbup:
      Hello World
      Hi,
      danke!

      Ich kann gerne noch mal spezifischer auf Backreferencing eingehen, da gibt es sicher genug zu schreiben ;)
      Da ich sowieso grade am .NET Projekt bin bietet sich das baldige Einfügen von Code sowieso an, da würde ich das dann mitmachen.
      Den Fehler hab ich ausgebessert, ist mir nicht aufgefallen :)

      Was genau hättest du dir denn an tiefergehenden Erläuterungen gewünscht?

      Grüße
      "Life isn't about winning the race. Life is about finishing the race and how many people we can help finish the race." ~Marc Mero

      Nun bin ich also auch soweit: Keine VB-Fragen per PM! Es gibt hier ein Forum, verdammt!
      In dem Pattern für das If-Else-Konstrukt sind noch ein ​[i] und ein ​[/i] reingerutscht - ich glaube, die gehören da nicht hin ;)

      Ich halte es übrigens für sehr schwierig, solch ein umfangreiches Thema in einem Post hier umfangreich zu erklären. Wäre es nicht besser, sowas in Form einer Website oä zu machen, wo man dann unterschiedliche (Unter-) Seiten und Links erstellen kann? Sowas in der Art von der MSDN, halt nur für Regular Expressions. Man könnte dann auch sehr interessante Animationen zur Verfügung stellen, welche darstellen, wie Regex Engines arbeiten :)

      Auf jeden Fall habe ich noch ein paar Ideen, was man hinzufügen könnte:
      • Eine Übersicht über die wichtigen Character Escapes
      • Eine Übersicht über Character Classes (speziell Unicode Categories)
      • Atomic Zero-Width Assertions (die Bekannten sind ​^ und ​$, es gibt aber auch noch ​\G, ​\b usw.)
      • An- und Ausschalten von Regex-Options innerhalb von Patterns (bspw. mit ​(?s:pattern) oder ​(?s))
      • Die Ersetzungs-patterns


      Grüße
      Stefan
      @nafets generell geb ich dir recht. Was aber irgendwo dagegen spricht ist die Tatsache, dass es zu dem Thema bereits etliche Webseiten gibt. Ich finde man sollte sich eher drauf konzentrieren, alle Aspekte von RegEx in Verbindung mit .NET-Sprachen zu beleuchten - da gibt es nicht so viel an Material.
      Das mit dem An-/Ausschalten von RegEx-Options (Modifiern) finde ich ne gute Idee. Geil wär, wenn sich jemand Zeit nimmt, und mal eine RegEx-Klasse schreibt, die es erlaubt, einen Pattern inkl. Modifiern in der Form /[pattern].+whatever/gi zu übergeben -wie man es aus vielen anderen Sprachen kennt. Ich kann mir vorstellen dass das vielen auch irgendwo leichter fällt, mehrere Modifier zu verwenden. So schwierig wär das ja nicht, nur ich selber hab momentan echt nicht die Zeit dafür - leider.

      Link :thumbup:
      Hello World
      @Link
      Ich meinte auch, dass die Website sich mit .NET-Regex auseinandersetzen sollte - quasi eine übersichtliche und umfassende Dokumentation von allen Regex-Features im .NET Framework mit Beispielen, Erklärungen usw.

      Wo wäre denn der große Unterschied, wenn man Regex-Options direkt an das Pattern dranhängt, anstatt sie schön OOP-gemäß zu übergeben?
      Was ich weitaus interessanter fände wäre die Möglichkeit, rekursive Patterns zu erstellen. Also Patterns, die dann einen ganzen Baum herausgeben würden - nicht nur eine simple Liste aller Capture Groups. Ich hatte mich vor einer Weile mal daran versucht - bin aber schlussendlich daran gescheitert, eine gute Syntax zu erstellen. Passende Anwendungsbereiche wären bspw. das Parsen von Dateiformaten wie XML oder JSON - man könnte dann ein ganzes Dateiformat mit nur einem einzigen Pattern parsen.

      Grüße
      Stefan

      Nachtrag:
      Habe gerade etwas über Balancing Groups gelesen - habt ihr die schonmal genutzt? Die würden wohl auch Rekursive Gruppen ersetzen können, wenn ich das richtig verstanden habe.

      Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „nafets“ ()

      Hi,

      @nafets
      Wo wäre denn der große Unterschied, wenn man Regex-Options direkt an das Pattern dranhängt, anstatt sie schön OOP-gemäß zu übergeben?

      Naja ich fänd's halt geil^^ Und übergeben wird dann ja auch wieder wie gewohnt, nur wird das anhand dessen gemacht, was am Ende nach / kommt (oder welches Zeichen man halt nimmt). So wird dann der Pattern /\w+/mi zum Pattern \w+ und die RegEx-Options .IgnoreCase und .MultiLine. Mir gefällt einfach die Schreibweise äußerst gut, zumal es in den meisten Sprachen ja auch so gemacht wird. Aber VB ist halt einfach neben der Spur ^^

      Ja rekursive pattern sind ne coole Sache. regex101.com/r/tZ5mA0/1
      Könnte man auch ins Tutorial aufnehmen, wär sicher für viele interessant wie das geht.


      Link :thumbup:
      Hello World
      @Link
      Ich habs jetzt nicht getestet, so sollte es jedoch möglich sein, ein Pattern mit Regex-Optionen zu erstellen:

      C#-Quellcode

      1. public string MakeDotNetPattern(string pattern)
      2. {
      3. return Regex.Replace(pattern, @"\A/(?<pattern>.*)/(?<options>[imnsx]*)\z", "(?${options})${pattern}");
      4. }

      Und seit wann sind rekursive Regex-Patterns in .NET möglich? Ich dachte immer, dass .NET das nicht unterstützt?