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

Aparat wyrażeń regularnych na platformie .NET to zaawansowane, w pełni funkcjonalne narzędzie, które przetwarza tekst na podstawie dopasowań wzorca, a nie na porównywaniu i dopasowywaniu tekstu literału. W większości przypadków dopasowanie do wzorca przebiega szybko i skutecznie. Jednak w niektórych przypadkach aparat wyrażeń regularnych może wydawać się powolny. W skrajnych przypadkach może nawet pozornie przestać odpowiadać, ponieważ przetwarza stosunkowo mało danych wejściowych w ciągu kilku godzin lub 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 metody do przetwarzania niezaufanych danych wejściowych należy przekazać limit czasu. Złośliwy użytkownik może przekazać dane wejściowe , RegularExpressionspowodując atak typu "odmowa usługi". ASP.NET podstawowe interfejsy API platformy, które używają RegularExpressions przekroczenia limitu czasu.

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

Ogólnie rzecz biorąc, wyrażenia regularne mogą przyjmować dwa typy danych wejściowych: ograniczone i nieograniczone. Ograniczone dane wejściowe to tekst pochodzący ze znanego lub niezawodnego źródła i zgodny ze wstępnie zdefiniowanym formatem. Nieprzeciągniętych danych wejściowych jest tekstem pochodzącym z zawodnego źródła, takiego jak użytkownik internetowy, i może nie być zgodne ze wstępnie zdefiniowanym lub oczekiwanym formatem.

Wzorce wyrażeń regularnych są często zapisywane w celu dopasowania do prawidłowych danych wejściowych. Oznacza to, że deweloperzy sprawdzają tekst, który chcą dopasować, a następnie piszą wzorzec wyrażenia regularnego, który mu odpowiada. Następnie deweloperzy określają, czy wzorzec wymaga poprawek, testując go dla 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 nieprzeciętnych danych wejściowych.

