Freigeben über


Bewährte Methoden für reguläre Ausdrücke in .NET

Die Engine für reguläre Ausdrücke in .NET ist ein leistungsstarkes Tool mit vollem Funktionsumfang, das Texte auf Grundlage von Musterübereinstimmungen verarbeitet, anstatt Literaltext zu vergleichen und nach Übereinstimmungen mit diesem zu suchen. In den meisten Fällen führt sie musterabgleiche schnell und effizient durch. Gelegentlich kann die Engine für reguläre Ausdrücke jedoch langsam wirken. In extremen Fällen kann es sogar vorkommen, dass sie nicht mehr reagiert, da sie eine relativ geringe Eingabe im Laufe der Stunden oder sogar Tage verarbeitet.

In diesem Artikel werden einige der bewährten Methoden beschrieben, die Entwickler einführen können, um sicherzustellen, dass ihre regulären Ausdrücke eine optimale Leistung erzielen.

Warnung

Wenn Sie System.Text.RegularExpressions verwenden, um nicht vertrauenswürdige Eingaben zu verarbeiten, übergeben Sie ein Timeout. Ein böswilliger Benutzer kann Eingaben für RegularExpressionsbereitstellen, was zu einem Denial-of-Service-Angriffführt. ASP.NET Core-Framework-APIs, die RegularExpressions verwenden, übergeben ein Timeout.

Berücksichtigen Sie die Eingabequelle.

Im Allgemeinen können reguläre Ausdrücke zwei Arten von Eingaben akzeptieren: eingeschränkt oder nicht eingeschränkt. Die eingeschränkte Eingabe ist ein Text, der aus einer bekannten oder zuverlässigen Quelle stammt und einem vordefinierten Format folgt. Nicht eingeschränkte Eingaben sind Text, der aus einer unzuverlässigen Quelle stammt, z. B. einem Webbenutzer, und möglicherweise kein vordefiniertes oder erwartetes Format befolgt.

Muster für reguläre Ausdrücke werden häufig so geschrieben, dass sie mit gültigen Eingaben übereinstimmen. Das heißt, Entwickler untersuchen den Text, den sie abgleichen möchten, und schreiben dann ein Muster für reguläre Ausdrücke, das dem Text entspricht. Entwickler bestimmen dann, ob dieses Muster Korrektur oder weitere Ausarbeitung erfordert, indem Sie es mit mehreren gültigen Eingabeelementen testen. Wenn das Muster mit allen angenommenen gültigen Eingaben übereinstimmt, wird es als produktionsbereit deklariert und kann in eine freigegebene Anwendung aufgenommen werden. Durch diesen Ansatz eignet sich das Muster des regulären Ausdrucks für den Abgleich mit eingeschränkten Eingaben. Es eignet sich jedoch nicht für den Abgleich unbeschränkter Eingaben.

Um nicht eingeschränkte Eingaben abzugleichen, muss ein regulärer Ausdruck drei Arten von Text effizient verarbeiten:

  • Text, der dem Muster für reguläre Ausdrücke entspricht.
  • Text, der nicht mit dem Muster für reguläre Ausdrücke übereinstimmt.
  • Text, der fast mit dem Muster für reguläre Ausdrücke übereinstimmt.

Der letzte Texttyp ist besonders problematisch für einen regulären Ausdruck, der geschrieben wurde, um eingeschränkte Eingaben zu verarbeiten. Wenn ein solcher regulärer Ausdruck zudem auf umfangreicher Rückverfolgung beruht, kann die Engine für reguläre Ausdrücke für die Verarbeitung von scheinbar harmlosem Text übermäßig lange Zeit brauchen (in manchen Fällen mehrere Stunden oder Tage).

Warnung

Im folgenden Beispiel wird ein regulärer Ausdruck verwendet, der für übermäßige Rückverfolgung anfällig ist und wahrscheinlich gültige E-Mail-Adressen zurückweist. Sie sollten sie nicht in einer E-Mail-Überprüfungsroutine verwenden. Wenn Sie einen regulären Ausdruck benötigen, der E-Mail-Adressen überprüft, lesen Sie So überprüfen Sie: Dass Zeichenfolgen im gültigen E-Mail-Format vorliegen.

Betrachten Sie beispielsweise einen häufig verwendeten, aber problematischen regulären Ausdruck zum Überprüfen des Alias einer E-Mail-Adresse. Der reguläre Ausdruck ^[0-9A-Z]([-.\w]*[0-9A-Z])*$ wird geschrieben, um zu verarbeiten, was als gültige E-Mail-Adresse betrachtet wird. Eine gültige E-Mail-Adresse besteht aus einem alphanumerischen Zeichen, gefolgt von null oder mehr Zeichen, die alphanumerisch, punkte oder Bindestriche sein können. Der reguläre Ausdruck muss mit einem alphanumerischen Zeichen enden. Wie das folgende Beispiel zeigt, obwohl dieser reguläre Ausdruck gültige Eingaben problemlos verarbeitet, ist seine Leistung ineffizient, wenn er fast gültige Eingaben verarbeitet.

using System;
using System.Diagnostics;
using System.Text.RegularExpressions;

public class DesignExample
{
    public static void Main()
    {
        Stopwatch sw;
        string[] addresses = { "AAAAAAAAAAA@contoso.com",
                             "AAAAAAAAAAaaaaaaaaaa!@contoso.com" };
        // The following regular expression should not actually be used to
        // validate an email address.
        string pattern = @"^[0-9A-Z]([-.\w]*[0-9A-Z])*$";
        string input;

        foreach (var address in addresses)
        {
            string mailBox = address.Substring(0, address.IndexOf("@"));
            int index = 0;
            for (int ctr = mailBox.Length - 1; ctr >= 0; ctr--)
            {
                index++;

                input = mailBox.Substring(ctr, index);
                sw = Stopwatch.StartNew();
                Match m = Regex.Match(input, pattern, RegexOptions.IgnoreCase);
                sw.Stop();
                if (m.Success)
                    Console.WriteLine("{0,2}. Matched '{1,25}' in {2}",
                                      index, m.Value, sw.Elapsed);
                else
                    Console.WriteLine("{0,2}. Failed  '{1,25}' in {2}",
                                      index, input, sw.Elapsed);
            }
            Console.WriteLine();
        }
    }
}

// The example displays output similar to the following:
//     1. Matched '                        A' in 00:00:00.0007122
//     2. Matched '                       AA' in 00:00:00.0000282
//     3. Matched '                      AAA' in 00:00:00.0000042
//     4. Matched '                     AAAA' in 00:00:00.0000038
//     5. Matched '                    AAAAA' in 00:00:00.0000042
//     6. Matched '                   AAAAAA' in 00:00:00.0000042
//     7. Matched '                  AAAAAAA' in 00:00:00.0000042
//     8. Matched '                 AAAAAAAA' in 00:00:00.0000087
//     9. Matched '                AAAAAAAAA' in 00:00:00.0000045
//    10. Matched '               AAAAAAAAAA' in 00:00:00.0000045
//    11. Matched '              AAAAAAAAAAA' in 00:00:00.0000045
//
//     1. Failed  '                        !' in 00:00:00.0000447
//     2. Failed  '                       a!' in 00:00:00.0000071
//     3. Failed  '                      aa!' in 00:00:00.0000071
//     4. Failed  '                     aaa!' in 00:00:00.0000061
//     5. Failed  '                    aaaa!' in 00:00:00.0000081
//     6. Failed  '                   aaaaa!' in 00:00:00.0000126
//     7. Failed  '                  aaaaaa!' in 00:00:00.0000359
//     8. Failed  '                 aaaaaaa!' in 00:00:00.0000414
//     9. Failed  '                aaaaaaaa!' in 00:00:00.0000758
//    10. Failed  '               aaaaaaaaa!' in 00:00:00.0001462
//    11. Failed  '              aaaaaaaaaa!' in 00:00:00.0002885
//    12. Failed  '             Aaaaaaaaaaa!' in 00:00:00.0005780
//    13. Failed  '            AAaaaaaaaaaa!' in 00:00:00.0011628
//    14. Failed  '           AAAaaaaaaaaaa!' in 00:00:00.0022851
//    15. Failed  '          AAAAaaaaaaaaaa!' in 00:00:00.0045864
//    16. Failed  '         AAAAAaaaaaaaaaa!' in 00:00:00.0093168
//    17. Failed  '        AAAAAAaaaaaaaaaa!' in 00:00:00.0185993
//    18. Failed  '       AAAAAAAaaaaaaaaaa!' in 00:00:00.0366723
//    19. Failed  '      AAAAAAAAaaaaaaaaaa!' in 00:00:00.1370108
//    20. Failed  '     AAAAAAAAAaaaaaaaaaa!' in 00:00:00.1553966
//    21. Failed  '    AAAAAAAAAAaaaaaaaaaa!' in 00:00:00.3223372
Imports System.Diagnostics
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim sw As Stopwatch
        Dim addresses() As String = {"AAAAAAAAAAA@contoso.com",
                                   "AAAAAAAAAAaaaaaaaaaa!@contoso.com"}
        ' The following regular expression should not actually be used to 
        ' validate an email address.
        Dim pattern As String = "^[0-9A-Z]([-.\w]*[0-9A-Z])*$"
        Dim input As String

        For Each address In addresses
            Dim mailBox As String = address.Substring(0, address.IndexOf("@"))
            Dim index As Integer = 0
            For ctr As Integer = mailBox.Length - 1 To 0 Step -1
                index += 1
                input = mailBox.Substring(ctr, index)
                sw = Stopwatch.StartNew()
                Dim m As Match = Regex.Match(input, pattern, RegexOptions.IgnoreCase)
                sw.Stop()
                if m.Success Then
                    Console.WriteLine("{0,2}. Matched '{1,25}' in {2}",
                                      index, m.Value, sw.Elapsed)
                Else
                    Console.WriteLine("{0,2}. Failed  '{1,25}' in {2}",
                                      index, input, sw.Elapsed)
                End If
            Next
            Console.WriteLine()
        Next
    End Sub
