.NET Framework 中的正则表达式的最佳做法

更新:2011 年 3 月

.NET Framework 中的正则表达式引擎是一种功能强大而齐全的工具,它基于模式匹配(而不是比较和匹配文本)来处理文本。 在大多数情况下,它可以快速、高效地执行模式匹配。 但在某些情况下,正则表达式引擎的速度似乎很慢。 在极端情况下,它甚至看似停止响应,因为它会用若干个小时甚至若干天处理相对小的输入。

本主题概述开发人员为了确保其正则表达式实现最佳性能可以采纳的一些最佳做法。 它包含下列部分:

  • 考虑输入源

  • 适当处理对象实例化

  • 控制回溯

  • 只在必要时捕获

  • 相关主题

考虑输入源

通常,正则表达式可接受两种类型的输入:受约束的输入或不受约束的输入。 受约束的输入是源自已知或可靠的源并遵循预定义格式的文本。 不受约束的输入是源自不可靠的源(如 Web 用户)并且可能不遵循预定义或预期格式的文本。

编写的正则表达式模式的目的通常是匹配有效输入。 也就是说,开发人员检查他们要匹配的文本,然后编写与其匹配的正则表达式模式。 然后,开发人员使用多个有效输入项进行测试,以确定此模式是否需要更正或进一步细化。 当模式可匹配所有假定的有效输入时,则将其声明为生产就绪并且可包括在发布的应用程序中。 这使得正则表达式模式适合匹配受约束的输入。 但它不适合匹配不受约束的输入。

若要匹配不受约束的输入,正则表达式必须能够高效处理以下三种文本:

  • 与正则表达式模式匹配的文本。

  • 与正则表达式模式不匹配的文本。

  • 与正则表达式模式大致匹配的文本。

对于为了处理受约束的输入而编写的正则表达式,最后一种文本类型尤其存在问题。 如果该正则表达式还依赖大量回溯,则正则表达式引擎可能会花费大量时间(在有些情况下,需要许多个小时或许多天)来处理看似无害的文本。

例如,考虑一种很常用但很有问题的用于验证电子邮件地址别名的正则表达式。 编写正则表达式 ^[0-9A-Z]([-.\w]*[0-9A-Z])*$ 的目的是,处理被视为有效的电子邮件地址,该地址由字母数字字符或下划线构成,后跟零个或多个可为字母数字、句点、下划线或连字符的字符。 该正则表达式必须以字母数字字符或下划线结束。 但正如下面的示例所示,尽管此正则表达式可以轻松处理有效输入,但在处理接近有效的输入时性能非常低效。

Imports System.Diagnostics
Imports System.Text.RegularExpressions

Module Example
   Public Sub Main()
      Dim sw As Stopwatch    
      Dim addresses() As String = { "AAAAAAAAAAA@anyco.com", 
                                 "AAAAAAAAAAaaaaaaaaaa!@anyco.com" }
      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
using System;
using System.Diagnostics;
using System.Text.RegularExpressions;

