Udostępnij za pośrednictwem


Najlepsze rozwiązania dotyczące wyrażeń regularnych na platformie .NET

Silnik wyrażeń regularnych w .NET to zaawansowane, w pełni funkcjonalne narzędzie, które przetwarza tekst na podstawie dopasowań wzorców, a nie poprzez porównywanie i dopasowywanie tekstu literalnego. W większości przypadków wykonuje ona szybkie i wydajne dopasowywanie wzorców. Jednak w niektórych przypadkach aparat wyrażeń regularnych może wydawać się powolny. W skrajnych przypadkach może nawet wydawać się przestać odpowiadać, ponieważ przetwarza stosunkowo małe dane wejściowe w ciągu kilku godzin, a nawet dni.

W tym artykule opisano niektóre z najlepszych rozwiązań, które deweloperzy mogą przyjąć, aby zapewnić, że ich wyrażenia regularne osiągną optymalną wydajność.

Ostrzeżenie

W przypadku używania System.Text.RegularExpressions do przetwarzania niezaufanych danych wejściowych należy ustawić limit czasu. Złośliwy użytkownik może podać dane wejściowe RegularExpressions, powodując atak typu "odmowa usługi" . API platformy ASP.NET Core, które używają RegularExpressions, przekazują limit czasu.

Rozważ źródło danych wejściowych

Ogólnie rzecz biorąc, wyrażenia regularne mogą akceptować dwa typy danych wejściowych: ograniczone lub nieograniczone. Ograniczone dane wejściowe to tekst pochodzący ze znanego lub niezawodnego źródła i zgodny ze wstępnie zdefiniowanym formatem. Niekontrolowane dane wejściowe to tekst pochodzący z zawodnego źródła, takiego jak użytkownik internetowy, i może nie spełniać wstępnie zdefiniowanego lub oczekiwanego formatu.

Wzorce wyrażeń regularnych są często zapisywane w celu dopasowania do prawidłowych danych wejściowych. Oznacza to, że deweloperzy badają tekst, który chce dopasować, a następnie piszą wzorzec wyrażenia regularnego pasujący do niego. Następnie deweloperzy określają, czy ten wzorzec wymaga korekty, czy dalszej opracowywania, testując go przy użyciu wielu prawidłowych elementów wejściowych. Gdy wzorzec pasuje do wszystkich domniemanych prawidłowych danych wejściowych, jest zadeklarowany jako gotowy do produkcji i może zostać uwzględniony w wydanej aplikacji. Takie podejście sprawia, że wzorzec wyrażenia regularnego nadaje się do dopasowywania ograniczonych danych wejściowych. Jednak nie nadaje się do dopasowywania nieograniczonych danych wejściowych.

Aby dopasować nieograniczone dane wejściowe, wyrażenie regularne musi efektywnie obsługiwać trzy rodzaje tekstu.

  • Tekst zgodny ze wzorcem wyrażenia regularnego.
  • Tekst, który nie jest zgodny ze wzorcem wyrażenia regularnego.
  • Tekst prawie zgodny ze wzorcem wyrażenia regularnego.

Ostatni typ tekstu jest szczególnie problematyczny w przypadku wyrażenia regularnego, które zostało zapisane do obsługi ograniczonych danych wejściowych. Jeśli to wyrażenie regularne opiera się na rozbudowanym wycofywaniu, aparat wyrażeń regularnych może poświęcić nadmierny czas na przetwarzanie pozornie nieszkodliwego tekstu (w niektórych przypadkach wiele godzin lub dni).

Ostrzeżenie

W poniższym przykładzie użyto wyrażenia regularnego, które jest podatne na nadmierne wycofywanie i prawdopodobnie odrzuci prawidłowe adresy e-mail. Nie należy jej używać w procedurze weryfikacji wiadomości e-mail. Jeśli chcesz wyrażenia regularnego weryfikującego adresy e-mail, zobacz Jak: Sprawdzać, czy ciągi są w prawidłowym formacie poczty e-mail.

Rozważmy na przykład często używane, ale problematyczne wyrażenie regularne do sprawdzania poprawności aliasu adresu e-mail. Wyrażenie regularne ^[0-9A-Z]([-.\w]*[0-9A-Z])*$ jest utworzone do przetwarzania uznawanych za prawidłowe adresów e-mail. Prawidłowy adres e-mail składa się z znaku alfanumerycznego, po którym następuje zero lub więcej znaków, które mogą być alfanumeryczne, kropki lub łączniki. Wyrażenie regularne musi kończyć się znakiem alfanumerycznym. Jednak jak pokazano w poniższym przykładzie, chociaż to wyrażenie regularne łatwo obsługuje prawidłowe dane wejściowe, jego wydajność jest nieefektywna, gdy przetwarza niemal prawidłowe dane wejściowe:

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