End Module
' The example displays output similar to the following:
'     1. Matched '                        A' in 00:00:00.0007122
'     2. Matched '                       AA' in 00:00:00.0000282
'     3. Matched '                      AAA' in 00:00:00.0000042
'     4. Matched '                     AAAA' in 00:00:00.0000038
'     5. Matched '                    AAAAA' in 00:00:00.0000042
'     6. Matched '                   AAAAAA' in 00:00:00.0000042
'     7. Matched '                  AAAAAAA' in 00:00:00.0000042
'     8. Matched '                 AAAAAAAA' in 00:00:00.0000087
'     9. Matched '                AAAAAAAAA' in 00:00:00.0000045
'    10. Matched '               AAAAAAAAAA' in 00:00:00.0000045
'    11. Matched '              AAAAAAAAAAA' in 00:00:00.0000045
'    
'     1. Failed  '                        !' in 00:00:00.0000447
'     2. Failed  '                       a!' in 00:00:00.0000071
'     3. Failed  '                      aa!' in 00:00:00.0000071
'     4. Failed  '                     aaa!' in 00:00:00.0000061
'     5. Failed  '                    aaaa!' in 00:00:00.0000081
'     6. Failed  '                   aaaaa!' in 00:00:00.0000126
'     7. Failed  '                  aaaaaa!' in 00:00:00.0000359
'     8. Failed  '                 aaaaaaa!' in 00:00:00.0000414
'     9. Failed  '                aaaaaaaa!' in 00:00:00.0000758
'    10. Failed  '               aaaaaaaaa!' in 00:00:00.0001462
'    11. Failed  '              aaaaaaaaaa!' in 00:00:00.0002885
'    12. Failed  '             Aaaaaaaaaaa!' in 00:00:00.0005780
'    13. Failed  '            AAaaaaaaaaaa!' in 00:00:00.0011628
'    14. Failed  '           AAAaaaaaaaaaa!' in 00:00:00.0022851
'    15. Failed  '          AAAAaaaaaaaaaa!' in 00:00:00.0045864
'    16. Failed  '         AAAAAaaaaaaaaaa!' in 00:00:00.0093168
'    17. Failed  '        AAAAAAaaaaaaaaaa!' in 00:00:00.0185993
'    18. Failed  '       AAAAAAAaaaaaaaaaa!' in 00:00:00.0366723
'    19. Failed  '      AAAAAAAAaaaaaaaaaa!' in 00:00:00.1370108
'    20. Failed  '     AAAAAAAAAaaaaaaaaaa!' in 00:00:00.1553966
'    21. Failed  '    AAAAAAAAAAaaaaaaaaaa!' in 00:00:00.3223372

Die Ausgabe im obigen Beispiel zeigt, dass die Engine für reguläre Ausdrücke den gültigen E-Mail-Alias in etwa demselben Zeitintervall verarbeitet, unabhängig von dessen Länge. Wenn die nahezu gültige E-Mail-Adresse hingegen mehr als fünf Zeichen enthält, wird die Verarbeitungszeit für jedes zusätzliche Zeichen in der Zeichenfolge ungefähr verdoppelt. Daher würde eine fast gültige 28-stellige Zeichenfolge eine Stunde dauern, und eine fast gültige 33-stellige Zeichenfolge würde fast einen Tag dauern.

Da dieser reguläre Ausdruck ausschließlich unter Berücksichtigung des zuzuordnenden Eingabeformats entwickelt wurde, berücksichtigt er nicht die Eingabe, die nicht mit dem Muster übereinstimmt. Dieses Versäumnis kann wiederum unkontrollierte Eingaben ermöglichen, die nahezu mit dem Muster für reguläre Ausdrücke übereinstimmen und die Leistung erheblich beeinträchtigen.

Um dieses Problem zu lösen, können Sie die folgenden Aktionen ausführen:

  • Bei der Entwicklung eines Musters sollten Sie berücksichtigen, wie sich das Backtracking auf die Leistung der Regex-Engine auswirken kann, insbesondere wenn Ihr regulärer Ausdruck für die Verarbeitung unbegrenzter Eingaben konzipiert ist. Weitere Informationen finden Sie im Abschnitt Steuern der Rückverfolgung.

  • Testen Sie den regulären Ausdruck gründlich mit ungültigen, fast gültigen und gültigen Eingaben. Sie können Rex- verwenden, um Eingaben für einen bestimmten regulären Ausdruck zufällig zu generieren. Rex ist ein Tool zum Durchsuchen regulärer Ausdrücke von Microsoft Research.

Objektinstanziierung angemessen behandeln

Im Herzen des regulären Ausdrucksobjektmodells von .NET steht die System.Text.RegularExpressions.Regex-Klasse, die die reguläre Ausdrucksmaschine repräsentiert. Häufig ist der wichtigste Faktor, der die Leistung regulärer Ausdrücke beeinflusst, die Art, wie die Regex Engine verwendet wird. Das Definieren eines regulären Ausdrucks erfordert eine enge Kopplung des regulären Ausdrucksmotors mit einem regulären Ausdrucksmuster. Dieser Kopplungsprozess ist teuer, unabhängig davon, ob ein Regex-Objekt erstellt wird, indem seinem Konstruktor ein regulärer Ausdruck übergeben wird, oder ob eine statische Methode aufgerufen wird, die den regulären Ausdruck und die zu analysierende Zeichenfolge empfängt.

Hinweis

Eine ausführliche Erläuterung der Leistungsauswirkungen der Verwendung interpretierter und kompilierter regulärer Ausdrücke finden Sie im Blogbeitrag Optimieren der Leistung regulärer Ausdrücke, Teil II: Übernehmen von Backtracking.

Sie können das Modul für reguläre Ausdrücke mit einem bestimmten Muster für reguläre Ausdrücke koppeln und dann das Modul verwenden, um den Text auf verschiedene Arten abzugleichen:

  • Sie können eine statische Musterabgleichsmethode aufrufen, z. B. Regex.Match(String, String). Diese Methode erfordert keine Instanziierung eines regulären Ausdrucksobjekts.

  • Sie können ein Regex-Objekt instanziieren und eine Methode zum Abgleichen von Instanzmustern eines interpretierten regulären Ausdrucks aufrufen. Dies ist die Standardmethode zum Binden der Regex-Engine an ein Muster für reguläre Ausdrücke. Es ergibt sich, wenn ein Regex-Objekt ohne options Argument instanziiert wird, das das Compiled-Flag enthält.

  • Sie können ein Regex-Objekt instanziieren und eine Methode für Musterabgleich einer quellgenerierten regulären Expression aufrufen. Diese Technik wird in den meisten Fällen empfohlen. Platzieren Sie dazu das attribut GeneratedRegexAttribute auf eine partielle Methode, die Regexzurückgibt.

  • Sie können ein Regex-Objekt instanziieren und eine Instanzmusterabgleichsmethode eines kompilierten regulären Ausdrucks aufrufen. Reguläre Ausdrucksobjekte stellen kompilierte Muster dar, wenn ein Regex-Objekt mit einem options-Argument instanziiert wird, das das Compiled-Flag enthält.