public class Example
{
   public static void Main()
   {
      Stopwatch sw;    
      string[] addresses = { "AAAAAAAAAAA@anyco.com", 
                             "AAAAAAAAAAaaaaaaaaaa!@anyco.com" };
      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

如该示例输出所示,正则表达式引擎处理有效电子邮件别名的时间间隔大致相同,与其长度无关。 另一方面,当接近有效的电子邮件地址包含五个以上字符时,字符串中每增加一个字符,处理时间会大约增加一倍。 这意味着,处理接近有效的 28 个字符构成的字符串将需要一个小时,处理接近有效的 33 个字符构成的字符串将需要接近一天的时间。

由于开发此正则表达式时只考虑了要匹配的输入的格式,因此未能考虑与模式不匹配的输入。 这反过来会使与正则表达式模式近似匹配的不受约束输入的性能显著降低。

若要解决此问题,可执行下列操作:

  • 开发模式时,应考虑回溯对正则表达式引擎的性能的影响程度,特别是当正则表达式设计用于处理不受约束的输入时。 有关更多信息,请参见控制回溯部分。

  • 使用无效输入、接近有效的输入以及有效输入对正则表达式进行完全测试。 若要为特定正则表达式随机生成输入,可以使用 Rex,这是 Microsoft Research 提供的正则表达式探索工具。

适当处理对象实例化

.NET Framework 的正则表达式对象模型的核心是 System.Text.RegularExpressions.Regex 类,它表示正则表达式引擎。 通常,影响正则表达式性能的单个最大因素是 Regex 引擎的使用方式。 定义正则表达式需要将正则表达式引擎与正则表达式模式紧密耦合。 无论该耦合过程是需要通过向其构造函数传递正则表达式模式来实例化 Regex 还是通过向其传递正则表达式模式和要分析的字符串来调用静态方法,都必然会消耗大量资源。

注意注意

有关使用已解释并已编译的正则表达式的性能影响的详细讨论,请参见 BCL 团队博客中的 Optimizing Regular Expression Performance, Part II: Taking Charge of Backtracking(优化正则表达式性能,第 II 部分:控制回溯)。

可将正则表达式引擎与特定正则表达式模式耦合,然后使用该引擎以若干种方式匹配文本:

  • 可以调用静态模式匹配方法,如 Regex.Match(String, String)。 这不需要实例化正则表达式对象。

  • 可以实例化一个 Regex 对象并调用已解释的正则表达式的实例模式匹配方法。 这是将正则表达式引擎绑定到正则表达式模式的默认方法。 如果实例化 Regex 对象时未使用包括 Compiled 标记的 options 参数,则会生成此方法。

  • 可以实例化一个 Regex 对象并调用已编译的正则表达式的实例模式匹配方法。 当使用包括 Compiled 标记的 options 参数实例化 Regex 对象时,正则表达式对象表示已编译的模式。

  • 可以创建一个与特定正则表达式模式紧密耦合的特殊用途的 Regex 对象,编译该对象,并将其保存到独立程序集中。 为此,可调用 Regex.CompileToAssembly 方法。

这种调用正则表达式匹配方法的特殊方式会对应用程序产生显著影响。 以下各节讨论何时使用静态方法调用、已解释的正则表达式和已编译的正则表达式,以改进应用程序的性能。

重要说明重要事项

如果方法调用中重复使用同一正则表达式或者应用程序大量使用正则表达式对象,则方法调用的形式(静态、已解释、已编译)会影响性能。

静态正则表达式

建议将静态正则表达式方法用作使用同一正则表达式重复实例化正则表达式对象的替代方法。 与正则表达式对象使用的正则表达式模式不同,实例方法调用所使用的模式中的操作代码或已编译的 Microsoft 中间语言 (MSIL) 由正则表达式引擎缓存在内部。

例如,事件处理程序会频繁调用其他方法来验证用户输入。 下面的代码中反映了这一点,其中一个 Button 控件的 Click 事件用于调用名为 IsValidCurrency 的方法,该方法检查用户是否输入了后跟至少一个十进制数的货币符号。

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
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.";
}

下面的示例显示 IsValidCurrency 方法的一个非常低效的实现。 请注意,每个方法调用使用相同模式重新实例化 Regex 对象。 这反过来意味着,每次调用该方法时,都必须重新编译正则表达式模式。

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
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);
   }
}

应将此低效代码替换为对静态 Regex.IsMatch(String, String) 方法的调用。 这样便不必在您每次要调用模式匹配方法时都实例化 Regex 对象,还允许正则表达式引擎从其缓存中检索正则表达式的已编译版本。

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
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); 
   }
}

默认情况下,将缓存最后 15 个最近使用的静态正则表达式模式。 对于需要大量已缓存的静态正则表达式的应用程序,可通过设置 Regex.CacheSize 属性来调整缓存大小。

此示例中使用的正则表达式 \p{Sc}+\s*\d+ 可验证输入字符串是否包含一个货币符号和至少一个十进制数。 模式的定义如下表所示。

模式

说明

\p{Sc}+

与 Unicode 符号、货币类别中的一个或多个字符匹配。

\s*

匹配零个或多个空白字符。

\d+

匹配一个或多个十进制数字。

已解释的与已编译的正则表达式

将解释未通过 Compiled 选项的规范绑定到正则表达式引擎的正则表达式模式。 在实例化正则表达式对象时,正则表达式引擎会将正则表达式转换为一组操作代码。 调用实例方法时,操作代码会转换为 MSIL 并由 JIT 编译器执行。 同样,当调用一种静态正则表达式方法并且在缓存中找不到该正则表达式时,正则表达式引擎会将该正则表达式转换为一组操作代码并将其存储在缓存中。 然后,它将这些操作代码转换为 MSIL,以便于 JIT 编译器执行。 已解释的正则表达式会减少启动时间,但会使执行速度变慢。 因此,在少数方法调用中使用正则表达式时或调用正则表达式方法的确切数量未知但预期很小时,使用已解释的正则表达式的效果最佳。 随着方法调用数量的增加,执行速度变慢对性能的影响会超过减少启动时间带来的性能改进。

