Рекомендации по использованию регулярных выражений в .NET

Обработчик регулярных выражений в .NET — мощное средство, обрабатывающее текст на основе совпадения шаблонов, а не сравнивающее непосредственно текст. В большинстве случаев сопоставление шаблонов выполняется быстро и эффективно. Однако в некоторых случаях подсистема регулярных выражений может оказаться медленной. В крайних случаях он даже может перестать отвечать, обрабатывая относительно небольшой объем входной информации в течение часов или даже дней.

В этой статье описаны некоторые рекомендации, которые разработчики могут принять, чтобы обеспечить оптимальную производительность своих регулярных выражений.

Предупреждение

При использовании System.Text.RegularExpressions для обработки ненадежных входных данных передайте время ожидания. Злоумышленник может предоставить входные данные RegularExpressions, вызывая атаку типа "отказ в обслуживании". API платформы ASP.NET Core, использующие RegularExpressions, передают время ожидания.

Учет источника входных данных

Регулярные выражения могут принимать два типа входных данных: определенные и произвольные. Ограниченные входные данные — это текст, исходящий из известного или надежного источника и соответствующий предварительно определенному формату. Неуверенные входные данные — это текст, исходящий из ненадежного источника, например веб-пользователя, и может не соответствовать предопределенным или ожидаемым форматам.

Шаблоны регулярных выражений часто записываются в соответствие с допустимыми входными данными. Это значит, что разработчик анализирует текст, который требуется найти, и составляет шаблон регулярного выражения. Затем разработчик определяет, требуется ли корректировка шаблона или его уточнение, тестируя его с различными подходящими входными данными. Если шаблон соответствует всем предполагаемым допустимым входным данным, он объявляется готовым к рабочей среде и может быть включен в выпущенное приложение. Этот подход делает шаблон регулярного выражения подходящим для сопоставления ограниченных входных данных. Однако он не делает его подходящим для сопоставления несеченных входных данных.

Чтобы соответствовать неократным входным данным, регулярное выражение должно эффективно обрабатывать три типа текста:

  • Текст, соответствующий шаблону регулярного выражения.
  • Текст, который не соответствует шаблону регулярного выражения.
  • Текст, почти соответствующий шаблону регулярного выражения.

Последний вид текста представляет особую проблему для регулярных выражений, написанных для обработки определенных входных данных. Если в регулярном выражении также широко используется поиск с возвратом, обработчик регулярных выражений может неожиданно долго (в некоторых случаях часы или дни) обрабатывать, казалось бы, безобидный текст.

Предупреждение

В следующем примере используется регулярное выражение, которое подвержено чрезмерному обратному отслеживанию и, скорее всего, отклоняет допустимые адреса электронной почты. Его не следует использовать в подпрограмме проверки электронной почты. Если требуется регулярное выражение, проверяющее адреса электронной почты, см. информацию в разделе Практическое руководство. Проверка строк на соответствие формату электронной почты.

Например, рассмотрим часто используемое, но проблематичное регулярное выражение для проверки псевдонима адреса электронной почты. Регулярное выражение ^[0-9A-Z]([-.\w]*[0-9A-Z])*$ записывается для обработки того, что считается допустимым адресом электронной почты. Допустимый адрес электронной почты состоит из буквенно-цифрового символа, за которым следует нулевая или более символов, которые могут быть буквенно-цифровыми, периодами или дефисами. Регулярное выражение должно оканчиваться алфавитно-цифровым символом. Однако, как показано в следующем примере, хотя это регулярное выражение легко обрабатывает допустимые входные данные, его производительность неэффективна при обработке почти допустимых входных данных:

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

Как показано в выходных данных предыдущего примера, обработчик регулярных выражений обрабатывает допустимый псевдоним электронной почты примерно в тот же интервал времени независимо от его длины. С другой стороны, если почти допустимый адрес электронной почты имеет более пяти символов, время обработки приблизительно удваивается для каждого дополнительного символа в строке. Таким образом, почти допустимая строка 28 символов займет более часа для обработки, и почти допустимая строка 33 символов займет почти день для обработки.

Так как это регулярное выражение было разработано исключительно с учетом формата входных данных, он не учитывает входные данные, которые не соответствуют шаблону. Этот надзор, в свою очередь, может разрешить неограниченные входные данные, которые почти соответствуют шаблону регулярного выражения, чтобы значительно снизить производительность.