Die besondere Art und Weise, wie Sie reguläre Ausdrucksabgleichsmethoden aufrufen, kann sich auf die Leistung Ihrer Anwendung auswirken. In den folgenden Abschnitten wird erläutert, wann statische Methodenaufrufe, vom Quell generierte reguläre Ausdrücke, interpretierte reguläre Ausdrücke und kompilierte reguläre Ausdrücke verwendet werden sollen, um die Leistung Ihrer Anwendung zu verbessern.

Wichtig

Die Form des Methodenaufrufs (statisch, interpretiert, generiert, kompiliert) wirkt sich auf die Leistung aus, wenn derselbe reguläre Ausdruck wiederholt in Methodenaufrufen verwendet wird oder eine Anwendung umfangreiche Verwendung regulärer Ausdrucksobjekte vornimmt.

Statische reguläre Ausdrücke

Statische reguläre Ausdrucksmethoden werden als Alternative zum wiederholten Instanziieren eines regulären Ausdrucksobjekts mit demselben regulären Ausdruck empfohlen. Im Gegensatz zu regulären Ausdrucksmustern, die von regulären Ausdrucksobjekten verwendet werden, werden entweder die Vorgangscodes (Opcodes) oder die kompilierte allgemeine Zwischensprache (CIL) aus Mustern, die in statischen Methodenaufrufen verwendet werden, intern vom Regulären Ausdrucksmodul zwischengespeichert.

Beispielsweise ruft ein Ereignishandler häufig eine andere Methode auf, um die Benutzereingabe zu überprüfen. Dieses Beispiel wird im folgenden Code veranschaulicht, in dem das Ereignis Button eines Steuerelements Click verwendet wird, um eine Methode namens IsValidCurrencyaufzurufen, die überprüft, ob der Benutzer ein Währungssymbol gefolgt von mindestens einer Dezimalziffer eingegeben hat.

public void OKButton_Click(object sender, EventArgs e)
{
   if (! String.IsNullOrEmpty(sourceCurrency.Text))
      if (RegexLib.IsValidCurrency(sourceCurrency.Text))
         PerformConversion();
      else
         status.Text = "The source currency value is invalid.";
}
Public Sub OKButton_Click(sender As Object, e As EventArgs) _
           Handles OKButton.Click

    If Not String.IsNullOrEmpty(sourceCurrency.Text) Then
        If RegexLib.IsValidCurrency(sourceCurrency.Text) Then
            PerformConversion()
        Else
            status.Text = "The source currency value is invalid."
        End If
    End If
End Sub

Im folgenden Beispiel wird eine ineffiziente Implementierung der IsValidCurrency-Methode gezeigt:

Hinweis

Jeder Methodenaufruf instanziiert ein neues Regex-Objekt mit demselben Muster. Dies bedeutet wiederum, dass das Muster für reguläre Ausdrücke jedes Mal neu kompiliert werden muss, wenn die Methode aufgerufen wird.

using System;
using System.Text.RegularExpressions;

public class RegexLib
{
   public static bool IsValidCurrency(string currencyValue)
   {
      string pattern = @"\p{Sc}+\s*\d+";
      Regex currencyRegex = new Regex(pattern);
      return currencyRegex.IsMatch(currencyValue);
   }
}
Imports System.Text.RegularExpressions

Public Module RegexLib
    Public Function IsValidCurrency(currencyValue As String) As Boolean
        Dim pattern As String = "\p{Sc}+\s*\d+"
        Dim currencyRegex As New Regex(pattern)
        Return currencyRegex.IsMatch(currencyValue)
    End Function
End Module

Ersetzen Sie den vorherigen ineffizienten Code durch einen Aufruf der statischen Regex.IsMatch(String, String)-Methode. Bei diesem Ansatz ist es nicht erforderlich, jedes Mal ein Regex -Objekt instanziieren zu müssen, wenn Sie eine Musterabgleichsmethode aufrufen möchten, und ermöglicht es dem Regulären Ausdrucksmodul, eine kompilierte Version des regulären Ausdrucks aus dem Cache abzurufen.

using System;
using System.Text.RegularExpressions;

public class RegexLib2
{
   public static bool IsValidCurrency(string currencyValue)
   {
      string pattern = @"\p{Sc}+\s*\d+";
      return Regex.IsMatch(currencyValue, pattern);
   }
}
Imports System.Text.RegularExpressions

Public Module RegexLib
    Public Function IsValidCurrency(currencyValue As String) As Boolean
        Dim pattern As String = "\p{Sc}+\s*\d+"
        Return Regex.IsMatch(currencyValue, pattern)
    End Function
End Module

Standardmäßig werden die letzten 15 zuletzt verwendeten statischen Muster für reguläre Ausdrücke zwischengespeichert. Für Anwendungen, die eine größere Anzahl von zwischengespeicherten statischen regulären Ausdrücken erfordern, kann die Größe des Caches angepasst werden, indem die eigenschaft Regex.CacheSize festgelegt wird.

Der reguläre Ausdruck \p{Sc}+\s*\d+, der in diesem Beispiel verwendet wird, überprüft, ob die Eingabezeichenfolge über ein Währungssymbol und mindestens eine Dezimalziffer verfügt. Das Muster wird wie in der folgenden Tabelle dargestellt definiert:

Muster BESCHREIBUNG
\p{Sc}+ Entspricht einem oder mehreren Zeichen in der Kategorie Unicode-Symbol, Währung.
\s* Übereinstimmung mit null oder mehreren Leerzeichen.
\d+ Entspricht einer oder mehreren Dezimalziffern.

Interpretierte vs. quellcodegenerierte vs. kompilierte reguläre Ausdrücke

Muster für reguläre Ausdrücke, die nicht durch Angabe der Compiled-Option an die Engine für reguläre Ausdrücke gebunden sind, werden interpretiert. Wenn ein reguläres Ausdrucksobjekt instanziiert wird, konvertiert das Modul für reguläre Ausdrücke den regulären Ausdruck in einen Satz von Vorgangscodes. Wenn eine Instanzmethode aufgerufen wird, werden die Vorgangscodes in CIL konvertiert und vom JIT-Compiler ausgeführt. Ebenso wird, wenn eine statische Methode für reguläre Ausdrücke aufgerufen wird und der reguläre Ausdruck nicht im Cache gefunden werden kann, der reguläre Ausdruck vom Modul für reguläre Ausdrücke in eine Reihe von Operationscodes umgewandelt und im Cache gespeichert. Anschließend werden diese Vorgangscodes in CIL konvertiert, sodass der JIT-Compiler sie ausführen kann. Interpretierte reguläre Ausdrücke reduzieren die Startzeit auf kosten einer langsameren Ausführungszeit. Aufgrund dieses Prozesses werden sie am besten verwendet, wenn der reguläre Ausdruck in einer kleinen Anzahl von Methodenaufrufen verwendet wird oder wenn die genaue Anzahl der Aufrufe an reguläre Ausdrucksmethoden unbekannt ist, aber erwartet wird, dass sie klein sind. Da sich die Anzahl der Methodenaufrufe erhöht, wird der Leistungsgewinn von einer reduzierten Startzeit durch die langsamere Ausführungsgeschwindigkeit überholt.

Muster für reguläre Ausdrücke, die durch Angabe der Compiled-Option an die Engine für reguläre Ausdrücke gebunden sind, werden kompiliert. Wenn ein reguläres Ausdrucksobjekt instanziiert wird oder eine statische Methode für reguläre Ausdrücke aufgerufen wird und der reguläre Ausdruck nicht im Cache gefunden werden kann, konvertiert das Modul für reguläre Ausdrücke den regulären Ausdruck in einen zwischengeschalteten Satz von Vorgangscodes. Diese Codes werden dann in CIL konvertiert. Wenn eine Methode aufgerufen wird, führt der JIT-Compiler die CIL aus. Im Gegensatz zu interpretierten regulären Ausdrücken erhöhen kompilierte reguläre Ausdrücke die Startzeit, führen aber einzelne Musterabgleichsmethoden schneller aus. Daher steigt der Performance-Vorteil, der durch die Kompilierung des regulären Ausdrucks entsteht, proportional zur Anzahl der aufgerufenen regulären Ausdrucksmethoden.