将编译未通过 Compiled 选项的规范绑定到正则表达式引擎的正则表达式模式。 这意味着,当实例化正则表达式对象时或当调用一种静态正则表达式方法并且在缓存中找不到该正则表达式时,正则表达式引擎会将该正则表达式转换为一组中间操作代码,这些代码之后会转换为 MSIL。 调用方法时,JIT 编译器将执行该 MSIL。 与已解释的正则表达式相比,已编译的正则表达式增加了启动时间,但执行各种模式匹配方法的速度更快。 因此,相对于调用的正则表达式方法的数量,因编译正则表达式而产生的性能产生了改进。

简言之,当您使用特定正则表达式调用正则表达式方法相对不频繁时,建议使用已解释的正则表达式。 当您使用特定正则表达式调用正则表达式方法相对频繁时,应使用已编译的正则表达式。 很难确定已解释的正则表达式执行速度减慢超出启动时间减少带来的性能增益的确切阈值,或已编译的正则表达式启动速度减慢超出执行速度加快带来的性能增益的阈值。 这依赖于各种因素,包括正则表达式的复杂程度和它处理的特定数据。 若要确定已解释或已编译的正则表达式是否可为特定应用程序方案提供最佳性能,可以使用 Stopwatch 类来比较其执行时间。

下面的示例在读取 Theodore Dreiser 的“财务”中的前十个句子和读取所有句子时比较已编译和已解释的正则表达式的性能。 如示例输出所示,当只对匹配方法的正则表达式进行十次调用时,已解释的正则表达式与已编译的正则表达式相比,可提供更好的性能。 但是,当进行大量调用(在此示例中,超过 13,000 次调用)时,已编译的正则表达式可提供更好的性能。

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 sentnces 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
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 sentnces 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

该示例中使用的正则表达式模式 \b(\w+((\r?\n)|,?\s))*\w+[.?:;!] 的定义如下表所示。

模式

说明

\b

在单词边界处开始匹配。

\w+

匹配一个或多个单词字符。

(\r? \n)|,? \s)

匹配零个或一个回车符后跟一个换行符,或零个或一个逗号后跟一个空白字符。

(\w+((\r? \n)|,? \s))*

匹配一个或多个单词字符的零个或多个事例,后跟零个或一个回车符和换行符,或后跟零个或一个逗号、一个空格字符。

\w+

匹配一个或多个单词字符。

[.?:;!]

匹配句号、问号、冒号、分号或感叹号。

正则表达式:编译为程序集

.NET Framework 还允许您创建一个包含已编译的正则表达式的程序集。 这样会将正则表达式编译对性能造成的影响从运行时转移到设计时。 但是,这还涉及一些其他工作:必须提前定义正则表达式并将其编译为程序集。 然后,编译器在编译使用该程序集的正则表达式的源代码时,可以引用此程序集。 程序集内的每个已编译正则表达式都由从 Regex 派生的类来表示。

若要将正则表达式编译为程序集,可调用 Regex.CompileToAssembly(RegexCompilationInfo[], AssemblyName) 方法并向其传递表示要编译的正则表达式的 RegexCompilationInfo 对象数组和包含有关要创建的程序集的信息的 AssemblyName 对象。

建议您在以下情况下将正则表达式编译为程序集:

  • 如果您是要创建可重用正则表达式库的组件开发人员。

  • 如果您预期正则表达式的模式匹配方法要被调用的次数无法确定 -- 从任意位置,次数可能为一次两次到上千上万次。 与已编译或已解释的正则表达式不同,编译为单独程序集的正则表达式可提供与方法调用数量无关的一致性能。

如果使用已编译的正则表达式来优化性能,则不应使用反射来创建程序集,加载正则表达式引擎并执行其模式匹配方法。 这要求您避免动态生成正则表达式模式,并且要在创建程序集时指定模式匹配选项(如不区分大小写的模式匹配)。 它还要求将创建程序集的代码与使用正则表达式的代码分离。

下面的示例演示如何创建包含已编译的正则表达式的程序集。 它用单个正则表达式类 SentencePattern 创建一个名为 RegexLib.dll 的程序集,其中包含已解释与已编译的正则表达式部分中使用的句子匹配的正则表达式模式。

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
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);
   }
}

在将示例编译为可执行文件并运行时,它会创建一个名为 RegexLib.dll 的程序集。 正则表达式用名为 Utilities.RegularExpressions.SentencePattern 并由 Regex 派生的类来表示。 然后,下面的示例使用已编译的正则表达式从 Theodore Dreiser 的“财务”文本中提取句子。

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.
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.