Чтобы устранить эту проблему, можно выполнить одно из следующих действий.

  • При создании шаблона необходимо учитывать, как поиск с возвратом может повлиять на производительность обработчика регулярных выражений, особенно если регулярное выражение должно обрабатывать произвольные входные данные. Дополнительные сведения см. в подразделе Грамотное использование поиска с возвратом.

  • Тщательно протестируйте регулярное выражение с помощью недопустимых, практически допустимых и допустимых входных данных. Rex можно использовать для случайного создания входных данных для определенного регулярного выражения. Rex — это средство исследования регулярных выражений из Microsoft Research.

Правильное создание объектов

В самом центре . Объектная модель регулярных выражений NET — это System.Text.RegularExpressions.Regex класс, представляющий обработчик регулярных выражений. Часто производительность регулярного выражения зависит именно от того, как используется обработчик Regex. Определение регулярного выражения предполагает установление тесной взаимозависимости между обработчиком регулярных выражений и шаблоном регулярного выражения. Этот процесс связывания, будь то создание экземпляра Regex объекта путем передачи конструктора шаблона регулярного выражения или вызова статического метода путем передачи его шаблона регулярного выражения и строки для анализа, является необходимостью дорогой.

Примечание.

Подробное обсуждение последствий использования интерпретированных и скомпилированных регулярных выражений см. в статье "Оптимизация производительности регулярных выражений", часть II. Выполнение обратного отслеживания в блоге группы BCL.

Подсистему регулярных выражений можно сочетать с определенным шаблоном регулярного выражения, а затем использовать обработчик для сопоставления текста несколькими способами:

  • Можно вызвать статический метод поиска совпадения с шаблоном, такой как Regex.Match(String, String). Этот метод не требует создания экземпляра объекта регулярного выражения.

  • Можно создать экземпляр Regex объекта и вызвать метод сопоставления шаблонов экземпляра интерпретированного регулярного выражения, который является методом по умолчанию для привязки обработчика регулярных выражений к шаблону регулярного выражения. Он дает результат, когда объект Regex создается без аргумента options, включающего флажок Compiled.

  • Можно создать экземпляр объекта Regex и вызвать метод поиска совпадения с шаблоном этого экземпляра для скомпилированного регулярного выражения. Объекты регулярных выражений представляют скомпилированные шаблоны, когда объект Regex создается с аргументом options с флажком Compiled.

  • Вы можете создать объект специального назначения Regex , который тесно связан с определенным шаблоном регулярного выражения, компилировать его и сохранить его в автономной сборке. Метод можно вызвать для компиляции Regex.CompileToAssembly и сохранения.

Конкретный способ вызова методов сопоставления регулярных выражений может повлиять на производительность приложения. В последующих разделах рассказывается, когда лучше использовать вызовы статического метода, интерпретированные регулярные выражения и скомпилированные регулярные выражения для повышения производительности приложения.

Внимание

Способ вызова метода (статический, интерпретированный, скомпилированный) влияет на производительность, если одно регулярное выражение используется многократно при вызове методов или если приложение активно использует объекты регулярных выражений.

Статические регулярные выражения

Статические методы регулярных выражений рекомендуется использовать как альтернативу многократному созданию объектов регулярных выражений с одним регулярным выражением. В отличие от шаблонов регулярных выражений, используемых объектами регулярных выражений, коды операций или скомпилированный общий промежуточный язык (CIL) из шаблонов, используемых в вызовах статических методов, кэшируются подсистемой регулярных выражений.

Например, обработчик событий часто вызывает другой метод для проверки пользовательского ввода. Этот пример отражается в следующем коде, в котором Button событие элемента управления Click используется для вызова метода с именемIsValidCurrency, который проверка указывает, введен ли пользователь символ валюты, за которым следует по крайней мере одна десятичная цифра.

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

Неэффективная реализация IsValidCurrency метода показана в следующем примере:

Примечание.

Каждый метод вызывает повторное создание Regex объекта с одинаковым шаблоном. А это значит, что шаблон регулярного выражения перекомпилируется при каждом вызове метода.

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

