1. 程式人生 > 實用技巧 >.NET 中的正則表示式最佳做法(官方轉載)

.NET 中的正則表示式最佳做法(官方轉載)

.NET 中的正則表示式引擎是一種功能強大而齊全的工具,它基於模式匹配(而不是比較和匹配文字)來處理文字。 在大多數情況下,它可以快速、高效地執行模式匹配。 但在某些情況下,正則表示式引擎的速度似乎很慢。 在極端情況下,它甚至看似停止響應,因為它會用若干個小時甚至若干天處理相對小的輸入。

本主題概述開發人員為了確保其正則表示式實現最佳效能可以採納的一些最佳做法。

考慮輸入源

通常,正則表示式可接受兩種型別的輸入:受約束的輸入或不受約束的輸入。 受約束的輸入是源自已知或可靠的源並遵循預定義格式的文字。 不受約束的輸入是源自不可靠的源(如 Web 使用者)並且可能不遵循預定義或預期格式的文字。

編寫的正則表示式模式的目的通常是匹配有效輸入。 也就是說,開發人員檢查他們要匹配的文字,然後編寫與其匹配的正則表示式模式。 然後,開發人員使用多個有效輸入項進行測試,以確定此模式是否需要更正或進一步細化。 當模式可匹配所有假定的有效輸入時,則將其宣告為生產就緒並且可包括在釋出的應用程式中。 這使得正則表示式模式適合匹配受約束的輸入。 但它不適合匹配不受約束的輸入。

若要匹配不受約束的輸入,正則表示式必須能夠高效處理以下三種文字:

  • 與正則表示式模式匹配的文字。

  • 與正則表示式模式不匹配的文字。

  • 與正則表示式模式大致匹配的文字。

對於為了處理受約束的輸入而編寫的正則表示式,最後一種文字型別尤其存在問題。 如果該正則表示式還依賴大量回溯,則正則表示式引擎可能會花費大量時間(在有些情況下,需要許多個小時或許多天)來處理看似無害的文字。

例如,考慮一種很常用但很有問題的用於驗證電子郵件地址別名的正則表示式。 編寫正則表示式 ^[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 = { "[email protected]",
                             "[email protected]" };
      // 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

如該示例輸出所示,正則表示式引擎處理有效電子郵件別名的時間間隔大致相同,與其長度無關。 另一方面,當接近有效的電子郵件地址包含五個以上字元時,字串中每增加一個字元,處理時間會大約增加一倍。 這意味著,處理接近有效的 28 個字元構成的字串將需要一個小時,處理接近有效的 33 個字元構成的字串將需要接近一天的時間。

由於開發此正則表示式時只考慮了要匹配的輸入的格式,因此未能考慮與模式不匹配的輸入。 這反過來會使與正則表示式模式近似匹配的不受約束輸入的效能顯著降低。

若要解決此問題,可執行下列操作:

  • 開發模式時,應考慮回溯對正則表示式引擎的效能的影響程度,特別是當正則表示式設計用於處理不受約束的輸入時。 有關詳細資訊,請參閱控制回溯部分。

  • 使用無效輸入、接近有效的輸入以及有效輸入對正則表示式進行完全測試。 若要為特定正則表示式隨機生成輸入,可以使用 Rex,這是 Microsoft Research 提供的正則表示式探索工具。

適當處理物件例項化

.NET 正則表示式物件模型的核心是 xref:System.Text.RegularExpressions.Regex?displayProperty=nameWithType 類,表示正則表示式引擎。 通常,影響正則表示式效能的單個最大因素是 xref:System.Text.RegularExpressions.Regex 引擎的使用方式。 定義正則表示式需要將正則表示式引擎與正則表示式模式緊密耦合。 無論該耦合過程是需要通過向其建構函式傳遞正則表示式模式來例項化 xref:System.Text.RegularExpressions.Regex 還是通過向其傳遞正則表示式模式和要分析的字串來呼叫靜態方法,都必然會消耗大量資源。

可將正則表示式引擎與特定正則表示式模式耦合,然後使用該引擎以若干種方式匹配文字:

這種呼叫正則表示式匹配方法的特殊方式會對應用程式產生顯著影響。 以下各節討論何時使用靜態方法呼叫、已解釋的正則表示式和已編譯的正則表示式,以改進應用程式的效能。

靜態正則表示式

建議將靜態正則表示式方法用作使用同一正則表示式重複例項化正則表示式物件的替代方法。 與正則表示式物件使用的正則表示式模式不同,靜態方法呼叫所使用的模式中的操作程式碼或已編譯的 Microsoft 中間語言 (MSIL) 由正則表示式引擎快取在內部。

例如,事件處理程式會頻繁呼叫其他方法來驗證使用者輸入。 下面的程式碼中反映了這一點,其中一個 xref:System.Windows.Forms.Button 控制元件的 xref:System.Windows.Forms.Control.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.";
}