Muster für reguläre Ausdrücke, die über das Randsteuerelement einer Methode, die Regex mit dem Attribut GeneratedRegexAttribute an das Modul für reguläre Ausdrücke gebunden zurückgibt, werden quellengeneriert. Der Quellgenerator, der an den Compiler anbindet, gibt als C#-Code eine benutzerdefinierte Regexabgeleitete Implementierung mit Logik aus, die dem entspricht, was RegexOptions.Compiled in CIL emittiert. Sie erhalten alle Vorteile der Durchsatzleistung von RegexOptions.Compiled (mehr, in der Tat) und die Startvorteile von Regex.CompileToAssembly, aber ohne die Komplexität von CompileToAssembly. Die emittierte Quelle ist Teil Ihres Projekts, was bedeutet, dass sie auch leicht sichtbar und zu debuggen ist.

Zusammenfassend empfehlen wir Folgendes:

  • Verwenden Sie interpretierte reguläre Ausdrücke, wenn Sie reguläre Ausdrucksmethoden mit einem bestimmten regulären Ausdruck relativ selten aufrufen.
  • Verwenden Sie quellengenerierte reguläre Ausdrücke, wenn Sie Regex in C#-Argumenten verwenden, die zur Kompilierungszeit bekannt sind, und Sie einen bestimmten regulären Ausdruck relativ häufig verwenden.
  • Verwenden Sie kompilierte reguläre Ausdrücke, wenn Sie reguläre Ausdrücke mit einem bestimmten regulären Ausdruck relativ häufig aufrufen und .NET 6 oder eine frühere Version verwenden.

Es ist schwierig, den genauen Schwellenwert zu ermitteln, bei dem die langsameren Ausführungsgeschwindigkeiten interpretierter regulärer Ausdrücke gegenüber ihren reduzierten Startzeitgewinnen überwiegen. Es ist auch schwierig, den Schwellenwert zu ermitteln, ab dem die langsameren Startzeiten der quellgenerierten oder kompilierten regulären Ausdrücke die Vorteile ihrer schnelleren Ausführungsgeschwindigkeiten überwiegen. Die Schwellenwerte hängen von verschiedenen Faktoren ab, einschließlich der Komplexität des regulären Ausdrucks und der spezifischen Daten, die sie verarbeitet. Um zu ermitteln, welche regulären Ausdrücke die beste Leistung für Ihr bestimmtes Anwendungsszenario bieten, können Sie die Stopwatch Klasse verwenden, um ihre Ausführungszeiten zu vergleichen.

Das folgende Beispiel vergleicht die Leistung von kompilierten, quellgenerierten und interpretierten regulären Ausdrücken beim Lesen der ersten 10 Sätze und beim Lesen aller Sätze im Text von William D. Guthries Magna Carta und andere Adressen. Wenn nur 10 Aufrufe der Methoden zum Abgleichen regulärer Ausdrücke erfolgen, bietet ein interpretierter oder quellgenerierter regulärer Ausdruck eine bessere Leistung als ein kompilierter regulärer Ausdruck, wie die Ausgabe aus dem Beispiel zeigt. Ein kompilierter regulärer Ausdruck bietet jedoch eine bessere Leistung, wenn eine große Anzahl von Anrufen (in diesem Fall über 13.000) getätigt wird.

const string Pattern = @"\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]";

static readonly HttpClient s_client = new();

[GeneratedRegex(Pattern, RegexOptions.Singleline)]
private static partial Regex GeneratedRegex();

public async static Task RunIt()
{
    Stopwatch sw;
    Match match;
    int ctr;

    string text =
            await s_client.GetStringAsync("https://www.gutenberg.org/cache/epub/64197/pg64197.txt");

    // Read first ten sentences with interpreted regex.
    Console.WriteLine("10 Sentences with Interpreted Regex:");
    sw = Stopwatch.StartNew();
    Regex int10 = new(Pattern, RegexOptions.Singleline);
    match = int10.Match(text);
    for (ctr = 0; ctr <= 9; ctr++)
    {
        if (match.Success)
            // Do nothing with the match except get the next match.
            match = match.NextMatch();
        else
            break;
    }
    sw.Stop();
    Console.WriteLine($"   {ctr} matches in {sw.Elapsed}");

    // Read first ten sentences with compiled regex.
    Console.WriteLine("10 Sentences with Compiled Regex:");
    sw = Stopwatch.StartNew();
    Regex comp10 = new Regex(Pattern,
                 RegexOptions.Singleline | RegexOptions.Compiled);
    match = comp10.Match(text);
    for (ctr = 0; ctr <= 9; ctr++)
    {
        if (match.Success)
            // Do nothing with the match except get the next match.
            match = match.NextMatch();
        else
            break;
    }
    sw.Stop();
    Console.WriteLine($"   {ctr} matches in {sw.Elapsed}");

    // Read first ten sentences with source-generated regex.
    Console.WriteLine("10 Sentences with Source-generated Regex:");
    sw = Stopwatch.StartNew();

    match = GeneratedRegex().Match(text);
    for (ctr = 0; ctr <= 9; ctr++)
    {
        if (match.Success)
            // Do nothing with the match except get the next match.
            match = match.NextMatch();
        else
            break;
    }
    sw.Stop();
    Console.WriteLine($"   {ctr} matches in {sw.Elapsed}");

    // Read all sentences with interpreted regex.
    Console.WriteLine("All Sentences with Interpreted Regex:");
    sw = Stopwatch.StartNew();
    Regex intAll = new(Pattern, RegexOptions.Singleline);
    match = intAll.Match(text);
    int matches = 0;
    while (match.Success)
    {
        matches++;
        // Do nothing with the match except get the next match.
        match = match.NextMatch();
    }
    sw.Stop();
    Console.WriteLine($"   {matches:N0} matches in {sw.Elapsed}");

    // Read all sentences with compiled regex.
    Console.WriteLine("All Sentences with Compiled Regex:");
    sw = Stopwatch.StartNew();
    Regex compAll = new(Pattern,
                    RegexOptions.Singleline | RegexOptions.Compiled);
    match = compAll.Match(text);
    matches = 0;
    while (match.Success)
    {
        matches++;
        // Do nothing with the match except get the next match.
        match = match.NextMatch();
    }
    sw.Stop();
    Console.WriteLine($"   {matches:N0} matches in {sw.Elapsed}");

    // Read all sentences with source-generated regex.
    Console.WriteLine("All Sentences with Source-generated Regex:");
    sw = Stopwatch.StartNew();
    match = GeneratedRegex().Match(text);
    matches = 0;
    while (match.Success)
    {
        matches++;
        // Do nothing with the match except get the next match.
        match = match.NextMatch();
    }
    sw.Stop();
    Console.WriteLine($"   {matches:N0} matches in {sw.Elapsed}");

    return;
}
/* The example displays output similar to the following:

   10 Sentences with Interpreted Regex:
       10 matches in 00:00:00.0104920
   10 Sentences with Compiled Regex:
       10 matches in 00:00:00.0234604
   10 Sentences with Source-generated Regex:
       10 matches in 00:00:00.0060982
   All Sentences with Interpreted Regex:
       3,427 matches in 00:00:00.1745455
   All Sentences with Compiled Regex:
       3,427 matches in 00:00:00.0575488
   All Sentences with Source-generated Regex:
       3,427 matches in 00:00:00.2698670
*/

Das im Beispiel \b(\w+((\r?\n)|,?\s))*\w+[.?:;!]verwendete Muster für reguläre Ausdrücke wird wie in der folgenden Tabelle dargestellt definiert:

Muster BESCHREIBUNG
\b Der Vergleich beginnt an einer Wortgrenze.
\w+ Übereinstimmung mit einem oder mehreren Wortzeichen.
(\r?\n)|,?\s) Übereinstimmung mit keinem (null) oder einem Wagenrücklaufzeichen gefolgt von einem Zeilenvorschubzeichen oder keinem (null) oder einem Komma gefolgt von einem Leerzeichen.
(\w+((\r?\n)|,?\s))* Übereinstimmung mit keinem (null) oder mehreren Vorkommen eines oder mehrerer Wortzeichen gefolgt von keinem (null) oder einem Wagenrücklaufzeichen und einem Zeilenvorschubzeichen oder von keinem (null) oder einem Komma gefolgt von einem Leerzeichen.
\w+ Übereinstimmung mit einem oder mehreren Wortzeichen.
[.?:;!] Übereinstimmung mit einem Punkt, Fragezeichen, Doppelpunkt, Semikolon oder Ausrufezeichen.