Необходимо заменить предыдущий неэффективный код вызовом статического Regex.IsMatch(String, String) метода. Этот подход устраняет необходимость создания экземпляра Regex объекта при каждом вызове метода сопоставления шаблонов и позволяет обработчику регулярных выражений получить скомпилированную версию регулярного выражения из кэша.

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

По умолчанию кэшируется 15 последних использованных шаблонов статических регулярных выражений. Для приложений, которым требуется большее число кэшированных статических регулярных выражений, размер кэша можно задать с помощью свойства Regex.CacheSize.

Регулярное выражение \p{Sc}+\s*\d+ , используемое в этом примере, проверяет, что входная строка имеет символ валюты и по крайней мере одну десятичную цифру. Шаблон определен, как показано в следующей таблице:

Расписание Description
\p{Sc}+ Соответствует одному или нескольким символам в категории символов Юникода, валюта.
\s* Соответствует нулю или нескольким символам пробела.
\d+ Соответствует одному или нескольким десятичным цифрам.

Интерпретированные и скомпилированные регулярные выражения

Шаблоны регулярных выражений, которые не привязаны к обработчику регулярных выражений с помощью спецификации Compiled параметра, интерпретируются. При создании объекта регулярного выражения обработчик регулярных выражений преобразует регулярное выражение в набор кодов операций. При вызове метода экземпляра коды операций преобразуются в CIL и выполняются компилятором JIT. Аналогичным образом, когда вызывается метод статического регулярного выражения и не удается найти регулярное выражение в кэше, обработчик регулярных выражений преобразует регулярное выражение в набор кодов операций и сохраняет их в кэше. Затем он преобразует эти коды операций в CIL, чтобы компилятор JIT смог их выполнить. Интерпретированные регулярные выражения снижают время запуска ценой более медленного выполнения. Из-за этого процесса они лучше всего используются, если регулярное выражение используется в небольшом количестве вызовов методов, или если точное количество вызовов методов регулярного выражения неизвестно, но ожидается, что оно будет небольшим. По мере увеличения числа вызовов методов выгоду по производительности от быстрого запуска перевешивает низкая скорость выполнения.

Шаблоны регулярных выражений, привязанные к обработчику регулярных выражений указанием параметра Compiled, компилируются. Поэтому при создании экземпляра объекта регулярного выражения или при вызове метода статического регулярного выражения и не удается найти регулярное выражение в кэше, подсистема регулярных выражений преобразует регулярное выражение в промежуточный набор кодов операций. Затем эти коды преобразуются в CIL. При вызове метода компилятор JIT выполняет CIL. В отличие от интерпретированных регулярных выражений, скомпилированные регулярные выражения увеличивают время запуска, но позволяют выполнять отдельные методы поиска совпадения с шаблоном быстрее. В результате выгода по производительности от скомпилированных регулярных выражений увеличивается пропорционально числу вызовов методов регулярных выражений.

Подводя итог, мы рекомендуем использовать интерпретированные регулярные выражения, когда методы регулярного выражения с конкретным регулярным выражением вызываются относительно редко. Скомпилированные регулярные выражения следует использовать, когда методы регулярного выражения с конкретным регулярным выражением вызываются относительно часто. Трудно определить точное пороговое значение, при котором более медленные скорости выполнения интерпретируемых регулярных выражений перевешивают преимущества от снижения времени запуска, или пороговое значение, при котором более медленное время запуска скомпилированных регулярных выражений перевешивает преимущества от скорости выполнения. Он зависит от различных факторов, включая сложность регулярного выражения и конкретных данных, которые он обрабатывает. Чтобы определить, что позволит добиться лучшей производительности — интерпретированные или скомпилированные регулярные выражения, — можно использовать класс Stopwatch, чтобы сравнить время выполнения в обоих случаях.