下面的示例顯示 IsValidCurrency 方法的一個非常低效的實現。 請注意,每個方法呼叫使用相同模式重新例項化 xref:System.Text.RegularExpressions.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);
   }
}

應將此低效程式碼替換為對靜態 xref:System.Text.RegularExpressions.Regex.IsMatch(System.String%2CSystem.String)?displayProperty=nameWithType 方法的呼叫。 這樣便不必在你每次要呼叫模式匹配方法時都例項化 xref:System.Text.RegularExpressions.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);
   }
}

預設情況下,將快取最後 15 個最近使用的靜態正則表示式模式。 對於需要大量已快取的靜態正則表示式的應用程式,可通過設定 Regex.CacheSize 屬性來調整快取大小。

此示例中使用的正則表示式 \p{Sc}+\s*\d+ 可驗證輸入字串是否包含一個貨幣符號和至少一個十進位制數。 模式的定義如下表所示。

模式 描述
\p{Sc}+ 與 Unicode 符號、貨幣類別中的一個或多個字元匹配。
\s* 匹配零個或多個空白字元。
\d+ 匹配一個或多個十進位制數字。

已解釋與已編譯的正則表示式

將解釋未通過 RegexOptions.Compiled 選項的規範繫結到正則表示式引擎的正則表示式模式。 在例項化正則表示式物件時,正則表示式引擎會將正則表示式轉換為一組操作程式碼。 呼叫例項方法時,操作程式碼會轉換為 MSIL 並由 JIT 編譯器執行。 同樣,當呼叫一種靜態正則表示式方法並且在快取中找不到該正則表示式時,正則表示式引擎會將該正則表示式轉換為一組操作程式碼並將其儲存在快取中。 然後,它將這些操作程式碼轉換為 MSIL,以便於 JIT 編譯器執行。 已解釋的正則表示式會減少啟動時間,但會使執行速度變慢。 因此,在少數方法呼叫中使用正則表示式時或呼叫正則表示式方法的確切數量未知但預期很小時,使用已解釋的正則表示式的效果最佳。 隨著方法呼叫數量的增加,執行速度變慢對效能的影響會超過減少啟動時間帶來的效能改進。

將編譯通過 RegexOptions.Compiled 選項的規範繫結到正則表示式引擎的正則表示式模式。 這意味著,當例項化正則表示式物件時或當呼叫一種靜態正則表示式方法並且在快取中找不到該正則表示式時,正則表示式引擎會將該正則表示式轉換為一組中間操作程式碼,這些程式碼之後會轉換為 MSIL。 呼叫方法時,JIT 編譯器將執行該 MSIL。 與已解釋的正則表示式相比,已編譯的正則表示式增加了啟動時間,但執行各種模式匹配方法的速度更快。 因此,相對於呼叫的正則表示式方法的數量,因編譯正則表示式而產生的效能產生了改進。

簡言之,當你使用特定正則表示式呼叫正則表示式方法相對不頻繁時,建議使用已解釋的正則表示式。 當你使用特定正則表示式呼叫正則表示式方法相對頻繁時,應使用已編譯的正則表示式。 很難確定已解釋的正則表示式執行速度減慢超出啟動時間減少帶來的效能增益的確切閾值,或已編譯的正則表示式啟動速度減慢超出執行速度加快帶來的效能增益的閾值。 這依賴於各種因素,包括正則表示式的複雜程度和它處理的特定資料。 若要確定已解釋或已編譯的正則表示式是否可為特定應用程式方案提供最佳效能,可以使用 Diagnostics.Stopwatch 類來比較其執行時間。

下面的示例比較了已編譯和已解釋正則表示式在讀取 Theodore Dreiser 所著《金融家》中前十句文字和所有句文字時的效能。 如示例輸出所示,當只對匹配方法的正則表示式進行十次呼叫時,已解釋的正則表示式與已編譯的正則表示式相比,可提供更好的效能。 但是,當進行大量呼叫(在此示例中,超過 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

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

模式 描述
\b 在單詞邊界處開始匹配。
\w+ 匹配一個或多個單詞字元。
(\r?\n)|,?\s) 匹配零個或一個回車符後跟一個換行符,或零個或一個逗號後跟一個空白字元。
(\w+((\r?\n)|,?\s))* 匹配一個或多個單詞字元的零個或多個事例,後跟零個或一個回車符和換行符,或後跟零個或一個逗號、一個空格字元。
\w+ 匹配一個或多個單詞字元。
[.?:;!] 匹配句號、問號、冒號、分號或感嘆號。

正則表示式:編譯為程式集

