您现在的位置是:网站首页> 编程资料编程资料
.NET正则表达式的最佳用法_基础应用_
2023-05-24
307人已围观
简介 .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 = { "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 如该示例输出所示,正则表达式引擎处理有效电子邮件别名的时间间隔大致相同,与其长度无关。 另一方面,当接近有效的电子邮件地址包含五个以上字符时,字符串中每增加一个字符,处理时间会大约增加一倍。 这意味着,处理接近有效的 28 个字符构成的字符串将需要一个小时,处理接近有效的 33 个字符构成的字符串将需要接近一天的时间。
由于开发此正则表达式时只考虑了要匹配的输入的格式,因此未能考虑与模式不匹配的输入。 这反过来会使与正则表达式模式近似匹配的不受约束输入的性能显著降低。
若要解决此问题,可执行下列操作:
开发模式时,应考虑回溯对正则表达式引擎的性能的影响程度,特别是当正则表达式设计用于处理不受约束的输入时。 有关详细信息,请参阅控制回溯部分。
使用无效输入、接近有效的输入以及有效输入对正则表达式进行完全测试。 若要为特定正则表达式随机生成输入,可以使用 Rex,这是 Microsoft Research 提供的正则表达式探索工具。
适当处理对象实例化
.NET 正则表达式对象模型的核心是 xref:System.Text.RegularExpressions.Regex?displayProperty=nameWithType 类,表示正则表达式引擎。 通常,影响正则表达式性能的单个最大因素是 xref:System.Text.RegularExpressions.Regex 引擎的使用方式。 定义正则表达式需要将正则表达式引擎与正则表达式模式紧密耦合。 无论该耦合过程是需要通过向其构造函数传递正则表达式模式来实例化 xref:System.Text.RegularExpressions.Regex 还是通过向其传递正则表达式模式和要分析的字符串来调用静态方法,都必然会消耗大量资源。

可将正则表达式引擎与特定正则表达式模式耦合,然后使用该引擎以若干种方式匹配文本:
可以调用静态模式匹配方法,如 xref:System.Text.RegularExpressions.Regex.Match(System.String%2CSystem.String)?displayProperty=nameWithType。 这不需要实例化正则表达式对象。
可以实例化一个 xref:System.Text.RegularExpressions.Regex 对象并调用已解释的正则表达式的实例模式匹配方法。 这是将正则表达式引擎绑定到正则表达式模式的默认方法。 如果实例化 xref:System.Text.RegularExpressions.Regex 对象时未使用包括
options标记的 xref:System.Text.RegularExpressions.RegexOptions.Compiled 自变量,则会生成此方法。可以实例化一个 xref:System.Text.RegularExpressions.Regex 对象并调用已编译的正则表达式的实例模式匹配方法。 当使用包括 xref:System.Text.RegularExpressions.Regex 标记的
options参数实例化 xref:System.Text.RegularExpressions.RegexOptions.Compiled 对象时,正则表达式对象表示已编译的模式。可以创建一个与特定正则表达式模式紧密耦合的特殊用途的 xref:System.Text.RegularExpressions.Regex 对象,编译该对象,并将其保存到独立程序集中。 为此,可调用 xref:System.Text.RegularExpressions.Regex.CompileToAssembly*?displayProperty=nameWithType 方法。
这种调用正则表达式匹配方法的特殊方式会对应用程序产生显著影响。 以下各节讨论何时使用静态方法调用、已解释的正则表达式和已编译的正则表达式,以改进应用程序的性能。

静态正则表达式
建议将静态正则表达式方法用作使用同一正则表达式重复实例化正则表达式对象的替代方法。 与正则表达式对象使用的正则表达式模式不同,静态方法调用所使用的模式中的操作代码或已编译的 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