Jak pokazano w danych wyjściowych z poprzedniego przykładu, aparat wyrażeń regularnych przetwarza prawidłowy alias wiadomości e-mail w mniej więcej tym samym przedziale czasu niezależnie od jego długości. Z drugiej strony, gdy prawie prawidłowy adres e-mail ma więcej niż pięć znaków, czas przetwarzania wzrasta mniej więcej dwukrotnie dla każdego dodatkowego znaku w ciągu. W związku z tym prawie prawidłowy 28-znakowy ciąg zajmie ponad godzinę przetwarzania, a prawie prawidłowy ciąg 33-znakowy zajmie prawie dzień do przetworzenia.

Ponieważ to wyrażenie regularne zostało opracowane wyłącznie przez uwzględnienie formatu danych wejściowych do dopasowania, nie uwzględnia danych wejściowych, które nie są zgodne ze wzorcem. Z kolei ten nadzór może umożliwić nieograniczone wprowadzanie danych wejściowych, które prawie pasują do wzorca wyrażenia regularnego, aby znacznie obniżyć wydajność.

Aby rozwiązać ten problem, możesz wykonać następujące czynności:

  • Podczas tworzenia wzorca należy rozważyć, w jaki sposób wycofywanie może wpływać na wydajność silnika wyrażeń regularnych, szczególnie jeśli wyrażenie regularne jest zaprojektowane do przetwarzania nieograniczonych danych wejściowych. Aby uzyskać więcej informacji, zobacz sekcję Take Charge of Backtracking (Przejmowanie wycofywania).

  • Dokładnie przetestuj wyrażenie regularne przy użyciu nieprawidłowych, prawie prawidłowych i prawidłowych danych wejściowych. Rex umożliwia losowe generowanie danych wejściowych dla określonego wyrażenia regularnego. Rex to narzędzie do eksploracji wyrażeń regularnych firmy Microsoft Research.

Prawidłowo obsłuż instancjowanie obiektu

W samym sercu modelu obiektu wyrażeń regularnych platformy .NET znajduje się klasa System.Text.RegularExpressions.Regex, która reprezentuje aparat wyrażeń regularnych. Często największym czynnikiem wpływającym na wydajność wyrażeń regularnych jest sposób, w jaki używany jest silnik Regex. Definiowanie wyrażenia regularnego obejmuje ścisłe sprzężenie aparatu wyrażeń regularnych ze wzorcem wyrażeń regularnych. Proces sprzęgania jest kosztowny, niezależnie od tego, czy polega na zainstancjonowaniu obiektu Regex poprzez przekazanie do jego konstruktora wzorca wyrażenia regularnego, czy na wywołaniu metody statycznej poprzez przekazanie jej wzorca wyrażenia regularnego i ciągu do analizy.

Uwaga / Notatka

Aby zapoznać się ze szczegółowym omówieniem wpływu na wydajność używania interpretowanych i skompilowanych wyrażeń regularnych, zobacz wpis w blogu Optymalizacja wydajności wyrażeń regularnych, część II: Zarządzanie cofaniem.

Aparat wyrażeń regularnych można połączyć z określonym wzorcem wyrażeń regularnych, a następnie użyć aparatu, aby dopasować tekst na kilka sposobów:

  • Możesz wywołać statyczną metodę dopasowywania wzorca, taką jak Regex.Match(String, String). Ta metoda nie wymaga tworzenia obiektu wyrażenia regularnego.

  • Można utworzyć obiekt Regex i wywołać metodę dopasowywania wzorca na instancji interpretowanego wyrażenia regularnego, co jest domyślną metodą powiązania silnika wyrażeń regularnych z wzorcem. Utworzenie obiektu Regex następuje, gdy nie podano argumentu zawierającego flagę options do Compiled.

  • Możesz utworzyć obiekt Regex i wywołać metodę dopasowywania wzorca instancji na wyrażeniu regularnym generowanym przez źródło. Ta technika jest zalecana w większości przypadków. W tym celu umieść atrybut GeneratedRegexAttribute w metodzie częściowej, która zwraca Regexwartość.

  • Można utworzyć wystąpienie obiektu Regex i wywołać metodę dopasowywania wzorca do wystąpienia skompilowanego wyrażenia regularnego. Obiekty wyrażeń regularnych reprezentują skompilowane wzorce, gdy wystąpienie Regex obiektu jest tworzone z argumentem options zawierającym flagę Compiled.