藉助 .NET,還可以建立包含已編譯正則表示式的程式集。 這樣會將正則表示式編譯對效能造成的影響從執行時轉移到設計時。 但是,這還涉及一些其他工作:必須提前定義正則表示式並將其編譯為程式集。 然後,編譯器在編譯使用該程式集的正則表示式的原始碼時,可以引用此程式集。 程式集內的每個已編譯正則表示式都由從 xref:System.Text.RegularExpressions.Regex 派生的類來表示。

若要將正則表示式編譯為程式集,可呼叫 Regex.CompileToAssembly(RegexCompilationInfo[], AssemblyName) 方法並向其傳遞表示要編譯的正則表示式的 RegexCompilationInfo 物件陣列和包含有關要建立的程式集的資訊的 AssemblyName 物件。

建議你在以下情況下將正則表示式編譯為程式集:

  • 如果你是要建立可重用正則表示式庫的元件開發人員。

  • 如果你預期正則表示式的模式匹配方法要被呼叫的次數無法確定 -- 從任意位置,次數可能為一次兩次到上千上萬次。 與已編譯或已解釋的正則表示式不同,編譯為單獨程式集的正則表示式可提供與方法呼叫數量無關的一致性能。

如果使用已編譯的正則表示式來優化效能,則不應使用反射來建立程式集,載入正則表示式引擎並執行其模式匹配方法。 這要求你避免動態生成正則表示式模式,並且要在建立程式集時指定模式匹配選項(如不區分大小寫的模式匹配)。 它還要求將建立程式集的程式碼與使用正則表示式的程式碼分離。

下面的示例演示如何建立包含已編譯的正則表示式的程式集。 它建立包含一個正則表示式類 SentencePattern 的程式集 RegexLib.dll,其中包含已解釋與已編譯的正則表示式部分中使用的句子匹配的正則表示式模式。

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 並由 RegularExpressions.Regex 派生的類來表示。 然後,下面的示例使用已編譯正則表示式,從 Theodore Dreiser 所著《金融家》文字中提取句子。

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.

控制回溯

通常,正則表示式引擎使用線性進度在輸入字串中移動並將其編譯為正則表示式模式。 但是,當在正則表示式模式中使用不確定限定符(如 *+?)時,正則表示式引擎可能會放棄一部分成功的分部匹配,並返回以前儲存的狀態,以便為整個模式搜尋成功匹配。 此過程稱為回溯。

支援回溯可為正則表示式提供強大的功能和靈活性。 還可將控制正則表示式引擎操作的職責交給正則表示式開發人員來處理。 由於開發人員通常不瞭解此職責,因此其誤用回溯或依賴過多回溯通常會顯著降低正則表示式的效能。 在最糟糕的情況下,輸入字串中每增加一個字元,執行時間會加倍。 實際上,如果過多使用回溯,則在輸入與正則表示式模式近似匹配時很容易建立無限迴圈的程式設計等效形式;正則表示式引擎可能需要幾小時甚至幾天來處理相對短的輸入字串。

通常,儘管回溯不是匹配所必需的,但應用程式會因使用回溯而對效能產生負面影響。 例如,正則表示式 \b\p{Lu}\w*\b 將匹配以大寫字元開頭的所有單詞,如下表所示。

模式 描述
\b 在單詞邊界處開始匹配。
\p{Lu} 匹配大寫字元。
\w* 匹配零個或多個單詞字元。
\b 在單詞邊界處結束匹配。

由於單詞邊界與單詞字元不同也不是其子集,因此正則表示式引擎在匹配單詞字元時無法跨越單詞邊界。 這意味著,對於此正則表示式而言,回溯對任何匹配的總體成功不會有任何貢獻 -- 由於正則表示式引擎被強制為單詞字元的每個成功的初步匹配儲存其狀態,因此它只會降低效能。

如果確定不需要回溯,可使用 (?>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

在許多情況下,在將正則表示式模式與輸入文字匹配時,回溯很重要。 但是,過度回溯會嚴重降低效能,並且會產生應用程式已停止響應的感覺。 特別需要指出的是,當巢狀限定符並且與外部子表示式匹配的文字為與內部子表示式匹配的文字的子集時,尤其會出現這種情況。

例如,正則表示式模式 ^[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]) 檢視作為結束的美元符號,以確保前一個字元是字母數字。
$ 在輸入字串末尾結束匹配。

下面的示例演示瞭如何使用此正則表示式來匹配包含可能部件號的陣列。

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 中的正則表示式語言包括以下可用於消除巢狀限定符的語言元素。 有關詳細資訊,請參閱 分組構造