Aby dopasować nieprzeciągniętych danych wejściowych, wyrażenie regularne musi efektywnie obsługiwać trzy rodzaje tekstu:

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

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

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, aby wyrażenie regularne weryfikuje adresy e-mail, zobacz Instrukcje: sprawdzanie, 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 ^[0-9A-Z]([-.\w]*[0-9A-Z])*$ regularne jest zapisywane w celu przetworzenia tego, co jest uważane za prawidłowy adres 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 być zakończone 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 Example
{
   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 około 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żna wykonać następujące czynności:

  • Podczas tworzenia wzorca należy uwzględnić wpływ wycofywania na wydajność aparatu wyrażeń regularnych, szczególnie jeśli wyrażenie regularne jest przeznaczone 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.

Obsługa wystąpienia obiektu odpowiednio

W samym sercu programu . Model obiektu wyrażenia regularnego platformy NET jest klasą System.Text.RegularExpressions.Regex reprezentującą aparat wyrażeń regularnych. Często największym czynnikiem wpływającym na wydajność wyrażeń regularnych jest sposób, w jaki Regex jest używany silnik. Definiowanie wyrażenia regularnego polega na ścisłym sprzęganiu aparatu wyrażeń regularnych z wzorcem wyrażenia regularnego. Ten proces sprzęgania, niezależnie od tego, czy polega na utworzeniu wystąpienia Regex obiektu przez przekazanie jego konstruktora wzorca wyrażenia regularnego, czy wywołanie metody statycznej przez przekazanie go wzorca wyrażenia regularnego i przeanalizowania ciągu, jest koniecznością kosztowną.

Uwaga

Aby zapoznać się ze szczegółowym omówieniem wpływu na wydajność używania interpretowanych i skompilowanych wyrażeń regularnych, zobacz Optymalizowanie wydajności wyrażeń regularnych, część II: przejmowanie wycofywania w blogu zespołu BCL.

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 utworzenia wystąpienia obiektu wyrażenia regularnego.

  • Można utworzyć wystąpienie obiektu i wywołać metodę Regex dopasowywania wzorca wystąpienia interpretowanego wyrażenia regularnego, która jest domyślną metodą powiązania aparatu wyrażeń regularnych z wzorcem wyrażeń regularnych. Powoduje to utworzenie wystąpienia Regex obiektu bez argumentu zawierającego options flagę Compiled .

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

  • Możesz utworzyć obiekt specjalnego przeznaczenia Regex , który jest ściśle powiązany z określonym wzorcem wyrażeń regularnych, skompilować go i zapisać w autonomicznym zestawie. Możesz wywołać metodę Regex.CompileToAssembly , aby ją skompilować i zapisać.

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 oraz interpretowanych i skompilowanych wyrażeń regularnych, aby poprawić wydajność aplikacji.

Ważne

Jeśli to samo wyrażenie regularne jest używane wielokrotnie w wywołaniach metod lub jeśli obiekty wyrażeń regularnych są często używane w aplikacji, sposób wywoływania metody (statyczny, interpretowany, skompilowany) znacząco wpływa na wydajność.

Statyczne wyrażenia regularne

Statyczne metody wyrażeń regularnych są zalecane jako alternatywa dla wielokrotnego tworzenia wystąpienia obiektu wyrażenia regularnego z tym samym wyrażeniem regularnym. W przeciwieństwie do wzorców wyrażeń regularnych używanych przez obiekty wyrażeń regularnych, kody operacji 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ę do weryfikacji danych wejściowych użytkownika. Ten przykład jest odzwierciedlony w poniższym kodzie, w którym Button zdarzenie kontrolki Click jest używane do wywoływania metody o nazwie IsValidCurrency, 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

Każde wywołanie metody ponownie inicjuje Regex obiekt o tym samym wzorcu. To z kolei oznacza, że wzorzec wyrażenia regularnego należy kompilować podczas każdego wywołania metody.

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 RegexLib
{
   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 buforowanych jest piętnaście ostatnio używanych statycznych wzorców wyrażeń regularnych. 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:

Wzorzec opis
\p{Sc}+ Dopasuje co najmniej jeden znak w kategorii Symbol Unicode, Waluta.
\s* Dopasuje zero lub więcej znaków odstępu.
\d+ Dopasuje co najmniej jedną cyfrę dziesiętną.

Interpretowane a skompilowane wyrażenia regularne

Wzorce wyrażeń regularnych, które nie są powiązane z aparatem wyrażeń regularnych Compiled za pomocą specyfikacji opcji, są interpretowane. Kiedy tworzone jest wystąpienie obiektu wyrażenia regularnego, aparat wyrażeń regularnych konwertuje wyrażenie regularne na zestaw kodów operacji. Po wywołaniu metody wystąpienia kody operacji są konwertowane na format 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 ograniczają czas uruchamiania kosztem wolniejszego czasu wykonania. 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. Wraz ze wzrostem liczby wywołań metod przyrost wydajności związany ze skróceniem czasu uruchamiania jest coraz mniejszy wskutek wolniejszego wykonywania.

Wzorce wyrażeń regularnych powiązane z aparatem wyrażeń regularnych Compiled za pomocą specyfikacji opcji 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. Po wywołaniu metody kompilator JIT wykonuje element CIL. W przeciwieństwie do interpretowanych wyrażeń regularnych, skompilowane wyrażenia regularne wydłużają czas uruchamiania, ale wykonanie pojedynczych metod dopasowania do wzorca jest szybsze. W rezultacie korzyści w zakresie wydajności wynikające z kompilowania wyrażeń regularnych rosną proporcjonalnie do liczby wywoływanych metod wyrażeń regularnych.

Podsumowując, zaleca się używać interpretowanych wyrażeń regularnych, kiedy stosunkowo rzadko są wywoływane metody wyrażeń regularnych z określonym wyrażeniem regularnym. Zaleca się używać skompilowanych wyrażeń regularnych, kiedy stosunkowo często są wywoływane metody wyrażeń regularnych z określonym wyrażeniem regularnym. Trudno jest określić dokładny próg, przy którym wolniejsze szybkości wykonywania interpretowanych wyrażeń regularnych przewyższają zyski ze zmniejszonego czasu uruchamiania lub próg, przy którym wolniejsze czasy uruchamiania skompilowanych wyrażeń regularnych przewyższają zyski z szybszych szybkości wykonywania. Zależy to od różnych czynników, w tym złożoności wyrażenia regularnego i określonych danych, które przetwarza. Aby określić, czy wyrażenia regularne interpretowane lub kompilowane oferują najlepszą wydajność dla danego scenariusza aplikacji, możesz użyć Stopwatch klasy do porównania ich czasów wykonywania.

Poniższy przykład porównuje wydajność skompilowanych i interpretowanych wyrażeń regularnych podczas odczytywania pierwszych 10 zdań i odczytywania wszystkich zdań w tekście the Financier The Financier TheOdore Dreiser. Jak pokazuje dane wyjściowe z przykładu, gdy do metod dopasowywania wyrażeń regularnych są wykonywane tylko 10 wywołań, interpretowane wyrażenie regularne zapewnia lepszą wydajność niż skompilowane wyrażenie regularne. Jednak skompilowane wyrażenie regularne oferuje lepszą wydajność dla dużej liczby wywołań (w tym przypadku ponad 13 000).

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

public class Example
{
   public static void Main()
   {
      string pattern = @"\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]";
      Stopwatch sw;
      Match match;
      int ctr;

      StreamReader inFile = new StreamReader(@".\Dreiser_TheFinancier.txt");
      string input = inFile.ReadToEnd();
      inFile.Close();

      // Read first ten sentences with interpreted regex.
      Console.WriteLine("10 Sentences with Interpreted Regex:");
      sw = Stopwatch.StartNew();
      Regex int10 = new Regex(pattern, RegexOptions.Singleline);
      match = int10.Match(input);
      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("   {0} matches in {1}", ctr, 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(input);
      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("   {0} matches in {1}", ctr, sw.Elapsed);

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

      // Read all sentences with compiled regex.
      Console.WriteLine("All Sentences with Compiled Regex:");
      sw = Stopwatch.StartNew();
      Regex compAll = new Regex(pattern,
                      RegexOptions.Singleline | RegexOptions.Compiled);
      match = compAll.Match(input);
      matches = 0;
      while (match.Success) {
         matches++;
         // Do nothing with the match except get the next match.
         match = match.NextMatch();
      }
      sw.Stop();
      Console.WriteLine("   {0:N0} matches in {1}", matches, sw.Elapsed);
   }
}
// The example displays the following output:
//       10 Sentences with Interpreted Regex:
//          10 matches in 00:00:00.0047491
//       10 Sentences with Compiled Regex:
//          10 matches in 00:00:00.0141872
//       All Sentences with Interpreted Regex:
//          13,443 matches in 00:00:01.1929928
//       All Sentences with Compiled Regex:
//          13,443 matches in 00:00:00.7635869
//
//       >compare1
//       10 Sentences with Interpreted Regex:
//          10 matches in 00:00:00.0046914
//       10 Sentences with Compiled Regex:
//          10 matches in 00:00:00.0143727
//       All Sentences with Interpreted Regex:
//          13,443 matches in 00:00:01.1514100
//       All Sentences with Compiled Regex:
//          13,443 matches in 00:00:00.7432921
Imports System.Diagnostics
Imports System.IO
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim pattern As String = "\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]"
        Dim sw As Stopwatch
        Dim match As Match
        Dim ctr As Integer

        Dim inFile As New StreamReader(".\Dreiser_TheFinancier.txt")
        Dim input As String = inFile.ReadToEnd()
        inFile.Close()

        ' Read first ten sentences with interpreted regex.
        Console.WriteLine("10 Sentences with Interpreted Regex:")
        sw = Stopwatch.StartNew()
        Dim int10 As New Regex(pattern, RegexOptions.SingleLine)
        match = int10.Match(input)
        For ctr = 0 To 9
            If match.Success Then
                ' Do nothing with the match except get the next match.
                match = match.NextMatch()
            Else
                Exit For
            End If
        Next
        sw.Stop()
        Console.WriteLine("   {0} matches in {1}", ctr, sw.Elapsed)

        ' Read first ten sentences with compiled regex.
        Console.WriteLine("10 Sentences with Compiled Regex:")
        sw = Stopwatch.StartNew()
        Dim comp10 As New Regex(pattern,
                     RegexOptions.SingleLine Or RegexOptions.Compiled)
        match = comp10.Match(input)
        For ctr = 0 To 9
            If match.Success Then
                ' Do nothing with the match except get the next match.
                match = match.NextMatch()
            Else
                Exit For
            End If
        Next
        sw.Stop()
        Console.WriteLine("   {0} matches in {1}", ctr, sw.Elapsed)

        ' Read all sentences with interpreted regex.
        Console.WriteLine("All Sentences with Interpreted Regex:")
        sw = Stopwatch.StartNew()
        Dim intAll As New Regex(pattern, RegexOptions.SingleLine)
        match = intAll.Match(input)
        Dim matches As Integer = 0
        Do While match.Success
            matches += 1
            ' Do nothing with the match except get the next match.
            match = match.NextMatch()
        Loop
        sw.Stop()
        Console.WriteLine("   {0:N0} matches in {1}", matches, sw.Elapsed)

        ' Read all sentences with compiled regex.
        Console.WriteLine("All Sentences with Compiled Regex:")
        sw = Stopwatch.StartNew()
        Dim compAll As New Regex(pattern,
                       RegexOptions.SingleLine Or RegexOptions.Compiled)
        match = compAll.Match(input)
        matches = 0
        Do While match.Success
            matches += 1
            ' Do nothing with the match except get the next match.
            match = match.NextMatch()
        Loop
        sw.Stop()
        Console.WriteLine("   {0:N0} matches in {1}", matches, sw.Elapsed)
    End Sub
End Module
' The example displays output like the following:
'       10 Sentences with Interpreted Regex:
'          10 matches in 00:00:00.0047491
'       10 Sentences with Compiled Regex:
'          10 matches in 00:00:00.0141872
'       All Sentences with Interpreted Regex:
'          13,443 matches in 00:00:01.1929928
'       All Sentences with Compiled Regex:
'          13,443 matches in 00:00:00.7635869
'       
'       >compare1
'       10 Sentences with Interpreted Regex:
'          10 matches in 00:00:00.0046914
'       10 Sentences with Compiled Regex:
'          10 matches in 00:00:00.0143727
'       All Sentences with Interpreted Regex:
'          13,443 matches in 00:00:01.1514100
'       All Sentences with Compiled Regex:
'          13,443 matches in 00:00:00.7432921

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

Wzorzec opis
\b Rozpoczyna dopasowanie na granicy wyrazu.
\w+ Pasuje do co najmniej jednego znaku wyrazu.
(\r?\n)|,?\s) Pasuje do zera lub jednego powrotu karetki, po którym następuje znak nowego wiersza albo zero lub jeden przecinek, 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.
[.?:;!] Pasuje do kropki, znaku zapytania, dwukropka, średnika lub wykrzyknika.

Wyrażenia regularne: skompilowane do zestawu

Platforma .NET umożliwia również tworzenie zestawu zawierającego skompilowane wyrażenia regularne. Ta funkcja przenosi wydajność kompilacji wyrażeń regularnych od czasu wykonywania do czasu projektowania. Jednak wiąże się to również z dodatkową pracą. Należy zdefiniować wyrażenia regularne z wyprzedzeniem i skompilować je do zestawu. Kompilator może następnie odwoływać się do tego zestawu podczas kompilowania kodu źródłowego, który używa wyrażeń regularnych zestawu. Każde skompilowane wyrażenie regularne w zestawie jest reprezentowane przez klasę pochodzącą z Regexklasy .

Aby skompilować wyrażenia regularne do zestawu, należy wywołać Regex.CompileToAssembly(RegexCompilationInfo[], AssemblyName) metodę i przekazać ją do tablicy RegexCompilationInfo obiektów i AssemblyName obiektu. Obiekty RegexCompilationInfo reprezentują wyrażenia regularne do skompilowania, a AssemblyName obiekt zawierający informacje o zestawie do utworzenia.

Zaleca się, aby kompilować wyrażenia regularne do zestawu w następujących sytuacjach:

  • Jeśli jesteś deweloperem składników, który chce utworzyć bibliotekę wyrażeń regularnych wielokrotnego użytku.
  • Jeśli oczekujesz, że metody dopasowywania wzorców wyrażenia regularnego będą wywoływane nieokreśloną liczbą razy — w dowolnym miejscu od raz do dwóch do tysięcy lub dziesiątek tysięcy razy. W przeciwieństwie do skompilowanych lub interpretowanych wyrażeń regularnych, wyrażenia regularne kompilowane do oddzielnych zestawów oferują wydajność spójną niezależnie od liczby wywołań metod.

Jeśli używasz skompilowanych wyrażeń regularnych do optymalizacji wydajności, nie należy używać odbicia do tworzenia zestawu, ładowania aparatu wyrażeń regularnych i wykonywania jego metod dopasowywania wzorców. Unikanie odbicia wymaga, aby nie kompilować wzorców wyrażeń regularnych dynamicznie i określać wszelkie opcje dopasowywania wzorców, takie jak dopasowywanie wzorca bez uwzględniania wielkości liter, podczas tworzenia zestawu. Wymaga to również odseparowania kodu, który tworzy zestaw, od kodu, który używa wyrażenia regularnego.

W poniższym przykładzie pokazano, w jaki sposób utworzyć zestaw zawierający skompilowane wyrażenie regularne. Tworzy zestaw o nazwie RegexLib.dll z pojedynczą klasą wyrażeń regularnych. SentencePattern Ta klasa zawiera wzorzec wyrażenia regularnego dopasowywania zdań używany w sekcji Interpretowane a skompilowane wyrażenia regularne.

using System;
using System.Reflection;
using System.Text.RegularExpressions;

public class Example
{
   public static void Main()
   {
      RegexCompilationInfo SentencePattern =
                           new RegexCompilationInfo(@"\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]",
                                                    RegexOptions.Multiline,
                                                    "SentencePattern",
                                                    "Utilities.RegularExpressions",
                                                    true);
      RegexCompilationInfo[] regexes = { SentencePattern };
      AssemblyName assemName = new AssemblyName("RegexLib, Version=1.0.0.1001, Culture=neutral, PublicKeyToken=null");
      Regex.CompileToAssembly(regexes, assemName);
   }
}
Imports System.Reflection
Imports System.Text.RegularExpressions

Module Example
    Public Sub Main()
        Dim SentencePattern As New RegexCompilationInfo("\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]",
                                                        RegexOptions.Multiline,
                                                        "SentencePattern",
                                                        "Utilities.RegularExpressions",
                                                        True)
        Dim regexes() As RegexCompilationInfo = {SentencePattern}
        Dim assemName As New AssemblyName("RegexLib, Version=1.0.0.1001, Culture=neutral, PublicKeyToken=null")
        Regex.CompileToAssembly(regexes, assemName)
    End Sub
End Module

Gdy przykład zostanie skompilowany do pliku wykonywalnego i uruchomiony, tworzy zestaw o nazwie RegexLib.dll. Klasa pochodząca Utilities.RegularExpressions.SentencePattern z Regex klasy reprezentuje wyrażenie regularne. W poniższym przykładzie użyto skompilowanego wyrażenia regularnego, aby wyodrębnić zdania z tekstu theodore Dreiser's The Financier:

using System;
using System.IO;
using System.Text.RegularExpressions;
using Utilities.RegularExpressions;

public class Example
{
   public static void Main()
   {
      SentencePattern pattern = new SentencePattern();
      StreamReader inFile = new StreamReader(@".\Dreiser_TheFinancier.txt");
      string input = inFile.ReadToEnd();
      inFile.Close();

      MatchCollection matches = pattern.Matches(input);
      Console.WriteLine("Found {0:N0} sentences.", matches.Count);
   }
}
// The example displays the following output:
//      Found 13,443 sentences.
Imports System.IO
Imports System.Text.RegularExpressions
Imports Utilities.RegularExpressions

Module Example
    Public Sub Main()
        Dim pattern As New SentencePattern()
        Dim inFile As New StreamReader(".\Dreiser_TheFinancier.txt")
        Dim input As String = inFile.ReadToEnd()
        inFile.Close()

        Dim matches As MatchCollection = pattern.Matches(input)
        Console.WriteLine("Found {0:N0} sentences.", matches.Count)
    End Sub
End Module
' The example displays the following output:
'      Found 13,443 sentences.

Przejmowanie wycofywania

Zazwyczaj aparat wyrażeń regularnych używa progresji liniowej do przechodzenia przez ciąg wejściowy i porównywania 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. Proces ten jest znany pod nazwą wycofywania.

Napiwek

Aby uzyskać więcej informacji na temat wycofywania, zobacz Szczegóły zachowania wyrażenia regularnego i wycofywania. Szczegółowe omówienie wycofywania można znaleźć w wpisach w blogu Ulepszenia wyrażeń regularnych na platformie .NET 7 i Optymalizowanie wydajności wyrażeń regularnych.

Obsługa wycofywania daje wyrażeniom regularnym duże możliwości i elastyczność. Dodatkowo odpowiedzialność za kontrolowanie operacji aparatu wyrażeń regularnych spada na deweloperów wyrażeń regularnych. Ponieważ deweloperzy często nie są tego świadomi, błędne użycie wycofywania lub nadmierne poleganie na wycofywaniu często odgrywa najbardziej znaczącą rolę w zmniejszeniu wydajności wyrażeń regularnych. W scenariuszu najgorszego przypadku czas wykonywania może podwajać się dla każdego dodatkowego znaku w ciągu wejściowym. W rzeczywistości, korzystając z wycofywania nadmiernie, łatwo jest utworzyć programowy odpowiednik nieskończonej pętli, jeśli dane wejściowe prawie pasują do wzorca wyrażenia regularnego. Aparat wyrażeń regularnych może potrwać kilka godzin, a 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:

Wzorzec opis
\b Rozpoczyna dopasowanie na granicy wyrazu.
\p{Lu} Dopasuje wielkie litery.
\w* Dopasuje zero lub więcej znaków wyrazów.
\b Kończy dopasowanie na granicy wyrazu.

Ponieważ granica wyrazu nie jest taka sama jak lub podzbiór słowa, nie ma możliwości, aby aparat wyrażeń regularnych przekroczył granicę słowa podczas dopasowywania znaków wyrazów. 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ę (wprowadzoną RegexOptions.NonBacktracking na platformie .NET 7). Aby uzyskać więcej informacji, zobacz Tryb nonbacktracking.

  • Za pomocą (?>subexpression) elementu języka, znanego jako grupa niepodzielna. W poniższym przykładzie jest analizowana składnia ciągu wejściowego 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 Example
    {
       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 dla dopasowania wzorca wyrażenia regularnego do tekstu wejściowego. Należy pamiętać, że nadmierne używanie wycofywania może poważnie obniżyć wydajność i stworzyć wrażanie, ze aplikacja przestała odpowiadać. W szczególności ten problem pojawia się, gdy kwantyfikatory są zagnieżdżone, a tekst zgodny z podrażeniem zewnętrznym jest podzbiorem tekstu, który pasuje do wewnętrznego podrażenia.

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 wzorzec ^[0-9A-Z]([-.\w]*[0-9A-Z])*\$$ wyrażenia regularnego jest przeznaczony do dopasowania numeru części składającego się z co najmniej jednego znaku alfanumerycznego. Jakiekolwiek dodatkowe znaki mogą być znakami alfanumerycznymi, łącznikami, podkreśleniami lub kropkami, ale ostatni znak musi być alfanumeryczny. Znak dolara przerywa 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 asercją wyprzedzającą lub wsteczną o zerowej szerokości. Asercji 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:

Wzorzec opis
^ Rozpoczyna dopasowanie na początku ciągu wejściowego.
[0-9A-Z] Dopasowuje znak alfanumeryczny. Numer części musi zawierać przynajmniej jeden znak.
[-.\w]* Dopasowanie do zera lub większej liczby wystąpień dowolnego znaku słowa, łącznika lub kropki.
\$ Dopasowanie do znaku dolara.
(?<=[0-9A-Z]) Spójrz za końcowy znak dolara, aby upewnić się, że poprzedni znak jest alfanumeryczny.
$ Dopasowywanie kończy się 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 Example
{
   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 ) Pozytywna asercja wyprzedzająca o zerowej szerokości. Wyprzedza bieżące położenie, aby określić, czy subexpression pasuje do ciągu wejściowego.
(?! subexpression ) Negatywna asercja wyprzedzająca o zerowej szerokości. Przyjrzyj się bieżącemu położeniu, aby określić, czy subexpression ciąg wejściowy nie jest zgodny.
(?<= subexpression ) Pozytywna asercja wsteczna o zerowej szerokości. Szuka bieżącej pozycji, aby określić, czy subexpression pasuje do ciągu wejściowego.
(?<! subexpression ) Negatywna asercja wsteczna 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żenie regularne przetwarza dane wejściowe, które niemal pasują do wzorca wyrażenia regularnego, często może nadmiernie używać wycofywania, co znacznie wpływa na wydajność. Oprócz dokładnego rozważenia użycia wycofywania i testowania wyrażenia regularnego względem niemal pasujących danych wejściowych należy zawsze ustawić wartość limitu czasu, aby zminimalizować wpływ nadmiernego wycofywania, jeśli wystąpi.

Interwał limitu czasu wyrażenia regularnego definiuje okres, przez który aparat 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 przekraczać określony interwał limitu czasu, ale nie spędzi więcej czasu na wycofywanie niż określony interwał limitu czasu. 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 interwał limitu czasu i dopasowanie nie zostanie znalezione na końcu tego interwału, metoda wyrażenia regularnego zgłasza RegexMatchTimeoutException wyjątek. W procedurze obsługi wyjątków możesz wybrać ponowienie próby dopasowania z dłuższym interwałem przekroczenia limitu czasu, porzucić próbę dopasowania i założyć, że nie ma dopasowania lub porzucić próbę dopasowania i zarejestrować 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 pasującej operacji, interwał limitu czasu jest zwiększany o 350 milisekund, a Regex obiekt jest potwierdzany ponownie. Jeśli nowy interwał przekroczenia limitu czasu przekracza jedną sekundę, metoda ponownie wywróci wyjątek do elementu wywołującego.

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

public class Example
{
   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:               {0:N0}", info.Item1);
         Console.WriteLine("Average Word Length: {0:N2} characters", info.Item2);
      }
      catch (IOException e) {
         Console.WriteLine("IOException reading file '{0}'", title);
         Console.WriteLine(e.Message);
      }
      catch (RegexMatchTimeoutException e) {
         Console.WriteLine("The operation timed out after {0:N0} milliseconds",
                           e.MatchTimeout.TotalMilliseconds);
      }
   }
}

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 grupę przechwytywania numerowanego i (?<podwyrażenie)) nazw>, które definiuje nazwaną grupę przechwytywania. Konstrukcje grupujące są niezbędne do tworzenia odwołań wstecznych i do definiowania podwyrażeń, do których jest stosowany kwantyfikator.