Konkretny sposób wywoływania metod dopasowywania wyrażeń regularnych może mieć wpływ na wydajność aplikacji. W poniższych sekcjach omówiono, kiedy używać wywołań metod statycznych, wyrażeń regularnych generowanych przez źródło, interpretowanych wyrażeń regularnych i skompilowanych wyrażeń regularnych w celu poprawy wydajności aplikacji.

Ważne

Forma wywołania metody (statyczna, interpretowana, wygenerowana przez źródło, skompilowana) ma wpływ na wydajność, jeśli to samo wyrażenie regularne jest używane wielokrotnie w wywołaniach metod lub jeśli aplikacja intensywnie korzysta z obiektów wyrażeń regularnych.

Statyczne wyrażenia regularne

Zaleca się stosowanie statycznych metod wyrażeń regularnych jako alternatywy dla wielokrotnego tworzenia obiektu z tym samym wyrażeniem regularnym. W przeciwieństwie do wzorców wyrażeń regularnych używanych przez obiekty wyrażeń regularnych, kody operacji (opcodes) lub skompilowany wspólny język pośredni (CIL) z wzorców używanych w wywołaniach metod statycznych są buforowane wewnętrznie przez aparat wyrażeń regularnych.

Na przykład program obsługi zdarzeń często wywołuje inną metodę w celu zweryfikowania danych wejściowych użytkownika. Ten przykład znajduje swoje odzwierciedlenie w poniższym kodzie, w którym zdarzenie kontrolki Button jest używane do wywołania metody o nazwie Click, która sprawdza, czy użytkownik wprowadził symbol waluty, po którym następuje co najmniej jedna cyfra dziesiętna.

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

Nieefektywna implementacja IsValidCurrency metody jest pokazana w poniższym przykładzie:

Uwaga / Notatka

Każde wywołanie metody ponownie inicjuje Regex obiekt o tym samym wzorcu. Oznacza to z kolei, że wzorzec wyrażenia regularnego musi być ponownie skompilowany za każdym razem, gdy metoda jest wywoływana.

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

Powyższy nieefektywny kod należy zastąpić wywołaniem metody statycznej Regex.IsMatch(String, String) . Takie podejście eliminuje konieczność tworzenia wystąpienia Regex obiektu za każdym razem, gdy chcesz wywołać metodę dopasowywania wzorca, i umożliwia aparatowi wyrażeń regularnych pobranie skompilowanej wersji wyrażenia regularnego z pamięci podręcznej.

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

Domyślnie ostatnie 15 ostatnio używanych statycznych wzorców wyrażeń regularnych jest buforowanych. W przypadku aplikacji, które wymagają większej liczby buforowanych statycznych wyrażeń regularnych, rozmiar pamięci podręcznej można dostosować, ustawiając Regex.CacheSize właściwość .

Wyrażenie \p{Sc}+\s*\d+ regularne używane w tym przykładzie sprawdza, czy ciąg wejściowy ma symbol waluty i co najmniej jedną cyfrę dziesiętną. Wzorzec jest zdefiniowany, jak pokazano w poniższej tabeli:

Wzór Opis
\p{Sc}+ Dopasowuje jeden lub więcej znaków w kategorii Symboli Walutowych Unicode.
\s* Dopasuje zero lub więcej białych znaków.
\d+ Dopasowuje co najmniej jedną cyfrę dziesiętną.

Interpretowane vs. generowane ze źródła vs. skompilowane wyrażenia regularne

Wzorce wyrażeń regularnych, które nie są powiązane z silnikiem wyrażeń regularnych poprzez specyfikację opcji Compiled, są interpretowane. Gdy wystąpi obiekt wyrażenia regularnego, aparat wyrażeń regularnych konwertuje wyrażenie regularne na zestaw kodów operacji. Gdy metoda instancji jest wywoływana, kody operacji są konwertowane na CIL i wykonywane przez kompilator JIT. Podobnie, gdy wywoływana jest statyczna metoda wyrażenia regularnego i nie można odnaleźć wyrażenia regularnego w pamięci podręcznej, aparat wyrażeń regularnych konwertuje wyrażenie regularne na zestaw kodów operacji i przechowuje je w pamięci podręcznej. Następnie konwertuje te kody operacji na CIL, aby kompilator JIT mógł je wykonać. Interpretowane wyrażenia regularne zmniejszają czas uruchamiania kosztem wolniejszego czasu wykonywania. W związku z tym procesem najlepiej używać wyrażenia regularnego w niewielkiej liczbie wywołań metod lub jeśli dokładna liczba wywołań metod wyrażeń regularnych jest nieznana, ale oczekuje się, że będzie mała. W miarę wzrostu liczby wywołań metod zysk wydajności wynikający ze skróconego czasu uruchamiania zostaje zniwelowany przez wolniejszą szybkość wykonywania.