В следующем примере сравнивается производительность скомпилированных и интерпретированных регулярных выражений при чтении первых 10 предложений и при чтении всех предложений в тексте Theodore Dreiser The Financier. Как показано в выходных данных из примера, при выполнении только 10 вызовов к методам сопоставления регулярных выражений интерпретированное регулярное выражение обеспечивает лучшую производительность, чем скомпилированное регулярное выражение. Однако скомпилированное регулярное выражение обеспечивает более высокую производительность при большом числе вызовов (в данном случае 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

Шаблон регулярного выражения, используемый в примере, \b(\w+((\r?\n)|,?\s))*\w+[.?:;!]определяется, как показано в следующей таблице:

Расписание Description
\b Совпадение должно начинаться на границе слова.
\w+ Соответствует одному или нескольким символам слова.
(\r?\n)|,?\s) Соответствует либо нулю, либо одному возврату каретки, за которым следует символ новой строки, либо ноль или одна запятая, за которой следует пробел.
(\w+((\r?\n)|,?\s))* Соответствует нулю или нескольким вхождениям одного или нескольких символов слов, за которым следует либо нулевая, либо один возвращаемый знак каретки, либо нулевым или одной запятой, за которой следует символ пробела.
\w+ Соответствует одному или нескольким символам слова.
[.?:;!] Соответствует периоду, вопросительный знак, двоеточие, точка с запятой или восклицательный знак.

Регулярные выражения: компиляция в сборку

Также .NET позволяет создать сборку, которая содержит скомпилированные регулярные выражения. Эта возможность перемещает удар производительности компиляции регулярных выражений с момента выполнения до времени разработки. Однако она также включает в себя некоторую дополнительную работу. Необходимо заранее определить регулярные выражения и скомпилировать их в сборку. Затем компилятор может сослаться на эту сборку при компиляции исходного кода, использующего регулярные выражения из сборки. Каждое скомпилированное регулярное выражение в сборке представлено классом, унаследованным от Regex.

Чтобы скомпилировать регулярные выражения в сборку, вызовите Regex.CompileToAssembly(RegexCompilationInfo[], AssemblyName) метод и передайте его массив RegexCompilationInfo объектов и AssemblyName объекта. Объекты RegexCompilationInfo представляют скомпилированные регулярные выражения и AssemblyName объект, содержащий сведения о созданной сборке.

Рекомендуется компилировать регулярные выражения в сборку в следующих случаях:

  • Если вы являетесь разработчиком компонентов, который хочет создать библиотеку повторно используемых регулярных выражений.
  • Если вы ожидаете, что методы сопоставления шаблонов регулярного выражения будут вызываться неопределенным числом раз— от одного или двух до тысяч или десятков тысяч раз. В отличие от скомпилированных или интерпретированных регулярных выражений регулярные выражения, которые компилируются в отдельные сборки, обеспечивают производительность, согласованную независимо от количества вызовов методов.

Если вы используете скомпилированные регулярные выражения для оптимизации производительности, не следует использовать отражение для создания сборки, загрузки обработчика регулярных выражений и выполнения методов сопоставления шаблонов. Избегая отражения, требуется, чтобы не создавать шаблоны регулярных выражений динамически и указывать любые параметры сопоставления шаблонов, например сопоставление шаблонов без учета регистра, во время создания сборки. Также необходимо отделить код, создающий сборку, от кода, использующего регулярное выражение.

В следующем примере показано, как создать сборку, содержащую скомпилированное регулярное выражение. Он создает сборку RegexLib.dll с одним классом SentencePatternрегулярных выражений. Этот класс содержит шаблон регулярного выражения, соответствующий предложению, используемый в разделе "Интерпретированные и скомпилированные регулярные выражения ".

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

Когда код примера компилируется в исполняемый файл и выполняется, создается сборка с именем RegexLib.dll. Класс, Utilities.RegularExpressions.SentencePattern производный от Regex регулярного выражения. В следующем примере используется скомпилированное регулярное выражение для извлечения предложений из текста Финансиста Теодора Дрейзера:

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.

Грамотное использование поиска с возвратом

Обычно обработчик регулярных выражений двигается по входной строке линейным образом, сравнивая ее с шаблоном регулярного выражения. Однако если неопределенные квантификаторы, такие как *, +и используются в шаблоне регулярного выражения, подсистема регулярных выражений может отказаться от части успешных частичных совпадений и ? вернуться в ранее сохраненное состояние, чтобы найти успешное совпадение для всего шаблона. Этот процесс известен как поиск с возвратом.

Совет

Дополнительные сведения о обратном отслеживании см. в разделе "Сведения о поведении регулярных выражений" и обратном отслеживании. Подробные сведения о обратном отслеживании см. в статьях об улучшениях регулярного выражения в .NET 7 и оптимизации производительности регулярных выражений.

