如何使正則表達式操作超時以防止在 .NET 4.5 中掛起?
有時能夠限制正則表達式操作的模式匹配持續時間可能很有用。特別是,當使用使用者提供的模式來匹配數據時,由於嵌套的量詞和過多的回溯(請參閱災難性回溯),該模式可能表現出較差的性能。應用超時的一種方法是非同步執行正則表達式,但這可能很乏味並且會使程式碼混亂。
根據.NET Framework 4.5 Developer Preview 中的新增功能,似乎有一種新的內置方法可以支持這一點:
能夠限制正則表達式引擎在超時之前嘗試解析正則表達式的時間。
如何使用此功能?另外,使用時需要注意什麼?
我最近研究了這個話題,因為它讓我感興趣,並將在這裡介紹要點。相關的 MSDN 文件可在此處獲得,您可以查看
Regex該類以查看新的重載建構子和靜態方法。程式碼範例可以使用Visual Studio 11 Developer Preview執行。該類
Regex接受 aTimeSpan來指定超時持續時間。您可以在應用程序的宏觀和微觀層面指定超時,它們可以一起使用:
- 使用方法設置
"REGEX_DEFAULT_MATCH_TIMEOUT"屬性(宏應用範圍)AppDomain.SetData- 傳遞
matchTimeout參數(微本地化範圍)設置該
AppDomain屬性後,所有Regex操作都將使用該值作為預設超時。要覆蓋應用程序範圍的預設值,您只需將matchTimeout值傳遞給正則表達式建構子或靜態方法。如果AppDomain未設置且未指定預設值matchTimeout,則模式匹配將不會超時(即原始 pre-.NET 4.5 行為)。有兩個主要的例外需要處理:
RegexMatchTimeoutException: 發生超時時拋出。ArgumentOutOfRangeException``matchTimeout:當“為負數或大於大約 24 天”時拋出。此外,TimeSpan零值將導致它被拋出。儘管不允許使用負值,但有一個例外:接受 -1 ms 的值。該類在內部
Regex接受 -1 ms,它是Regex.InfiniteMatchTimeoutfield的值,以指示匹配不應超時(即原始 pre-.NET 4.5 行為)。使用 matchTimeout 參數
在下面的範例中,我將展示有效和無效的超時情況以及如何處理它們:
string input = "The quick brown fox jumps over the lazy dog."; string pattern = @"([a-z ]+)*!"; var timeouts = new[] { TimeSpan.FromSeconds(4), // valid TimeSpan.FromSeconds(-10) // invalid }; foreach (var matchTimeout in timeouts) { Console.WriteLine("Input: " + matchTimeout); try { bool result = Regex.IsMatch(input, pattern, RegexOptions.None, matchTimeout); } catch (RegexMatchTimeoutException ex) { Console.WriteLine("Match timed out!"); Console.WriteLine("- Timeout interval specified: " + ex.MatchTimeout); Console.WriteLine("- Pattern: " + ex.Pattern); Console.WriteLine("- Input: " + ex.Input); } catch (ArgumentOutOfRangeException ex) { Console.WriteLine(ex.Message); } Console.WriteLine(); }使用
Regex該類的實例時,您可以訪問該MatchTimeout屬性:string input = "The English alphabet has 26 letters"; string pattern = @"\d+"; var matchTimeout = TimeSpan.FromMilliseconds(10); var sw = Stopwatch.StartNew(); try { var re = new Regex(pattern, RegexOptions.None, matchTimeout); bool result = re.IsMatch(input); sw.Stop(); Console.WriteLine("Completed match in: " + sw.Elapsed); Console.WriteLine("MatchTimeout specified: " + re.MatchTimeout); Console.WriteLine("Matched with {0} to spare!", re.MatchTimeout.Subtract(sw.Elapsed)); } catch (RegexMatchTimeoutException ex) { sw.Stop(); Console.WriteLine(ex.Message); }使用 AppDomain 屬性
該
"REGEX_DEFAULT_MATCH_TIMEOUT"屬性用於設置應用程序範圍的預設值:AppDomain.CurrentDomain.SetData("REGEX_DEFAULT_MATCH_TIMEOUT", TimeSpan.FromSeconds(2));如果此屬性設置為無效
TimeSpan值或無效對象,TypeInitializationException則在嘗試使用正則表達式時將拋出 a。具有有效屬性值的範例:
// AppDomain default set somewhere in your application AppDomain.CurrentDomain.SetData("REGEX_DEFAULT_MATCH_TIMEOUT", TimeSpan.FromSeconds(2)); // regex use elsewhere... string input = "The quick brown fox jumps over the lazy dog."; string pattern = @"([a-z ]+)*!"; var sw = Stopwatch.StartNew(); try { // no timeout specified, defaults to AppDomain setting bool result = Regex.IsMatch(input, pattern); sw.Stop(); } catch (RegexMatchTimeoutException ex) { sw.Stop(); Console.WriteLine("Match timed out!"); Console.WriteLine("Applied Default: " + ex.MatchTimeout); } catch (ArgumentOutOfRangeException ex) { sw.Stop(); } catch (TypeInitializationException ex) { sw.Stop(); Console.WriteLine("TypeInitializationException: " + ex.Message); Console.WriteLine("InnerException: {0} - {1}", ex.InnerException.GetType().Name, ex.InnerException.Message); } Console.WriteLine("AppDomain Default: {0}", AppDomain.CurrentDomain.GetData("REGEX_DEFAULT_MATCH_TIMEOUT")); Console.WriteLine("Stopwatch: " + sw.Elapsed);將上面的範例與無效(負)值一起使用會導致拋出異常。處理它的程式碼將以下消息寫入控制台:
TypeInitializationException:“System.Text.RegularExpressions.Regex”的類型初始化程序引發了異常。
InnerException: ArgumentOutOfRangeException - 指定的參數超出了有效值的範圍。參數名稱:AppDomain 數據“REGEX_DEFAULT_MATCH_TIMEOUT”包含用於指定 System.Text.RegularExpressions.Regex 的預設匹配超時的無效值或對象。
在這兩個範例中,
ArgumentOutOfRangeException都沒有拋出。Regex為了完整起見,程式碼顯示了使用新的 .NET 4.5超時功能時可以處理的所有異常。覆蓋 AppDomain 預設值
AppDomain通過指定一個值來覆蓋預設matchTimeout值。在下一個範例中,匹配在 2 秒內超時,而不是預設的 5 秒。AppDomain.CurrentDomain.SetData("REGEX_DEFAULT_MATCH_TIMEOUT", TimeSpan.FromSeconds(5)); string input = "The quick brown fox jumps over the lazy dog."; string pattern = @"([a-z ]+)*!"; var sw = Stopwatch.StartNew(); try { var matchTimeout = TimeSpan.FromSeconds(2); bool result = Regex.IsMatch(input, pattern, RegexOptions.None, matchTimeout); sw.Stop(); } catch (RegexMatchTimeoutException ex) { sw.Stop(); Console.WriteLine("Match timed out!"); Console.WriteLine("Applied Default: " + ex.MatchTimeout); } Console.WriteLine("AppDomain Default: {0}", AppDomain.CurrentDomain.GetData("REGEX_DEFAULT_MATCH_TIMEOUT")); Console.WriteLine("Stopwatch: " + sw.Elapsed);結束語
MSDN 建議在所有正則表達式模式匹配操作中設置一個超時值。但是,它們不會將您的注意力吸引到這樣做時需要注意的問題上。我不建議設置 AppDomain 預設值並將其稱為一天。你需要知道你的輸入和你的模式。如果輸入很大,或者模式很複雜,則應使用適當的超時值。這可能還需要衡量您嚴格執行的正則表達式用法以分配合理的預設值。如果該值不夠長,則將超時值任意分配給曾經正常工作的正則表達式可能會導致其中斷。如果您認為它可能會過早中止匹配嘗試,請在分配值之前測量現有使用情況。
此外,此功能在處理使用者提供的模式時很有用。然而,學習如何編寫表現良好的正確模式很重要。對其進行超時以彌補在正確模式構造方面缺乏知識並不是好的做法。