控制回溯

通常,正则表达式引擎使用线性进度在输入字符串中移动并将其编译为正则表达式模式。 但是,当在正则表达式模式中使用不确定限定符(如 *、+ 和 ?)时,正则表达式引擎可能会放弃一部分成功的分部匹配,并返回以前保存的状态,以便为整个模式搜索成功匹配。 此过程称为回溯。

注意注意

有关回溯的更多信息,请参见正则表达式行为的详细信息回溯。有关回溯的详细讨论,请参见 BCL 团队博客中的 Optimizing Regular Expression Performance, Part II: Taking Charge of Backtracking(优化正则表达式性能,第 II 部分:控制回溯)。

支持回溯可为正则表达式提供强大的功能和灵活性。 还可将控制正则表达式引擎操作的职责交给正则表达式开发人员来处理。 由于开发人员通常不了解此职责,因此其误用回溯或依赖过多回溯通常会显著降低正则表达式的性能。 在最糟糕的情况下,输入字符串中每增加一个字符,执行时间会加倍。 实际上,如果过多使用回溯,则在输入与正则表达式模式近似匹配时很容易创建无限循环的编程等效形式;正则表达式引擎可能需要几小时甚至几天来处理相对短的输入字符串。

通常,尽管回溯不是匹配所必需的,但应用程序会因使用回溯而对性能产生负面影响。 例如,正则表达式 \b\p{Lu}\w*\b 将匹配以大写字符开头的所有单词,如下表所示。

模式

说明

\b

在单词边界处开始匹配。

\p{Lu}

匹配大写字符。

\w*

匹配零个或多个单词字符。

\b

在单词边界处结束匹配。

由于单词边界与单词字符不同也不是其子集,因此正则表达式引擎在匹配单词字符时无法跨越单词边界。 这意味着,对于此正则表达式而言,回溯对任何匹配的总体成功不会有任何贡献 -- 由于正则表达式引擎被强制为单词字符的每个成功的初步匹配保存其状态,因此它只会降低性能。

如果您确定不需要回溯,则可使用 (?>subexpression) 语言元素将其禁用。 下面的示例通过使用两个正则表达式来分析输入字符串。 第一个正则表达式 \b\p{Lu}\w*\b 依赖于回溯。 第二个正则表达式 \b\p{Lu}(?>\w*)\b 禁用回溯。 如示例输出所示,这两个正则表达式产生的结果相同。

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

在许多情况下,在将正则表达式模式与输入文本匹配时,回溯很重要。 在这些情况下,过量回溯会严重降低性能,并且会产生应用程序已停止响应的感觉。 特别需要指出的是,当嵌套限定符并且与外部子表达式匹配的文本为与内部子表达式匹配的文本的子集时,尤其会出现这种情况。

例如,正则表达式模式 ^[0-9A-Z]([-.\w]*[0-9A-Z])*\$$ 用于匹配至少包括一个字母数字字符的部件号。 任何附加字符可以包含字母数字字符、连字符、下划线或句号,但最后一个字符必须为字母数字。 美元符号用于终止部件号。 在某些情况下,由于限定符嵌套并且子表达式 [0-9A-Z] 是子表达式 [-.\w]* 的子集,因此此正则表达式模式会表现出极差的性能。

在这些情况下,可通过移除嵌套限定符并将外部子表达式替换为零宽度预测先行和回顾断言来优化正则表达式性能。 预测先行和回顾断言是定位点;它们不在输入字符串中移动指针,而是通过预测先行或回顾来检查是否满足指定条件。 例如,可将部件号正则表达式重写为 ^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$。 此正则表达式模式的定义如下表所示。

模式

说明

^

从输入字符串的开头部分开始匹配。

[0-9A-Z]

匹配字母数字字符。 部件号至少要包含此字符。

[-. \w]*

匹配零个或多个任意单词字符、连字符或句号。

\$

匹配美元符号。

(?<=[0-9A-Z])

查看作为结束的美元符号,以确保前一个字符是字母数字。

$

在输入字符串末尾结束匹配。

下面的示例演示了如何使用此正则表达式来匹配包含可能部件号的数组。

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.
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.

.NET Framework 中的正则表达式语言包括以下可用于消除嵌套限定符的语言元素。 有关更多信息,请参见分组构造

语言元素

说明

(?=subexpression)

零宽度正预测先行。 查看当前位置,以确定 subexpression 是否与输入字符串匹配。

(?!subexpression)

零宽度负预测先行。 查看当前位置,以确定 subexpression 是否不与输入字符串匹配。