Поддержка поиска с возвратом обеспечивает мощность и гибкость регулярных выражений. При этом ответственность за управление работой обработчика регулярных выражений лежит на разработчике регулярных выражений. Поскольку разработчики часто не отдают себе отчет в этой ответственности, неправильное или излишнее использование поиска с возвратом часто становится причиной снижения производительности регулярных выражений. В самом неблагоприятном случае время обработки может удваиваться при каждом добавлении символа во входную строку. На самом деле, используя чрезмерное отслеживание, легко создать программный эквивалент бесконечного цикла, если входные данные почти совпадают с шаблоном регулярного выражения. Обработчик регулярных выражений может занять несколько часов или даже дней для обработки относительно короткой входной строки.

Часто приложения платят штраф за производительность за использование обратного отслеживания, даже если обратная дорожка не является важной для матча. Например, регулярное выражение \b\p{Lu}\w*\b соответствует всем словам, начинающимся с верхнего регистра, как показано в следующей таблице:

Расписание Description
\b Совпадение должно начинаться на границе слова.
\p{Lu} Соответствует верхнему регистру.
\w* Соответствует нулю или нескольким символам слова.
\b Совпадение должно заканчиваться на границе слова.

Так как граница слова не совпадает с подмножеством или подмножеством символа слова, нет возможности, что обработчик регулярных выражений пересекает границу слова при сопоставлении символов слов. Таким образом, для этого регулярного выражения обратная дорожка никогда не может способствовать общему успеху любого совпадения. Это может снизить производительность, так как подсистема регулярных выражений вынуждена сохранять состояние для каждого успешного предварительного совпадения символа слова.

Если вы определите, что обратная дорожка не требуется, ее можно отключить несколькими способами:

  • Задав параметр (представленный RegexOptions.NonBacktracking в .NET 7). Дополнительные сведения см. в режиме nonbacktracking.

  • С помощью элемента языка, известного (?>subexpression) как атомарная группа. В следующем примере производится анализ входной строки с использованием двух регулярных выражений. Функционирование первого регулярного выражения, \b\p{Lu}\w*\b, основано на поиске с возвратом. Второе регулярное выражение, \b\p{Lu}(?>\w*)\b, отключает поиск с возвратом. Как показано в выходных данных из примера, оба они создают один и тот же результат:

    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
    

Часто поиск с возвратом очень важен для поиска во входной строке совпадения с шаблоном регулярного выражения. Тем не менее избыточное использование поиска с возвратом может сильно снизить производительность и создать впечатление, что приложение перестало отвечать. В частности, эта проблема возникает при вложенных квантификаторах, а текст, соответствующий внешнему подтексту, является подмножеством текста, соответствующего внутреннему подтексту.

Предупреждение

Помимо предотвращения чрезмерного обратного отслеживания, следует использовать функцию времени ожидания, чтобы убедиться, что чрезмерная обратная дорожка не снижает производительность регулярных выражений. Дополнительные сведения см. в разделе Использование значений времени ожидания.

Например, шаблон регулярного выражения ^[0-9A-Z]([-.\w]*[0-9A-Z])*\$$ должен искать номер части, состоящий из по крайней мере одного алфавитно-цифрового символа. Дополнительные символы могут включать в себя алфавитно-цифровые символы, дефис, подчеркивание или точку, но последний символ должен быть алфавитно-цифровым. Знак доллара завершает номер части. В некоторых случаях этот шаблон регулярных выражений может иметь низкую производительность, так как квантификаторы вложены, и поскольку подтекст является подмножеством подтекстов [0-9A-Z][-.\w]*.

В таких случаях можно оптимизировать производительность, удалив вложенные квантификаторы и заменив внешнюю часть выражения утверждением просмотра вперед или назад нулевой ширины. Утверждения Lookahead и lookbehind являются привязками. Они не перемещают указатель в входной строке, а вместо этого смотрят вперед или позади, чтобы проверка соответствует ли указанное условие. Например, регулярное выражение для поиска номера части можно записать следующим образом: ^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$. Этот шаблон регулярного выражения определен, как показано в следующей таблице:

Расписание Description
^ Начало совпадения в начале входной строки.
[0-9A-Z] Совпадение с алфавитно-цифровым символом. Номер части должен состоять по меньшей мере из этого символа.
[-.\w]* Совпадение с нулем или большим числом вхождений любого символа слова, дефиса или точки.
\$ Совпадение со знаком доллара.
(?<=[0-9A-Z]) Обратите внимание на конечный знак доллара, чтобы убедиться, что предыдущий символ буквенно-цифровой.
$ Совпадение должно заканчиваться в конце входной строки.