Wzorce wyrażeń regularnych, które są wiązane z silnikiem wyrażeń regularnych poprzez specyfikację opcji Compiled, są kompilowane. W związku z tym, gdy wystąpi obiekt wyrażenia regularnego lub gdy jest wywoływana statyczna metoda wyrażenia regularnego, a wyrażenie regularne nie może być znalezione w pamięci podręcznej, aparat wyrażeń regularnych konwertuje wyrażenie regularne na pośredni zestaw kodów operacji. Te kody są następnie konwertowane na format CIL. Kiedy metoda jest wywoływana, kompilator JIT wykonuje CIL. W przeciwieństwie do interpretowanych wyrażeń regularnych skompilowane wyrażenia regularne zwiększają czas uruchamiania, ale szybciej wykonują poszczególne metody dopasowywania wzorców. W rezultacie korzyść z wydajności wynikającą z kompilowania wyrażenia regularnego zwiększa się proporcjonalnie do liczby wywoływanych metod wyrażeń regularnych.

Wzorce wyrażeń regularnych powiązane z aparatem wyrażeń regularnych poprzez przystrojenie metody zwracającej Regex za pomocą atrybutu GeneratedRegexAttribute są generowane z kodu źródłowego. Generator źródłowy, który podłącza się do kompilatora, emituje kod C# jako niestandardową implementację pochodną o logice podobnej do tego, co Regex emituje w CIL. Uzyskasz wszystkie korzyści z RegexOptions.Compiled wydajności przepustowości (w rzeczywistości nawet więcej) oraz korzyści z uruchamiania programu Regex.CompileToAssembly, ale bez złożoności CompileToAssembly. Źródło, które jest emitowane, jest częścią projektu, co oznacza, że można go również łatwo wyświetlać i debugować.

Podsumowując, zalecamy:

  • Użyj interpretowanych wyrażeń regularnych, gdy wywołujesz metody wyrażeń regularnych z określonym wyrażeniem regularnym stosunkowo rzadko.
  • Użyj wyrażeń regularnych generowanych przez źródło , jeśli używasz Regex w języku C# z argumentami znanymi w czasie kompilacji i używasz stosunkowo często określonego wyrażenia regularnego.
  • Jeśli stosunkowo często wywołujesz metody wyrażeń regularnych z określonym wyrażeniem regularnym i używasz platformy .NET 6 lub starszej wersji, użyj skompilowanych wyrażeń regularnych.

Trudno jest określić dokładny próg, przy którym wolniejsze szybkości wykonywania interpretowanych wyrażeń regularnych przewyższają zyski ze skróconego czasu uruchamiania. Trudno jest również określić próg, przy którym wolniejsze czasy uruchamiania wygenerowanych lub skompilowanych wyrażeń regularnych przewyższają zyski z szybszych szybkości wykonywania. Progi zależą od różnych czynników, w tym złożoności wyrażenia regularnego i określonych danych, które przetwarza. Aby określić, które wyrażenia regularne oferują najlepszą wydajność dla danego scenariusza aplikacji, możesz użyć Stopwatch klasy , aby porównać ich czasy wykonywania.

W poniższym przykładzie porównano wydajność skompilowanych, wygenerowanych z kodu źródłowego i zinterpretowanych wyrażeń regularnych w czasie odczytywania pierwszych 10 zdań i wszystkich zdań w tekście książki Williama D. Guthrie "Magna Carta i inne przemówienia". Jak pokazano w danych wyjściowych z przykładu, gdy tylko 10 wywołań jest wykonywane do metod dopasowywania wyrażeń regularnych, interpretowane lub generowane przez źródło wyrażenie regularne zapewnia lepszą wydajność niż skompilowane wyrażenie regularne. Jednak skompilowane wyrażenie regularne zapewnia lepszą wydajność, gdy zostanie wykonana duża liczba wywołań (w tym przypadku ponad 13 000).

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
*/

Wzorzec wyrażeń regularnych używany w przykładzie \b(\w+((\r?\n)|,?\s))*\w+[.?:;!], jest zdefiniowany, jak pokazano w poniższej tabeli:

Wzór Opis
\b Rozpocznij dopasowanie od granicy słowa.
\w+ Pasuje do co najmniej jednego znaku wyrazu.
(\r?\n)|,?\s) Pasuje do zera lub jednego znaku powrotu karetki, po którym następuje znak nowego wiersza, albo do zera lub jednego przecinka, po którym następuje znak odstępu.
(\w+((\r?\n)|,?\s))* Dopasuje zero lub więcej wystąpień co najmniej jednego znaku słowa, po których następuje zero lub jeden znak powrotu karetki i znak nowego wiersza albo zero lub jeden przecinek, po którym następuje znak odstępu.
\w+ Pasuje do co najmniej jednego znaku wyrazu.
[.?:;!] Odpowiada kropce, znakowi zapytania, dwukropkowi, średnikowi lub wykrzyknikowi.

Przejmij kontrolę nad przeszukiwaniem wstecznym

Zwykle aparat wyrażeń regularnych używa progresji liniowej, aby przejść przez ciąg wejściowy i porównać go ze wzorcem wyrażenia regularnego. Jednak jeśli nieokreślone kwantyfikatory, takie jak *, +i ? są używane we wzorcu wyrażenia regularnego, aparat wyrażeń regularnych może zrezygnować z części pomyślnych częściowych dopasowań i powrócić do wcześniej zapisanego stanu, aby wyszukać pomyślne dopasowanie dla całego wzorca. Ten proces jest nazywany wycofywaniem.

Wskazówka

Aby uzyskać więcej informacji na temat powracania, zobacz Szczegóły zachowania wyrażenia regularnego i Powracanie. Aby szczegółowo omówić wycofywanie, zobacz Ulepszenia wyrażeń regularnych na platformie .NET 7 oraz Optymalizowanie wydajności wyrażeń regularnych w wpisach na blogu.

Obsługa przeszukiwania wstecznego zapewnia moc i elastyczność wyrażeń regularnych. Odpowiedzialność za kontrolowanie działania aparatu wyrażeń regularnych spoczywa również na deweloperach wyrażeń regularnych. Ponieważ programiści często nie są świadomi tej odpowiedzialności, ich błędne użycie backtrackingu lub poleganie na nadmiernym backtrackingu najczęściej odgrywa najważniejszą rolę w obniżaniu wydajności wyrażeń regularnych. W najgorszym scenariuszu czas wykonywania może podwoić się dla każdego dodatkowego znaku w ciągu wejściowym. W rzeczywistości, nadmierne korzystanie z wycofywania może łatwo utworzyć programowy odpowiednik nieskończonej pętli, gdy dane wejściowe prawie pasują do wzorca wyrażenia regularnego. Silnik wyrażeń regularnych może zająć godziny lub nawet dni, aby przetworzyć stosunkowo krótki ciąg wejściowy.

Często aplikacje płacą karę za korzystanie z wycofywania, mimo że wycofywanie nie jest niezbędne dla dopasowania. Na przykład wyrażenie \b\p{Lu}\w*\b regularne pasuje do wszystkich wyrazów rozpoczynających się od wielkiej litery, jak pokazano w poniższej tabeli:

Wzór Opis
\b Rozpocznij dopasowanie od granicy słowa.
\p{Lu} Dopasowuje wielką literę.
\w* Dopasuje zero lub więcej znaków wyrazów.
\b Zakończ mecz na granicy wyrazu.

Ponieważ granica wyrazu nie jest tym samym ani nie jest podzbiorem znaku wyrazu, nie ma możliwości, aby aparat wyrażeń regularnych przekroczył granicę wyrazu podczas dopasowywania znaków wyrazowych. W związku z tym w przypadku tego wyrażenia regularnego wycofywanie nigdy nie może przyczynić się do ogólnego sukcesu każdego dopasowania. Może ona obniżyć wydajność tylko dlatego, że aparat wyrażeń regularnych jest zmuszony do zapisania stanu dla każdego pomyślnego wstępnego dopasowania znaku słowa.

Jeśli okaże się, że wycofywanie nie jest konieczne, można je wyłączyć na kilka sposobów:

  • Ustawiając opcję RegexOptions.NonBacktracking (wprowadzoną na platformie .NET 7). Aby uzyskać więcej informacji, zobacz tryb bez powrotu.

  • Za pomocą elementu językowego (?>subexpression), znanego jako grupa atomowa. Poniższy przykład analizuje ciąg wejściowy przy użyciu dwóch wyrażeń regularnych. Pierwszy element \b\p{Lu}\w*\b, opiera się na wycofywaniu. Drugi element , \b\p{Lu}(?>\w*)\bwyłącza wycofywanie. Jak pokazano w danych wyjściowych z przykładu, oba te elementy generują ten sam wynik:

    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
    