Steuern der Rückverfolgung

Normalerweise verwendet die Engine für reguläre Ausdrücke eine lineare Vorgehensweise, um eine Eingabezeichenfolge zu durchlaufen und sie mit einem regulären Ausdrucksmuster zu vergleichen. Wenn jedoch unbestimmte Quantifizierer wie *, +und ? in einem Muster für reguläre Ausdrücke verwendet werden, kann das Modul für reguläre Ausdrücke einen Teil der erfolgreichen Teilvergleiche aufgeben und zu einem zuvor gespeicherten Zustand zurückkehren, um nach einer erfolgreichen Übereinstimmung für das gesamte Muster zu suchen. Dieser Prozess wird als Backtracking bezeichnet.

Tipp

Weitere Informationen zur Rückverfolgung finden Sie unter Einzelheiten zum Verhalten regulärer Ausdrücke und Rückverfolgung. Ausführliche Besprechungen der Rückverfolgung finden Sie in den Blogbeiträgen Verbesserungen regulärer Ausdrücke in .NET 7 und Optimieren der Leistung regulärer Ausdrücke.

Durch die Unterstützung des Zurückverfolgens werden reguläre Ausdrücke leistungsstark und flexibel. Außerdem wird die Steuerung der Ausführung der Engine für reguläre Ausdrücke in die Hände der Entwickler von regulären Ausdrücken gelegt. Da sich Entwickler dieser Verantwortung oft nicht bewusst sind, spielt ihr Missbrauch von Backtracking oder die Abhängigkeit von übermäßigem Backtracking häufig die wichtigste Rolle beim Beeinträchtigen der Leistung regulärer Ausdrücke. In einem ungünstigsten Fall kann die Ausführungszeit für jedes zusätzliche Zeichen in der Eingabezeichenfolge verdoppelt werden. Tatsächlich kann man leicht die programmatische Entsprechung einer Endlosschleife erstellen, indem man übermäßig Backtracking verwendet, wenn die Eingabe nahezu mit dem Muster für reguläre Ausdrücke übereinstimmt. Die Reguläre-Ausdrücke-Engine könnte Stunden oder sogar Tage benötigen, um einen relativ kurzen Eingabestring zu verarbeiten.

Leistungseinbußen bei Anwendungen kommen häufig vor, wenn die Rückverfolgung verwendet wird, obwohl diese für einen Abgleich nicht erforderlich ist. Beispielsweise entspricht der reguläre Ausdruck \b\p{Lu}\w*\b allen Wörtern, die mit einem Großbuchstaben beginnen, wie die folgende Tabelle zeigt:

Muster BESCHREIBUNG
\b Der Vergleich beginnt an einer Wortgrenze.
\p{Lu} Übereinstimmung mit einem Großbuchstaben.
\w* Übereinstimmung mit keinem (null) oder mehreren Wortzeichen.
\b Der Vergleich endet an einer Wortgrenze.

Da eine Wortgrenze nicht mit einem Wortzeichen oder einer Teilmenge eines Worts übereinstimmt, besteht keine Möglichkeit, dass das Modul für reguläre Ausdrücke beim Abgleichen von Wortzeichen eine Wortgrenze überschreitet. Bei diesem regulären Ausdruck kann die Rückverfolgung daher nie zum Gesamterfolg eines Abgleichs beitragen. Sie kann lediglich die Leistung beeinträchtigen, da die Engine für reguläre Ausdrücke gezwungen wird, den Zustand für jeden erfolgreichen vorläufigen Abgleich eines Wortzeichens zu speichern.

Wenn Sie feststellen, dass die Zurückverfolgung nicht erforderlich ist, können Sie sie auf verschiedene Arten deaktivieren:

  • Durch Festlegen der option RegexOptions.NonBacktracking (eingeführt in .NET 7). Weitere Informationen finden Sie unter Nicht rückverfolgbarer Modus.

  • Mithilfe des (?>subexpression) Sprachelements, das als atomische Gruppe bezeichnet wird. Im folgenden Beispiel wird eine Eingabezeichenfolge mithilfe von zwei regulären Ausdrücken analysiert. Der erste, \b\p{Lu}\w*\b, beruht auf Rückverfolgung. Die zweite, \b\p{Lu}(?>\w*)\b, deaktiviert das Backtracking. Wie die Ausgabe aus dem Beispiel zeigt, erzeugen beide dasselbe Ergebnis:

    using System;
    using System.Text.RegularExpressions;
    
    public class BackTrack2Example
    {
        public static void Main()
        {
            string input = "This this word Sentence name Capital";
            string pattern = @"\b\p{Lu}\w*\b";
            foreach (Match match in Regex.Matches(input, pattern))
                Console.WriteLine(match.Value);
    
            Console.WriteLine();
    
            pattern = @"\b\p{Lu}(?>\w*)\b";
            foreach (Match match in Regex.Matches(input, pattern))
                Console.WriteLine(match.Value);
        }
    }
    // The example displays the following output:
    //       This
    //       Sentence
    //       Capital
    //
    //       This
    //       Sentence
    //       Capital
    
    Imports System.Text.RegularExpressions
    
    Module Example
        Public Sub Main()
            Dim input As String = "This this word Sentence name Capital"
            Dim pattern As String = "\b\p{Lu}\w*\b"
            For Each match As Match In Regex.Matches(input, pattern)
                Console.WriteLine(match.Value)
            Next
            Console.WriteLine()
    
            pattern = "\b\p{Lu}(?>\w*)\b"
            For Each match As Match In Regex.Matches(input, pattern)
                Console.WriteLine(match.Value)
            Next
        End Sub
    End Module
    ' The example displays the following output:
    '       This
    '       Sentence
    '       Capital
    '       
    '       This
    '       Sentence
    '       Capital
    

In vielen Fällen ist Backtracking für die Übereinstimmung eines regulären Ausdrucksmusters mit Eingabetext unerlässlich. Übermäßige Rückverfolgung kann jedoch die Leistung stark beeinträchtigen und den Eindruck erwecken, dass eine Anwendung nicht mehr reagiert hat. Insbesondere tritt dieses Problem auf, wenn Quantifizierer geschachtelt sind und der Text, der mit dem äußeren Unterausdruck übereinstimmt, eine Teilmenge des Texts ist, der mit dem inneren Unterausdruck übereinstimmt.

Warnung

Vermeiden Sie übermäßige Rückverfolgung, und verwenden Sie außerdem die Timeoutfunktion, um sicherzustellen, dass die Leistung von regulären Ausdrücken nicht zu sehr durch übermäßige Rückverfolgung beeinträchtigt wird. Weitere Informationen finden Sie im Abschnitt Verwenden von Timeoutwerten.

Das Muster für reguläre Ausdrücke ^[0-9A-Z]([-.\w]*[0-9A-Z])*\$$ soll beispielsweise mit einer Teilnummer übereinstimmen, die aus mindestens einem alphanumerischen Zeichen besteht. Alle zusätzlichen Zeichen können aus einem alphanumerischen Zeichen, einem Bindestrich, einem Unterstrich oder einem Punkt bestehen, obwohl das letzte Zeichen alphanumerisch sein muss. Ein Dollarzeichen beendet die Teilenummer. In einigen Fällen kann dieses Muster für reguläre Ausdrücke eine schlechte Leistung aufweisen, da Quantifizierer geschachtelt sind und da der Unterausdruck [0-9A-Z] eine Teilmenge des Unterausdrucks [-.\w]*ist.

In diesen Fällen können Sie die Leistung regulärer Ausdrücke optimieren, indem Sie die geschachtelten Quantifizierer entfernen und den äußeren Teilausdruck durch eine Lookahead- oder Lookbehindassertion mit einer Breite von 0 ersetzen. Lookahead- und Lookbehindassertionen sind Anker. Sie verschieben den Zeiger nicht in der Eingabezeichenfolge, sondern blicken vorwärts oder rückwärts, um zu überprüfen, ob eine bestimmte Bedingung erfüllt wird. Beispielsweise kann der reguläre Ausdruck der Teilenummer als ^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$umgeschrieben werden. Dieses Muster für reguläre Ausdrücke ist wie in der folgenden Tabelle dargestellt definiert:

Muster BESCHREIBUNG
^ Beginnt den Vergleich am Anfang der Eingabezeichenfolge.
[0-9A-Z] Übereinstimmung mit einem alphanumerischen Zeichen. Die Teilnummer muss mindestens dieses Zeichen enthalten.
[-.\w]* Übereinstimmung mit keinem oder mehreren Vorkommen eines beliebigen Wortzeichens, eines Bindestrichs oder eines Punkts.
\$ Übereinstimmung mit einem Dollarzeichen.
(?<=[0-9A-Z]) Sehen Sie hinter dem abschließenden Dollarzeichen nach, um sicherzustellen, dass das davorliegende Zeichen alphanumerisch ist.
$ Beenden Sie das Match am Ende der Eingabezeichenfolge.

Im folgenden Beispiel wird die Verwendung dieses regulären Ausdrucks veranschaulicht, um mit einem Array abzugleichen, das mögliche Teilenummern enthält:

using System;
using System.Text.RegularExpressions;

public class BackTrack4Example
{
    public static void Main()
    {
        string pattern = @"^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$";
        string[] partNos = { "A1C$", "A4", "A4$", "A1603D$", "A1603D#" };

        foreach (var input in partNos)
        {
            Match match = Regex.Match(input, pattern);
            if (match.Success)
                Console.WriteLine(match.Value);
            else
                Console.WriteLine("Match not found.");
        }
    }
}
// The example displays the following output:
//       A1C$
//       Match not found.
//       A4$
//       A1603D$
//       Match not found.
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim pattern As String = "^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$"
        Dim partNos() As String = {"A1C$", "A4", "A4$", "A1603D$",
                                    "A1603D#"}

        For Each input As String In partNos
            Dim match As Match = Regex.Match(input, pattern)
            If match.Success Then
                Console.WriteLine(match.Value)
            Else
                Console.WriteLine("Match not found.")
            End If
        Next
    End Sub
End Module
' The example displays the following output:
'       A1C$
'       Match not found.
'       A4$
'       A1603D$
'       Match not found.

Die Sprache für reguläre Ausdrücke in .NET enthält die folgenden Sprachelemente, die Sie verwenden können, um geschachtelte Quantifizierer zu beseitigen. Weitere Informationen finden Sie unter Gruppierungskonstrukte.

Sprachelement BESCHREIBUNG
(?=subexpression) Positives Lookahead mit einer Breite von 0. Lookaheadüberprüfung für die aktuelle Position, um zu ermitteln, ob subexpression mit der Eingabezeichenfolge übereinstimmt.
(?!subexpression) Negatives Lookahead mit einer Breite von 0. Lookaheadüberprüfung für die aktuelle Position, um zu ermitteln, ob subexpression nicht mit der Eingabezeichenfolge übereinstimmt.
(?<=subexpression) Positives Lookbehind mit einer Breite von 0. Schaut hinter die aktuelle Position, um festzustellen, ob subexpression dem Eingabestring entspricht.
(?<!subexpression) Negatives Lookbehind mit einer Breite von 0. Lookbehindüberprüfung für die aktuelle Position, um zu ermitteln, ob subexpression nicht mit der Eingabezeichenfolge übereinstimmt.

Verwenden von Timeoutwerten

Wenn Ihre regulären Ausdrücke Eingaben verarbeiten, die annähernd mit dem Muster des regulären Ausdrucks übereinstimmen, wird häufig übermäßige Rückverfolgung verwendet. Dies beeinträchtigt die Leistung signifikant. Zusätzlich zur sorgfältigen Prüfung Ihrer Verwendung von Backtracking und dem Testen des regulären Ausdrucks gegen ähnliche Eingaben sollten Sie stets ein Zeitlimit festlegen, um den Effekt eines übermäßigen Backtrackings zu minimieren, falls es auftritt.

Das Timeoutintervall für reguläre Ausdrücke definiert den Zeitraum, in dem das Modul für reguläre Ausdrücke nach einer einzelnen Übereinstimmung sucht, bevor ein Timeout aufgetreten ist. Abhängig vom Muster des regulären Ausdrucks und des Eingabetexts überschreitet die Ausführungszeit möglicherweise das angegebene Timeoutintervall, es wird jedoch nicht mehr Zeit für die Zurückverfolgung aufgewendet als das angegebene Timeoutintervall. Das Standardtimeoutintervall ist Regex.InfiniteMatchTimeout, was bedeutet, dass der reguläre Ausdruck kein Timeout hat. Sie können diesen Wert überschreiben und ein Timeoutintervall wie folgt definieren:

Wenn Sie ein Timeoutintervall definiert haben und eine Übereinstimmung am Ende dieses Intervalls nicht gefunden wird, löst die Methode für reguläre Ausdrücke eine RegexMatchTimeoutException Ausnahme aus. In Ihrem Ausnahmehandler können Sie versuchen, den Abgleich mit einem längeren Zeitüberschreitungsintervall erneut durchzuführen, den Abgleichversuch abbrechen und annehmen, dass keine Übereinstimmung vorhanden ist, oder den Abgleichversuch abbrechen und die Ausnahmeinformationen für eine zukünftige Analyse protokollieren.

Im folgenden Beispiel wird eine GetWordData-Methode definiert, die einen regulären Ausdruck mit einem Timeoutintervall von 350 Millisekunden instanziiert, um die Anzahl der Wörter und die durchschnittliche Anzahl von Zeichen in einem Wort in einem Textdokument zu berechnen. Wenn ein Timout für den entsprechenden Vorgang auftritt, wird das Timeoutintervall um 350 Millisekunden erhöht und das Regex-Objekt erneut instanziiert. Wenn das neue Timeoutintervall 1 Sekunde übersteigt, löst die Methode die Ausnahme erneut für den Aufrufer aus.

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;

public class TimeoutExample
{
    public static void Main()
    {
        RegexUtilities util = new RegexUtilities();
        string title = "Doyle - The Hound of the Baskervilles.txt";
        try
        {
            var info = util.GetWordData(title);
            Console.WriteLine($"Words:               {info.Item1:N0}");
            Console.WriteLine($"Average Word Length: {info.Item2:N2} characters");
        }
        catch (IOException e)
        {
            Console.WriteLine($"IOException reading file '{title}'");
            Console.WriteLine(e.Message);
        }
        catch (RegexMatchTimeoutException e)
        {
            Console.WriteLine($"The operation timed out after {e.MatchTimeout.TotalMilliseconds:N0} milliseconds");
        }
    }
}

public class RegexUtilities
{
    public Tuple<int, double> GetWordData(string filename)
    {
        const int MAX_TIMEOUT = 1000;   // Maximum timeout interval in milliseconds.
        const int INCREMENT = 350;      // Milliseconds increment of timeout.

        List<string> exclusions = new List<string>(new string[] { "a", "an", "the" });
        int[] wordLengths = new int[29];        // Allocate an array of more than ample size.
        string input = null;
        StreamReader sr = null;
        try
        {
            sr = new StreamReader(filename);
            input = sr.ReadToEnd();
        }
        catch (FileNotFoundException e)
        {
            string msg = String.Format("Unable to find the file '{0}'", filename);
            throw new IOException(msg, e);
        }
        catch (IOException e)
        {
            throw new IOException(e.Message, e);
        }
        finally
        {
            if (sr != null) sr.Close();
        }

        int timeoutInterval = INCREMENT;
        bool init = false;
        Regex rgx = null;
        Match m = null;
        int indexPos = 0;
        do
        {
            try
            {
                if (!init)
                {
                    rgx = new Regex(@"\b\w+\b", RegexOptions.None,
                                    TimeSpan.FromMilliseconds(timeoutInterval));
                    m = rgx.Match(input, indexPos);
                    init = true;
                }
                else
                {
                    m = m.NextMatch();
                }
                if (m.Success)
                {
                    if (!exclusions.Contains(m.Value.ToLower()))
                        wordLengths[m.Value.Length]++;

                    indexPos += m.Length + 1;
                }
            }
            catch (RegexMatchTimeoutException e)
            {
                if (e.MatchTimeout.TotalMilliseconds < MAX_TIMEOUT)
                {
                    timeoutInterval += INCREMENT;
                    init = false;
                }
                else
                {
                    // Rethrow the exception.
                    throw;
                }
            }
        } while (m.Success);

        // If regex completed successfully, calculate number of words and average length.
        int nWords = 0;
        long totalLength = 0;

        for (int ctr = wordLengths.GetLowerBound(0); ctr <= wordLengths.GetUpperBound(0); ctr++)
        {
            nWords += wordLengths[ctr];
            totalLength += ctr * wordLengths[ctr];
        }
        return new Tuple<int, double>(nWords, totalLength / nWords);
    }
}
Imports System.Collections.Generic
Imports System.IO
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim util As New RegexUtilities()
        Dim title As String = "Doyle - The Hound of the Baskervilles.txt"
        Try
            Dim info = util.GetWordData(title)
            Console.WriteLine("Words:               {0:N0}", info.Item1)
            Console.WriteLine("Average Word Length: {0:N2} characters", info.Item2)
        Catch e As IOException
            Console.WriteLine("IOException reading file '{0}'", title)
            Console.WriteLine(e.Message)
        Catch e As RegexMatchTimeoutException
            Console.WriteLine("The operation timed out after {0:N0} milliseconds",
                              e.MatchTimeout.TotalMilliseconds)
        End Try
    End Sub