Jednak zastosowanie tych elementów języka jest kosztowne. Powodują one, że GroupCollection obiekt zwrócony przez Match.Groups właściwość jest wypełniany najnowszymi nazwami nienazwanych lub nazwanymi przechwytywaniami. Jeśli jedna konstrukcja grupowania przechwyciła wiele podciągów w ciągu wejściowym, wypełniają również CaptureCollection obiekt zwrócony przez Group.Captures właściwość określonej grupy przechwytywania z wieloma Capture obiektami.

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 kolekcje i Group.Captures obiektyMatch.Groups:

Wzorzec opis
\b Rozpoczyna dopasowanie na granicy wyrazu.
\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?)+ Dopasuje jedno lub więcej wystąpień co najmniej jednego znaku słowa, po którym następuje opcjonalny przecinek lub średnik, po którym następuje opcjonalny znak odstępu. Ten wzorzec definiuje pierwszą grupę przechwytywania, która jest niezbędna, tak aby kombinacja wielu znaków wyrazów (czyli słowa), po których następuje opcjonalny symbol interpunkcyjny, zostanie powtórzona, dopóki aparat wyrażeń regularnych nie osiągnie końca 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 przechwytywaniem z dopasowania. W takim przypadku grupa (\w+[;,]?\s?) przechwytywania istnieje, aby + można było do niej zastosować kwantyfikator, co umożliwia dopasowanie wzorca wyrażenia regularnego do każdego wyrazu w zdaniu. W przeciwnym razie dopasowanie nastąpi dla ostatniego wyrazu w zdaniu.