語言元素 描述
(?= subexpression ) 零寬度正預測先行。 預測先行當前位置,以確定 subexpression 是否與輸入字串匹配。
(?! subexpression ) 零寬度負預測先行。 預測先行當前位置,以確定 subexpression 是否不與輸入字串匹配。
(?<= subexpression ) 零寬度正回顧。 回顧後發當前位置,以確定 subexpression 是否與輸入字串匹配。
(?<! subexpression ) 零寬度負回顧。 回顧後發當前位置,以確定 subexpression 是否不與輸入字串匹配。

使用超時值

如果正則表示式處理與正則表示式模式大致匹配的輸入,則通常依賴於會嚴重影響其效能的過度回溯。 除認真考慮對回溯的使用以及針對大致匹配輸入對正則表示式進行測試之外,還應始終設定一個超時值以確保最大程度地降低過度回溯的影響(如果有)。

正則表示式超時間隔定義了在超時前正則表示式引擎用於查詢單個匹配項的時間長度。預設超時間隔為 Regex.InfiniteMatchTimeout,這意味著正則表示式不會超時。可以按如下所示重寫此值並定義超時間隔:

  • 在例項化一個 Regex 物件(通過呼叫 Regex(String, RegexOptions, TimeSpan) 建構函式)時,提供一個超時值。
  • 呼叫靜態模式匹配方法,如 Regex.Match(String, String, RegexOptions, TimeSpan) 或 Regex.Replace(String, String, String, RegexOptions, TimeSpan),其中包含 matchTimeout 引數。
  • 對於通過呼叫 Regex.CompileToAssembly 方法建立的已編譯的正則表示式,可呼叫帶有 TimeSpan 型別的引數的建構函式。

如果定義了超時間隔並且在此間隔結束時未找到匹配項,則正則表示式方法將引發 RegexMatchTimeoutException 異常。 在異常處理程式中,可以選擇使用一個更長的超時間隔來重試匹配、放棄匹配嘗試並假定沒有匹配項,或者放棄匹配嘗試並記錄異常資訊以供未來分析。

下面的示例定義了一種 GetWordData 方法,此方法例項化了一個正則表示式,使其具有 350 毫秒的超時間隔,用於計算文字檔案中的詞語數和一個詞語中的平均字元數。 如果匹配操作超時,則超時間隔將延長 350 毫秒並重新例項化 Regex 物件。 如果新的超時間隔超過 1 秒,則此方法將再次向呼叫方引發異常。

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

只在必要時捕獲

.NET 中的正則表示式支援許多分組構造,這樣,便可以將正則表示式模式分組為一個或多個子表示式。 .NET 正則表示式語言中最常用的分組構造為 (subexpression)(用於定義編號捕獲組)和 (?subexpression)(用於定義命名捕獲組)。 分組構造是建立反向引用和定義要應用限定符的子表示式時所必需的。

但是,使用這些語言元素會產生一定的開銷。 它們會導致用最近的未命名或已命名捕獲來填充 GroupCollection 屬性返回的 Match.Groups 物件,如果單個分組構造已捕獲輸入字串中的多個子字串,則還會填充包含多個 CaptureCollection 物件的特定捕獲組的 Group.Captures 屬性返回的 Capture 物件。

通常,只在正則表示式中使用分組構造,這樣可對其應用限定符,而且以後不會使用這些子表示式捕獲的組。 例如,正則表示式 \b(\w+[;,]?\s?)+[.?!] 用於捕獲整個句子。 下表描述了此正則表示式模式中的語言元素及其對 Match 物件的 Match.Groups 和 Group.Captures 集合的影響。

模式 描述
\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.

當你只使用子表示式來對其應用限定符並且你對捕獲的文字不感興趣時,應禁用組捕獲。 例如,(?: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.

可以通過以下方式之一來禁用捕獲:

  • 使用 (?:subexpression) 語言元素。 此元素可防止在它應用的組中捕獲匹配的子字串。 它不在任何巢狀的組中禁用子字串捕獲。
  • 使用 ExplicitCapture 選項。 在正則表示式模式中禁用所有未命名或隱式捕獲。 使用此選項時,只能捕獲與使用 (?subexpression) 語言元素定義的命名組匹配的子字串。 可將 ExplicitCapture 標記傳遞給 options 類建構函式的 Regex 引數或 options 靜態匹配方法的 Regex 引數。
  • 在 n 語言元素中使用 (?imnsx) 選項。 此選項將在元素出現的正則表示式模式中的點處禁用所有未命名或隱式捕獲。 捕獲將一直禁用到模式結束或 (-n) 選項啟用未命名或隱式捕獲。 有關詳細資訊,請參閱 其他構造。
  • 在 n 語言元素中使用 (?imnsx:subexpression) 選項。 此選項可在 subexpression 中禁用所有未命名或隱式捕獲。 同時禁用任何未命名或隱式的巢狀捕獲組進行的任何捕獲。

原文連結