End Module

Public Class RegexUtilities
    Public Function GetWordData(filename As String) As Tuple(Of Integer, Double)
        Const MAX_TIMEOUT As Integer = 1000  ' Maximum timeout interval in milliseconds.
        Const INCREMENT As Integer = 350     ' Milliseconds increment of timeout.

        Dim exclusions As New List(Of String)({"a", "an", "the"})
        Dim wordLengths(30) As Integer        ' Allocate an array of more than ample size.
        Dim input As String = Nothing
        Dim sr As StreamReader = Nothing
        Try
            sr = New StreamReader(filename)
            input = sr.ReadToEnd()
        Catch e As FileNotFoundException
            Dim msg As String = String.Format("Unable to find the file '{0}'", filename)
            Throw New IOException(msg, e)
        Catch e As IOException
            Throw New IOException(e.Message, e)
        Finally
            If sr IsNot Nothing Then sr.Close()
        End Try

        Dim timeoutInterval As Integer = INCREMENT
        Dim init As Boolean = False
        Dim rgx As Regex = Nothing
        Dim m As Match = Nothing
        Dim indexPos As Integer = 0
        Do
            Try
                If Not init Then
                    rgx = New Regex("\b\w+\b", RegexOptions.None,
                                    TimeSpan.FromMilliseconds(timeoutInterval))
                    m = rgx.Match(input, indexPos)
                    init = True
                Else
                    m = m.NextMatch()
                End If
                If m.Success Then
                    If Not exclusions.Contains(m.Value.ToLower()) Then
                        wordLengths(m.Value.Length) += 1
                    End If
                    indexPos += m.Length + 1
                End If
            Catch e As RegexMatchTimeoutException
                If e.MatchTimeout.TotalMilliseconds < MAX_TIMEOUT Then
                    timeoutInterval += INCREMENT
                    init = False
                Else
                    ' Rethrow the exception.
                    Throw
                End If
            End Try
        Loop While m.Success

        ' If regex completed successfully, calculate number of words and average length.
        Dim nWords As Integer
        Dim totalLength As Long

        For ctr As Integer = wordLengths.GetLowerBound(0) To wordLengths.GetUpperBound(0)
            nWords += wordLengths(ctr)
            totalLength += ctr * wordLengths(ctr)
        Next
        Return New Tuple(Of Integer, Double)(nWords, totalLength / nWords)
    End Function
End Class

Erfassung nur bei Bedarf

Reguläre Ausdrücke in .NET unterstützen Gruppierungskonstrukte, mit denen Sie ein Muster für reguläre Ausdrücke in einen oder mehrere Unterausdrücke gruppieren können. Die am häufigsten verwendeten Gruppierungskonstrukte in der .NET-Regulärausdruckssprache sind (Subexpression), die eine nummerierte Erfassungsgruppe definiert, und (?<Name>Subexpression), die eine benannte Erfassungsgruppe definiert. Gruppierungskonstrukte sind für das Erstellen von Backreferences und zum Definieren eines Unterausdrucks, auf den ein Quantifizierer angewendet wird, unerlässlich.

Die Verwendung dieser Sprachelemente hat jedoch einen Kostenaufwand. Sie führen dazu, dass das von der GroupCollection-Eigenschaft zurückgegebene Match.Groups-Objekt mit den neuesten unbenannten oder benannten Erfassungen aufgefüllt wird. Wenn ein einzelnes Gruppierungskonstrukt mehrere Teilzeichenfolgen in der Eingabezeichenfolge erfasst hat, wird auch das von der CaptureCollection-Eigenschaft einer bestimmten Erfassungsgruppe zurückgegebene Group.Captures-Objekt mit mehreren Capture-Objekten aufgefüllt.

Häufig werden Gruppierungskonstrukte nur in einem regulären Ausdruck verwendet, sodass Quantifizierer darauf angewendet werden können. Die von diesen Unterausdrücke erfassten Gruppen werden später nicht verwendet. Beispielsweise ist der reguläre Ausdruck \b(\w+[;,]?\s?)+[.?!] so konzipiert, dass ein ganzer Satz erfasst wird. In der folgenden Tabelle werden die Sprachelemente in diesem regulären Ausdrucksmuster und ihre Auswirkungen auf die Match- und Match.Groups-Auflistungen des Group.Captures-Objekts beschrieben:

Muster BESCHREIBUNG
\b Der Vergleich beginnt an einer Wortgrenze.
\w+ Übereinstimmung mit einem oder mehreren Wortzeichen.
[;,]? Übereinstimmung mit keinem (null) oder einem Komma oder Semikolon.
\s? Übereinstimmung mit keinem (null) oder einem Leerzeichen.
(\w+[;,]?\s?)+ Entspricht einem oder mehreren Vorkommen eines oder mehrerer Wortzeichen gefolgt von einem optionalen Komma oder Semikolon gefolgt von einem optionalen Leerzeichen. Dieses Muster definiert die erste Erfassungsgruppe. Diese ist erforderlich, damit die Kombination mehrerer Wortzeichen (d. h. ein Wort) gefolgt von einem optionalen Interpunktionszeichen wiederholt wird, bis die Engine für reguläre Ausdrücke das Ende eines Satzes erreicht.
[.?!] Übereinstimmung mit einem Punkt, Fragezeichen oder Ausrufezeichen.

Wie das folgende Beispiel zeigt, werden, wenn eine Übereinstimmung gefunden wird, sowohl die GroupCollection- als auch die CaptureCollection-Objekte mit Erfassungen aus der Übereinstimmung gefüllt. In diesem Fall ist die Aufnahmegruppe (\w+[;,]?\s?) vorhanden, sodass der + Quantifizierer darauf angewendet werden kann, sodass das Muster für reguläre Ausdrücke jedem Wort in einem Satz entspricht. Andernfalls würde es mit dem letzten Wort in einem Satz übereinstimmen.

using System;
using System.Text.RegularExpressions;

public class Group1Example
{
    public static void Main()
    {
        string input = "This is one sentence. This is another.";
        string pattern = @"\b(\w+[;,]?\s?)+[.?!]";

        foreach (Match match in Regex.Matches(input, pattern))
        {
            Console.WriteLine($"Match: '{match.Value}' at index {match.Index}.");
            int grpCtr = 0;
            foreach (Group grp in match.Groups)
            {
                Console.WriteLine($"   Group {grpCtr}: '{grp.Value}' at index {grp.Index}.");
                int capCtr = 0;
                foreach (Capture cap in grp.Captures)
                {
                    Console.WriteLine($"      Capture {capCtr}: '{cap.Value}' at {cap.Index}.");
                    capCtr++;
                }
                grpCtr++;
            }
            Console.WriteLine();
        }
    }
}
// The example displays the following output:
//       Match: 'This is one sentence.' at index 0.
//          Group 0: 'This is one sentence.' at index 0.
//             Capture 0: 'This is one sentence.' at 0.
//          Group 1: 'sentence' at index 12.
//             Capture 0: 'This ' at 0.
//             Capture 1: 'is ' at 5.
//             Capture 2: 'one ' at 8.
//             Capture 3: 'sentence' at 12.
//
//       Match: 'This is another.' at index 22.
//          Group 0: 'This is another.' at index 22.
//             Capture 0: 'This is another.' at 22.
//          Group 1: 'another' at index 30.
//             Capture 0: 'This ' at 22.
//             Capture 1: 'is ' at 27.
//             Capture 2: 'another' at 30.
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim input As String = "This is one sentence. This is another."
        Dim pattern As String = "\b(\w+[;,]?\s?)+[.?!]"

        For Each match As Match In Regex.Matches(input, pattern)
            Console.WriteLine("Match: '{0}' at index {1}.",
                              match.Value, match.Index)
            Dim grpCtr As Integer = 0
            For Each grp As Group In match.Groups
                Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                                  grpCtr, grp.Value, grp.Index)
                Dim capCtr As Integer = 0
                For Each cap As Capture In grp.Captures
                    Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                      capCtr, cap.Value, cap.Index)
                    capCtr += 1
                Next
                grpCtr += 1
            Next
            Console.WriteLine()
        Next
    End Sub