W wielu przypadkach wycofywanie jest niezbędne do dopasowywania wzorca wyrażenia regularnego do tekstu wejściowego. Jednak nadmierne wycofywanie może poważnie obniżyć wydajność i spowodować wrażenie, że aplikacja przestała odpowiadać. W szczególności ten problem pojawia się, gdy kwantyfikatory są zagnieżdżone, i tekst pasujący do podwyrażenia zewnętrznego jest podzbiorem tekstu, który pasuje do podwyrażenia wewnętrznego.

Ostrzeżenie

Oprócz uniknięcia nadmiernego wycofywania należy użyć funkcji limitu czasu, aby upewnić się, że nadmierne wycofywanie nie poważnie obniża wydajności wyrażeń regularnych. Aby uzyskać więcej informacji, zobacz sekcję Use time-out values (Używanie wartości limitu czasu ).

Na przykład wyrażenie regularne ^[0-9A-Z]([-.\w]*[0-9A-Z])*\$$ ma na celu dopasowanie numeru katalogowego, który składa się z co najmniej jednego znaku alfanumerycznego. Wszelkie dodatkowe znaki mogą składać się z znaku alfanumerycznego, łącznika, podkreślenia lub kropki, choć ostatni znak musi być alfanumeryczny. Znak dolara kończy numer części. W niektórych przypadkach ten wzorzec wyrażenia regularnego może wykazywać niską wydajność, ponieważ kwantyfikatory są zagnieżdżone, a podwyrażenie [0-9A-Z] jest podzbiorem podwyrażenia [-.\w]*.

W takich przypadkach można zoptymalizować wydajność wyrażenia regularnego, usuwając zagnieżdżone kwantyfikatory i zastępując zewnętrzne podwyrażenie zerowej szerokości asercjami lookahead lub lookbehind. Asercje lookahead i lookbehind są kotwicami. Nie przenoszą wskaźnika w ciągu wejściowym, ale zamiast tego patrzą do przodu lub za sobą, aby sprawdzić, czy określony warunek jest spełniony. Na przykład wyrażenie regularne numeru części może zostać przepisane jako ^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$. Ten wzorzec wyrażenia regularnego jest zdefiniowany, jak pokazano w poniższej tabeli:

Wzór Opis
^ Rozpoczyna dopasowanie na początku ciągu wejściowego.
[0-9A-Z] Dopasuj znak alfanumeryczny. Numer części musi zawierać co najmniej ten znak.
[-.\w]* Dopasuj zero lub więcej wystąpień dowolnego znaku alfanumerycznego, łącznika bądź kropki.
\$ Dopasuj znak dolara.
(?<=[0-9A-Z]) Popatrz za ostatnim znakiem dolara, aby upewnić się, że poprzedni znak jest alfanumeryczny.
$ Zakończ dopasowanie na końcu ciągu wejściowego.

Poniższy przykład ilustruje użycie tego wyrażenia regularnego do dopasowania tablicy zawierającej możliwe numery części:

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.

Język wyrażeń regularnych na platformie .NET zawiera następujące elementy języka, których można użyć do wyeliminowania zagnieżdżonych kwantyfikatorów. Aby uzyskać więcej informacji, zobacz Konstrukcje grupowania.

Element języka Opis
(?= subexpression ) Dodatnie spojrzenie o zerowej szerokości. Wyprzedza bieżące położenie, aby określić, czy subexpression pasuje do ciągu wejściowego.
(?! subexpression ) Negatywne wyprzedzanie o zerowej długości. Sprawdza pozycję przed bieżącym położeniem, aby stwierdzić, czy subexpression nie pasuje do ciągu wejściowego.
(?<= subexpression ) Dodatnie spojrzenie o zerowej szerokości. Szuka bieżącej pozycji, aby określić, czy subexpression pasuje do ciągu wejściowego.
(?<! subexpression ) Ujemne spojrzenie wstecz o zerowej szerokości. Szuka bieżącego położenia, aby określić, czy subexpression nie jest zgodny z ciągiem wejściowym.

Użyj wartości limitu czasu

Jeśli wyrażenia regularne przetwarzają dane wejściowe, które prawie pasują do wzorca wyrażenia regularnego, często mogą polegać na nadmiernym wycofywaniu, co znacząco wpływa na wydajność. Oprócz starannego rozważenia użycia wycofywania oraz testowania wyrażenia regularnego na niemal pasujących danych wejściowych, należy zawsze ustawić limit czasu, aby zminimalizować wpływ nadmiernego wycofywania, jeśli do niego dojdzie.