using System;
using System.Text.RegularExpressions;

public class Example
{
   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: '{0}' at index {1}.",
                           match.Value, match.Index);
         int grpCtr = 0;
         foreach (Group grp in match.Groups) {
            Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                              grpCtr, grp.Value, grp.Index);
            int capCtr = 0;
            foreach (Capture cap in grp.Captures) {
               Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                 capCtr, cap.Value, 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 uniemożliwia grupie, (?:subexpression) do której ma zastosowanie 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 nie wypełnia GroupCollection kolekcji i CaptureCollection :

using System;
using System.Text.RegularExpressions;

public class Example
{
   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: '{0}' at index {1}.",
                           match.Value, match.Index);
         int grpCtr = 0;
         foreach (Group grp in match.Groups) {
            Console.WriteLine("   Group {0}: '{1}' at index {2}.",
                              grpCtr, grp.Value, grp.Index);
            int capCtr = 0;
            foreach (Capture cap in grp.Captures) {
               Console.WriteLine("      Capture {0}: '{1}' at {2}.",
                                 capCtr, cap.Value, 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ć na jeden z poniższych sposobów:

  • (?:subexpression) Użyj elementu language. Ten element zapobiega przechwytywaniu dopasowanych podciągów w grupie, do której jest stosowany. Nie wyłącza przechwytywania podciągów w żadnych grupach zagnieżdżonych.

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

  • n Użyj opcji w elemecie (?imnsx) języka. Powoduje to wyłączenie wszystkich nienazwanych lub niejawnych przechwytywań od miejsca we wzorcu wyrażenia regularnego, w którym znajduje się ten element. Przechwytywanie jest wyłączone do końca wzorca lub do momentu (-n) włączenia opcji nienazwanych lub niejawnych przechwytywania. Aby uzyskać więcej informacji, zobacz Różne konstrukcje.

  • n Użyj opcji w elemecie (?imnsx:subexpression) języka. Ta opcja wyłącza wszystkie nienazwane lub niejawne przechwytywanie w programie subexpression. Przechwytywania przez jakiekolwiek nienazwane lub niejawne zagnieżdżone grupy przechwytywania również są wyłączone.

Nazwa opis
Szczegóły dotyczące zachowania wyrażeń regularnych 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.
Śledzenie wsteczne Informacje, co to jest wycofywanie i jak wpływa na wydajność wyrażeń regularnych oraz analiza elementów języka, które dostarczają 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.