End Module
' The example displays the following output:
'       Match: 'This is one sentence.' at index 0.
'          Group 0: 'This is one sentence.' at index 0.
'             Capture 0: 'This is one sentence.' at 0.
'          Group 1: 'sentence' at index 12.
'             Capture 0: 'This ' at 0.
'             Capture 1: 'is ' at 5.
'             Capture 2: 'one ' at 8.
'             Capture 3: 'sentence' at 12.
'       
'       Match: 'This is another.' at index 22.
'          Group 0: 'This is another.' at index 22.
'             Capture 0: 'This is another.' at 22.
'          Group 1: 'another' at index 30.
'             Capture 0: 'This ' at 22.
'             Capture 1: 'is ' at 27.
'             Capture 2: 'another' at 30.

Wenn Sie Unterausdrücke nur verwenden, um Quantifizierer auf sie anzuwenden und sich nicht für den erfassten Text interessieren, sollten Sie Gruppenerfassungen deaktivieren. Das (?:subexpression)-Sprachelement verhindert beispielsweise, dass die Gruppe, auf die es angewendet wird, übereinstimmende Teilzeichenfolgen erfasst. Das Muster des regulären Ausdrucks aus dem vorherigen Beispiel wird im folgenden Beispiel zu \b(?:\w+[;,]?\s?)+[.?!]geändert. Wie die Ausgabe im Beispiel zeigt, wird dadurch verhindert, dass die Engine für reguläre Ausdrücke die GroupCollection- und CaptureCollection-Auflistungen auffüllt:

using System;
using System.Text.RegularExpressions;

public class Group2Example
{
    public static void Main()
    {
        string input = "This is one sentence. This is another.";
        string pattern = @"\b(?:\w+[;,]?\s?)+[.?!]";

        foreach (Match match in Regex.Matches(input, pattern))
        {
            Console.WriteLine($"Match: '{match.Value}' at index {match.Index}.");
            int grpCtr = 0;
            foreach (Group grp in match.Groups)
            {
                Console.WriteLine($"   Group {grpCtr}: '{grp.Value}' at index {grp.Index}.");
                int capCtr = 0;
                foreach (Capture cap in grp.Captures)
                {
                    Console.WriteLine($"      Capture {capCtr}: '{cap.Value}' at {cap.Index}.");
                    capCtr++;
                }
                grpCtr++;
            }
            Console.WriteLine();
        }
    }
}
// The example displays the following output:
//       Match: 'This is one sentence.' at index 0.
//          Group 0: 'This is one sentence.' at index 0.
//             Capture 0: 'This is one sentence.' at 0.
//
//       Match: 'This is another.' at index 22.
//          Group 0: 'This is another.' at index 22.
//             Capture 0: 'This is another.' at 22.
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim input As String = "This is one sentence. This is another."
        Dim pattern As String = "\b(?:\w+[;,]?\s?)+[.?!]"

        For Each match As Match In Regex.Matches(input, pattern)
            Console.WriteLine("Match: '{0}' at index {1}.",
                              match.Value, match.Index)
            Dim grpCtr As Integer = 0
            For Each grp As Group In match.Groups
                Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                                  grpCtr, grp.Value, grp.Index)
                Dim capCtr As Integer = 0
                For Each cap As Capture In grp.Captures
                    Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                      capCtr, cap.Value, cap.Index)
                    capCtr += 1
                Next
                grpCtr += 1
            Next
            Console.WriteLine()
        Next
    End Sub
End Module
' The example displays the following output:
'       Match: 'This is one sentence.' at index 0.
'          Group 0: 'This is one sentence.' at index 0.
'             Capture 0: 'This is one sentence.' at 0.
'       
'       Match: 'This is another.' at index 22.
'          Group 0: 'This is another.' at index 22.
'             Capture 0: 'This is another.' at 22.

Sie können Aufzeichnungen auf eine der folgenden Arten deaktivieren:

  • Verwenden Sie das Sprachelement (?:subexpression). Dieses Element verhindert die Erfassung übereinstimmender Teilzeichenfolge in der Gruppe, auf die es angewendet wird. Die Erfassung von Teilzeichenfolgen in geschachtelten Gruppen wird dadurch nicht deaktiviert.

  • Verwenden Sie die ExplicitCapture-Option. Diese Option deaktiviert alle unbenannten oder impliziten Erfassungen im Muster für reguläre Ausdrücke. Wenn Sie diese Option verwenden, können nur Teilzeichenfolgen erfasst werden, die benannten Gruppen entsprechen, die mit dem (?<name>subexpression) Sprachelement definiert sind. Das ExplicitCapture-Flag kann an den options Parameter eines Regex Klassenkonstruktors oder an den options Parameter einer Regex statischen Abgleichsmethode übergeben werden.

  • Verwenden Sie die Option n im (?imnsx) Sprachenelement. Mit dieser Option werden alle nicht benannten oder impliziten Erfassungen ab dem Punkt innerhalb des regulären Ausdrucks-Musters deaktiviert, an dem das Element erscheint. Erfassungen werden entweder bis zum Ende des Musters deaktiviert oder bis die Option (-n) unbenannte oder implizite Erfassungen aktiviert. Weitere Informationen finden Sie unter Verschiedene Konstrukte.

  • Verwenden Sie die Option n im (?imnsx:subexpression) Sprachenelement. Mit dieser Option werden alle nicht benannten oder impliziten Erfassungen in subexpressiondeaktiviert. Erfassungen von unbenannten oder implizit geschachtelten Erfassungsgruppen sind ebenfalls deaktiviert.

Threadsicherheit

Die Regex-Klasse selbst ist threadsicher und nicht änderbar (schreibgeschützt). Das heißt, Regex Objekte können in jedem Thread erstellt und zwischen Threads freigegeben werden; Abgleichsmethoden können von einem beliebigen Thread aufgerufen und niemals einen globalen Zustand ändern.

Ergebnisobjekte (Match und MatchCollection), die von Regex zurückgegeben werden, sollten jedoch für einen einzelnen Thread verwendet werden. Obwohl viele dieser Objekte logisch unveränderlich sind, können ihre Implementierungen die Berechnung einiger Ergebnisse verzögern, um die Leistung zu verbessern, und daher müssen Aufrufer den Zugriff auf sie serialisieren.

Wenn Sie Regex Ergebnisobjekte für mehrere Threads freigeben müssen, können diese Objekte in threadsichere Instanzen konvertiert werden, indem sie ihre synchronisierten Methoden aufrufen. Mit Ausnahme von Enumerationen sind alle klassen regulärer Ausdrücke threadsicher oder können in threadsichere Objekte durch eine synchronisierte Methode konvertiert werden.

Aufzähler sind die einzige Ausnahme. Sie müssen Aufrufe von Sammlungsenumeratoren serialisieren. Die Regel lautet: Wenn eine Auflistung auf mehreren Threads gleichzeitig aufgezählt werden kann, sollten Sie Enumeratormethoden für das Stammobjekt der Vom Enumerator durchlaufenen Auflistung synchronisieren.

Titel BESCHREIBUNG
Einzelheiten zum Verhalten regulärer Ausdrücke Überprüft die Implementierung der Engine für reguläre Ausdrücke in .NET. Der Artikel konzentriert sich auf die Flexibilität regulärer Ausdrücke und erläutert die Verantwortung des Entwicklers für die Sicherstellung des effizienten und robusten Betriebs des Regulären Ausdrucksmoduls.
Rückverfolgung Erläutert, was backtracking ist und wie sich dies auf die Leistung regulärer Ausdrücke auswirkt, und untersucht Sprachelemente, die Alternativen zur Zurückverfolgung bereitstellen.
Sprachelemente für reguläre Ausdrücke – Kurzübersicht Beschreibt die Elemente der Sprache für reguläre Ausdrücke in .NET und enthält Links zu detaillierten Dokumentationen für jedes Sprachelement.