Przedział czasowy limitu wyrażeń regularnych określa czas, przez jaki silnik wyrażeń regularnych będzie szukał pojedynczego dopasowania przed upływem limitu czasu. W zależności od wzorca wyrażenia regularnego i tekstu wejściowego czas wykonywania może przekroczyć określony przedział czasowy limitu, ale nie poświęci więcej czasu na wycofywanie niż ten określony przedział. Domyślny interwał limitu czasu to Regex.InfiniteMatchTimeout, co oznacza, że limit czasu wyrażenia regularnego nie zostanie przekroczony. Tę wartość można zastąpić i zdefiniować interwał limitu czasu w następujący sposób:

Jeśli zdefiniowano przedział czasu oczekiwania i dopasowanie nie zostanie znalezione na końcu tego przedziału, metoda wyrażeń regularnych zgłasza wyjątek RegexMatchTimeoutException. W procedurze obsługi wyjątków możesz wybrać ponowienie próby dopasowania z dłuższym limitem czasu, zrezygnować z próby dopasowania i założyć brak dopasowania, lub zrezygnować z próby dopasowania i zalogować informacje o wyjątku na potrzeby przyszłej analizy.

W poniższym przykładzie zdefiniowano metodę GetWordData , która tworzy wystąpienie wyrażenia regularnego z interwałem limitu czasu wynoszącym 350 milisekund w celu obliczenia liczby wyrazów i średniej liczby znaków w słowie w dokumencie tekstowym. Jeśli upłynął limit czasu dopasowanej operacji, limit czasu jest zwiększany o 350 milisekund, a Regex obiekt jest ponownie inicjalizowany. Jeśli nowy przedział czasowy limitu czasu przekracza jedną sekundę, metoda ponownie zgłasza wyjątek do elementu wywołującego.

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

Przechwytywanie tylko wtedy, gdy jest to konieczne