В следующем примере показано использование этого регулярного выражения для сопоставления массива, содержащего возможные номера частей:

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.

Язык регулярных выражений в .NET включает следующие языковые элементы, которые можно использовать для исключения вложенных квантификаторов. Дополнительные сведения см. в разделе "Конструкции группирования".

Элемент языка Description
(?= subexpression ) Положительный просмотр вперед нулевой ширины. Просматривает текущую позицию, чтобы определить, соответствует ли subexpression входная строка.
(?! subexpression ) Отрицательный просмотр вперед нулевой ширины. Просматривает текущую позицию, чтобы определить, не соответствует ли subexpression входная строка.
(?<= subexpression ) Положительный просмотр назад нулевой ширины. Просматривает текущую позицию, чтобы определить, соответствует ли subexpression входная строка.
(?<! subexpression ) Отрицательный просмотр назад нулевой ширины. Просматривает текущую позицию, чтобы определить, не соответствует ли subexpression входная строка.

Использование значений времени ожидания

Если регулярные выражения обрабатывают входные данные, которые почти совпадают с шаблоном регулярного выражения, зачастую они могут использовать избыточный поиск с возвратом, что сильно влияет на их производительность. Помимо тщательного рассмотрения использования обратного отслеживания и тестирования регулярного выражения на соответствие входным данным, всегда следует задать значение времени ожидания, чтобы свести к минимуму эффект чрезмерной обратной дорожки, если это происходит.

Интервал времени ожидания регулярного выражения определяет период времени, когда обработчик регулярных выражений будет искать одно совпадение до истечения времени ожидания. В зависимости от шаблона регулярного выражения и входного текста время выполнения может превышать указанный интервал времени ожидания, но он не будет тратить больше времени обратного отслеживания, чем указанный интервал времени ожидания. Интервал времени ожидания по умолчанию — это Regex.InfiniteMatchTimeoutозначает, что регулярное выражение не истекает. Это значение можно переопределить и определить интервал времени ожидания следующим образом:

  • Regex(String, RegexOptions, TimeSpan) Вызовите конструктор, чтобы указать значение времени ожидания при создании экземпляра Regex объекта.

  • Вызовите статический метод сопоставления шаблонов, например Regex.Match(String, String, RegexOptions, TimeSpan) или Regex.Replace(String, String, String, RegexOptions, TimeSpan), который включает matchTimeout параметр.

  • Вызовите конструктор, имеющий параметр типа TimeSpan для скомпилированных регулярных выражений, созданных путем вызова Regex.CompileToAssembly метода.

  • Задайте значение на уровне процесса или AppDomain с помощью кода, например AppDomain.CurrentDomain.SetData("REGEX_DEFAULT_MATCH_TIMEOUT", TimeSpan.FromMilliseconds(100));.

Если вы определили интервал времени ожидания и совпадение не найдено в конце этого интервала, метод регулярного выражения создает RegexMatchTimeoutException исключение. В обработчике исключений можно повторить совпадение с длительным интервалом времени ожидания, отказаться от попытки сопоставления и предположить, что совпадения нет, или отказаться от попытки сопоставления и записать сведения об исключении для дальнейшего анализа.

В следующем примере определяется метод GetWordData, который создает регулярное выражение с интервалом времени ожидания 350 миллисекунд, чтобы вычислить количество слов и среднее количество символов в слове в текстовом документе. Если время ожидания операции сопоставления истекло, интервал времени ожидания увеличивается на 350 миллисекунд, а Regex объект повторно создается. Если новый интервал времени ожидания превышает одну секунду, метод повторно переронит исключение вызывающей стороны.

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

Захват только в случае необходимости

Регулярные выражения в .NET поддерживают конструкции группирования, которые позволяют группировать шаблон регулярного выражения в одну или несколько вложенных выражений. Наиболее часто в языке регулярных выражений .NET используются конструкции группирования (часть_выражения), которая определяет нумерованную группу записи, и (?<имя>часть_выражения), которая определяет именованную группу записи. Конструкции группирования крайне важны для создания обратных ссылок и для определения части выражения, к которой должен применяться квантификатор.