(?<=subexpression)

零宽度正回顾。 回顾当前位置,以确定 subexpression 是否与输入字符串匹配。

(?<!subexpression)

零宽度负回顾。 回顾当前位置,以确定 subexpression 是否不与输入字符串匹配。

只在必要时捕获

.NET Framework 中的正则表达式支持许多分组构造,这允许您将正则表达式模式分组为一个或多个子表达式。 .NET Framework 正则表达式语言中最常用的分组构造为 (子表达式)(用于定义编号的捕获组)和 (?<名称>子表达式)(用于定义命名的捕获组)。 分组构造是创建反向引用和定义要应用限定符的子表达式时所必需的。

但是,使用这些语言元素会产生一定的开销。 它们会导致用最近的未命名或已命名捕获来填充 Match.Groups 属性返回的 GroupCollection 对象,如果单个分组构造已捕获输入字符串中的多个子字符串,则还会填充包含多个 Capture 对象的特定捕获组的 Group.Captures 属性返回的 CaptureCollection 对象。

通常,只在正则表达式中使用分组构造,这样可对其应用限定符,而且以后不会使用这些子表达式捕获的组。 例如,正则表达式 \b(\w+[;,]?\s?)+[.?!] 用于捕获整个句子。 下表描述了此正则表达式模式中的语言元素及其对 Match 对象的 Match.GroupsGroup.Captures 集合的影响。

模式

说明

\b

在单词边界处开始匹配。

\w+

匹配一个或多个单词字符。

[;,]?

匹配零个或一个逗号或分号。

\s?

匹配零个或一个空白字符。

(\w+[;,]? \s?)+

匹配以下一个或多个事例:一个或多个单词字符,后跟一个可选逗号或分号,一个可选的空白字符。 用于定义第一个捕获组,它是必需的,以便将重复多个单词字符的组合(即单词)后跟可选标点符号,直至正则表达式引擎到达句子末尾。

[.?!]

匹配句号、问号或感叹号。

如下面的示例所示,当找到匹配时,GroupCollection 和 CapturesCollection 对象都将用匹配中的捕获内容来填充。 在此情况下,存在捕获组 (\w+[;,]?\s?),因此可对其应用 + 限定符,从而使得正则表达式模式可与句子中的每个单词匹配。 否则,它将匹配句子中的最后一个单词。

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.
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.

当您只使用子表达式来对其应用限定符并且您对捕获的文本不感兴趣时,应禁用组捕获。 例如,(?:subexpression) 语言元素可防止应用该元素的组捕获匹配的子字符串。 在下面的示例中,上一示例中的正则表达式模式更改为 \b(?:\w+[;,]?\s?)+[.?!]。 正如输出所示,它禁止正则表达式引擎填充 GroupCollection 和 CapturesCollection 集合。

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.
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.

可以通过以下方式之一来禁用捕获:

  • 使用 (?:subexpression) 语言元素。 此元素可防止在它应用的组中捕获匹配的子字符串。 它不在任何嵌套的组中禁用子字符串捕获。

  • 使用 ExplicitCapture 选项。 在正则表达式模式中禁用所有未命名或隐式捕获。 使用此选项时,只能捕获与使用 (?<name>subexpression) 语言元素定义的命名组匹配的子字符串。 可将 ExplicitCapture 标记传递给 Regex 类构造函数的 options 参数或 Regex 静态匹配方法的 options 参数。

  • 在 (?imnsx) 语言元素中使用 n 选项。 此选项将在元素出现的正则表达式模式中的点处禁用所有未命名或隐式捕获。 捕获将一直禁用到模式结束或 (-n) 选项启用未命名或隐式捕获。 有关更多信息,请参见其他构造

  • 在 (?imnsx:subexpression) 语言元素中使用 n 选项。 此选项可在 subexpression 中禁用所有未命名或隐式捕获。 同时禁用任何未命名或隐式的嵌套捕获组进行的任何捕获。

相关主题

标题

说明

正则表达式行为的详细信息

在 .NET Framework 中检查正则表达式引擎的实现。 该主题重点介绍正则表达式的灵活性,并说明开发人员确保正则表达式引擎高效、强健运行的职责。

回溯

说明何为回溯及其对正则表达式性能有何影响,并检查为回溯提供替代项的语言元素。

正则表达式语言元素

介绍 .NET Framework 中的正则表达式语言的元素,并提供指向每个语言元素的详细文档的链接。

修订记录

日期

修订记录

原因

2011 年 3 月

新增主题。

信息补充。