Wyrażenia regularne na platformie .NET obsługują konstrukcje grupowania, które umożliwiają grupowanie wzorca wyrażenia regularnego w co najmniej jedno wyrażenie podrzędne. Najczęściej używane konstrukcje grupowania w języku wyrażeń regularnych platformy .NET to (podwyrażenie, które definiuje numerowaną grupę przechwytywania, i )(?<>, które definiuje nazwaną grupę przechwytywania. Konstrukcje grupowania są niezbędne do tworzenia odwołań wstecznych i definiowania podwyrażeń, do których zastosowano kwantyfikator.

Jednak użycie tych elementów języka ma koszt. Powodują one, że GroupCollection obiekt zwrócony przez Match.Groups właściwość jest wypełniany najnowszymi przechwyceniami bez nazw lub z nazwami. Jeśli jedna konstrukcja grupowania przechwyciła wiele podłańcuchów w ciągu wejściowym, to również wypełniają one obiekt CaptureCollection zwrócony przez właściwość Group.Captures określonej grupy przechwytywania licznymi obiektami Capture.

Często konstrukcje grupowania są używane tylko w wyrażeniu regularnym, aby można było do nich stosować kwantyfikatory. Grupy przechwycone przez te podwyrażenia nie są używane później. Na przykład wyrażenie \b(\w+[;,]?\s?)+[.?!] regularne jest przeznaczone do przechwytywania całego zdania. W poniższej tabeli opisano elementy języka w tym wzorcu wyrażenia regularnego oraz ich wpływ na Match obiekty Match.Groups i kolekcje Group.Captures.

Wzór Opis
\b Rozpocznij dopasowanie od granicy słowa.
\w+ Pasuje do co najmniej jednego znaku wyrazu.
[;,]? Dopasuje zero lub jeden przecinek lub średnik.
\s? Dopasuje zero lub jeden znak odstępu.
(\w+[;,]?\s?)+ Dopasowuje jedno lub więcej wystąpień co najmniej jednej litery lub cyfry, po których może nastąpić przecinek lub średnik, a następnie opcjonalnie znak odstępu. Ten wzorzec definiuje pierwszą grupę przechwytywania, która jest niezbędna, aby możliwe było powtarzanie kombinacji wielu znaków wyrazów (czyli słowa), po których następuje opcjonalny symbol interpunkcyjny, aż do momentu, gdy silnik wyrażeń regularnych automatycznie osiągnie koniec zdania.
[.?!] Pasuje do kropki, znaku zapytania lub wykrzyknika.

Jak pokazano w poniższym przykładzie, po znalezieniu dopasowania, obiekty GroupCollection i CaptureCollection są wypełniane przechwyconymi danymi z dopasowania do wzorca. W tym przypadku grupa przechwytująca (\w+[;,]?\s?) istnieje po to, aby można było do niej zastosować kwantyfikator +, co umożliwia dopasowanie wzorca wyrażenia regularnego do każdego słowa w zdaniu. W przeciwnym razie będzie pasował do ostatniego słowa w zdaniu.

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.

Jeśli używasz podwyrażenia tylko do stosowania kwantyfikatorów do nich i nie interesuje Cię przechwycony tekst, należy wyłączyć przechwytywanie grup. Na przykład element języka (?:subexpression), do którego się odnosi, uniemożliwia grupie przechwytywanie pasujących podciągów. W poniższym przykładzie wzorzec wyrażenia regularnego z poprzedniego przykładu został zmieniony na \b(?:\w+[;,]?\s?)+[.?!]. Jak pokazano w danych wyjściowych, aparat wyrażeń regularnych zapobiega wypełnianiu kolekcji GroupCollection i CaptureCollection.

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.

Przechwytywanie można wyłączyć w jeden z następujących sposobów:

  • Użyj elementu języka (?:subexpression). Element ten uniemożliwia przechwytywanie pasujących podciągów w grupie, do której ma zastosowanie. Nie wyłącza przechwytywania podciągów w zagnieżdżonych grupach.

  • Użyj opcji ExplicitCapture. Wyłącza wszystkie nienazwane lub niejawne przechwytywania we wzorcu wyrażenia regularnego. W przypadku korzystania z tej opcji można przechwycić tylko podciągi zgodne z nazwanymi grupami zdefiniowanymi przy użyciu elementu języka (?<name>subexpression). Flagę ExplicitCapture można przekazać do options parametru konstruktora Regex klasy lub options parametru statycznej Regex metody dopasowania.

  • Użyj opcji n w elemencie języka (?imnsx). Ta opcja wyłącza wszystkie nienazwane lub niejawne przechwytywania od miejsca we wzorcu wyrażenia regularnego, w którym pojawia się dany element. Przechwytywanie jest wyłączone do końca wzorca lub do momentu, gdy opcja (-n) umożliwia nienazwane lub niejawne przechwytywanie. Aby uzyskać więcej informacji, zobacz Różne konstrukcje.

  • Użyj opcji n w elemencie języka (?imnsx:subexpression). Ta opcja wyłącza wszystkie nienazwane lub niejawne przechwytywania w subexpression. Przechwytywanie przez nienazwane lub ukryte zagnieżdżone grupy przechwytywania jest również wyłączone.

Bezpieczeństwo wątków

Sama Regex klasa jest bezpieczna wątkowo i niezmienna (tylko do odczytu). Oznacza to, że Regex obiekty można tworzyć w dowolnym wątku i współdzielić między wątkami. Metody dopasowywania mogą być wywoływane z dowolnego wątku i nigdy nie zmieniają żadnego stanu globalnego.

Jednak obiekty wynikowe (Match i MatchCollection) zwracane przez Regex powinny być używane w jednym wątku. Chociaż wiele z tych obiektów jest logicznie niezmiennych, ich implementacje mogą opóźnić obliczanie niektórych wyników, aby poprawić wydajność, a w rezultacie obiekty wywołujące muszą serializować dostęp do nich.

Jeśli potrzebujesz współużytkować Regex obiekty wyników w wielu wątkach, te obiekty można przekonwertować na wystąpienia bezpieczne wątkowo, wywołując ich zsynchronizowane metody. Z wyjątkiem modułów wyliczających wszystkie klasy wyrażeń regularnych są bezpieczne wątkowo lub mogą być konwertowane na obiekty bezpieczne wątkowo przez zsynchronizowaną metodę.

Moduły wyliczania są jedynym wyjątkiem. Należy serializować wywołania do modułów wyliczania kolekcji. Reguła polega na tym, że jeśli kolekcję można wyliczyć jednocześnie na więcej niż jeden wątek, należy zsynchronizować metody modułu wyliczającego na obiekcie głównym kolekcji przechodzącej przez moduł wyliczający.

Nazwa Opis
Szczegóły zachowania wyrażenia regularnego Analizuje implementację aparatu wyrażeń regularnych na platformie .NET. Artykuł koncentruje się na elastyczności wyrażeń regularnych i wyjaśnia, jak deweloper ponosi odpowiedzialność za zapewnienie wydajnej i niezawodnej pracy aparatu wyrażeń regularnych.
Cofanie się Wyjaśnia, czym jest wycofywanie i jak wpływa na wydajność wyrażeń regularnych, oraz analizuje elementy języka, które zapewniają alternatywy dla wycofywania.
Język wyrażeń regularnych — podręczny wykaz Opisuje elementy języka wyrażeń regularnych na platformie .NET i zawiera linki do szczegółowej dokumentacji dla każdego elementu języka.