Однако использование этих языковых элементов имеет последствия. Они приводят к тому, что объект, GroupCollection возвращаемый Match.Groups свойством, заполняется последними неименованными или именованными захватами. Если одна конструкция группирования захватила несколько подстроок во входной строке, они также заполняют CaptureCollection объект, возвращаемый Group.Captures свойством определенной группы записи с несколькими Capture объектами.

Часто конструкции группировки используются в регулярном выражении только таким образом, чтобы к ним могли применяться квантификаторы. Группы, захваченные этими вложенными выражениями, не используются позже. Например, регулярное выражение \b(\w+[;,]?\s?)+[.?!] предназначено для записи всего предложения. В следующей таблице описываются элементы языка в этом шаблоне регулярных выражений и их влияние на Match коллекции и Group.Captures объектыMatch.Groups:

Расписание Description
\b Совпадение должно начинаться на границе слова.
\w+ Соответствует одному или нескольким символам слова.
[;,]? Соответствует нулю или одной запятой или точке с запятой.
\s? Соответствует нулю или одному символу пробела.
(\w+[;,]?\s?)+ Соответствует одному или нескольким вхождениям одного или нескольких символов слова, за которым следует необязательная запятая или точка с запятой, за которым следует необязательный символ пробела. Этот шаблон определяет первую группу записи, которая необходима, чтобы сочетание нескольких символов слов (то есть слово), за которым следует необязательный символ препинания, будет повторяться до тех пор, пока обработчик регулярных выражений не достигнет конца предложения.
[.?!] Соответствует периоду, вопросительный знак или восклицательный знак.

Как показано в следующем примере, когда совпадение найдено, объекты GroupCollection и CaptureCollection заполняются захваченными объектами из совпадения. В этом случае используется группа записи (\w+[;,]?\s?), чтобы к ней можно было применить квантификатор +, который позволяет шаблону регулярного выражения сопоставить каждое слово в предложении. В противном случае было бы найдено совпадение только с последним словом предложения.

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.

При использовании вложенных выражений только для применения квантификаторов к ним, и вы не заинтересованы в захваченном тексте, следует отключить записи групп. Например, языковой элемент (?:subexpression) запрещает группе, к которой он применен, захватывать совпавшие подстроки. В следующем примере шаблон регулярного выражения из предыдущего примера изменен на \b(?:\w+[;,]?\s?)+[.?!]. Как показано в выходных данных, модуль регулярных выражений не заполняет GroupCollection и 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.

Отключить захват можно одним из следующих способов:

  • Используйте языковой элемент (?:subexpression). Этот элемент отключает захват совпавших подстрок в группе, к которой он применен. Он не отключает записи подстроки в вложенных группах.

  • Использовать параметр ExplicitCapture. Он отключает все неименованные и неявные захваты для шаблона регулярных выражений. При использовании этого параметра захват выполняется только для тех подстрок, которые совпадают с именованными группами, определенными с помощью языкового элемента (?<name>subexpression). Флажок ExplicitCapture можно передать в параметр options конструктора класса Regex или в параметр options статического метода поиска совпадения Regex.

  • Использовать параметр n в языковом элементе (?imnsx). Этот параметр отключает все неименованные или неявные захваты, начиная с того места, на котором находится этот элемент в шаблоне регулярного выражения. Захват отключается либо до конца шаблона, либо пока захват для неименованных и неявных объектов не будет включен параметром (-n). Дополнительные сведения см. в разделе Прочие конструкции.

  • Использовать параметр n в языковом элементе (?imnsx:subexpression). Этот параметр отключает все неименованные и неявные захваты в части выражения subexpression. Захваты для всех вложенных неименованных и неявных групп также отключаются.

Заголовок Description
Подробные сведения о поведении регулярных выражений Описание реализации обработчика регулярных выражений в .NET. В этой статье рассматривается гибкость регулярных выражений и объясняется ответственность разработчика за обеспечение эффективной и надежной работы подсистемы регулярных выражений.
Поиск с возвратом Описание поиска с возвратом и того, как он влияет на производительность регулярных выражений. Описание языковых элементов, которые можно использовать вместо поиска с возвратом.
Элементы языка регулярных выражений — краткий справочник Описание элементов языка регулярных выражений в .NET и ссылки на подробную документацию для каждого языкового элемента.