.NET 中的正则表达式引擎是一种功能强大的功能齐全的工具,它基于模式匹配而不是比较和匹配文本文本来处理文本。 在大多数情况下,它会快速高效地执行模式匹配。 但是,在某些情况下,正则表达式引擎可能看起来很慢。 在极端情况下,它甚至可能停止响应,因为它在数小时甚至几天内处理相对较小的输入。
本文概述了开发人员可以采用的一些最佳做法,以确保其正则表达式实现最佳性能。
警告
如果使用 System.Text.RegularExpressions 处理不受信任的输入,则传递一个超时。 恶意用户可以通过输入 RegularExpressions
,从而导致 拒绝服务攻击。 使用 RegularExpressions
的 ASP.NET Core 框架 API 会传递一个超时。
考虑输入源
一般情况下,正则表达式可以接受两种类型的输入:受约束或不受约束。 受约束的输入是源自已知或可靠源且遵循预定义格式的文本。 不受约束的输入是源自不可靠的源(如 Web 用户)的文本,可能不会遵循预定义格式或预期格式。
正则表达式模式通常写入以匹配有效输入。 也就是说,开发人员检查要匹配的文本,然后编写与其匹配的正则表达式模式。 然后,开发人员通过使用多个有效的输入项对其进行测试来确定此模式是否需要更正或进一步细化。 当模式与所有假定的有效输入匹配时,它将声明为生产就绪,并且可以包含在已发布的应用程序中。 此方法使正则表达式模式适用于匹配约束输入。 但是,它不适合匹配不受约束的输入。
若要匹配不受约束的输入,正则表达式必须有效处理三种类型的文本:
- 与正则表达式模式匹配的文本。
- 与正则表达式模式不匹配的文本。
- 几乎与正则表达式模式匹配的文本。
最后一个文本类型对于已写入以处理受约束输入的正则表达式尤其有问题。 如果该正则表达式还依赖于广泛的 回溯,则正则表达式引擎可以花费大量时间(在某些情况下,许多小时或几天)处理看似无害的文本。
警告
以下示例使用容易过度回溯且可能拒绝有效电子邮件地址的正则表达式。 不应在电子邮件验证例程中使用它。 如果想要验证电子邮件地址的正则表达式,请参阅 “如何:验证字符串是否采用有效的电子邮件格式”。
例如,考虑用于验证电子邮件地址别名的常用但有问题的正则表达式。 正则表达式 ^[0-9A-Z]([-.\w]*[0-9A-Z])*$
被编写用于处理被视为有效的电子邮件地址。 有效的电子邮件地址包含一个字母数字字符,后跟零个或多个可为字母数字、句点或连字符的字符。 正则表达式必须以字母数字字符结尾。 但是,如以下示例所示,尽管此正则表达式可以轻松处理有效输入,但在处理几乎有效的输入时,其性能效率低下:
using System;
using System.Diagnostics;
using System.Text.RegularExpressions;
public class DesignExample
{
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 对象,还是通过传递正则表达式模式和要分析的字符串来调用静态方法。
注释
使用解释和编译正则表达式可能对性能有影响,关于这一点的详细讨论,请参阅博客文章 优化正则表达式性能,第二部分:掌控回溯。
可以将正则表达式引擎与特定的正则表达式模式耦合,然后使用引擎以多种方式匹配文本:
可以调用静态模式匹配方法,例如 Regex.Match(String, String)。 此方法不需要实例化正则表达式对象。
可以实例化 Regex 对象并调用解释的正则表达式的实例模式匹配方法,这是将正则表达式引擎绑定到正则表达式模式的默认方法。 如果实例化 Regex 对象时未使用包括
options
标记的 Compiled 自变量,则会生成此方法。可以实例化 Regex 对象并调用源生成的正则表达式的实例模式匹配方法。 在大多数情况下,建议使用此方法。 为此,请将 GeneratedRegexAttribute 属性置于返回
Regex
的分部方法上。可以实例化 Regex 对象并调用已编译正则表达式的实例模式匹配方法。 当使用包含Regex标志的
options
参数实例化Compiled对象时,正则表达式对象表示编译的正则表达式模式。
调用正则表达式匹配方法的特定方式可能会影响应用程序的性能。 以下各节讨论何时使用静态方法调用、源生成的正则表达式、解释的正则表达式和已编译的正则表达式来提高应用程序的性能。
重要
方法调用的形式(静态、解释、源生成、已编译)在方法调用中重复使用同一正则表达式,或者应用程序广泛使用正则表达式对象时,会影响性能。
静态正则表达式
建议使用静态正则表达式方法,以替代重复实例化相同正则表达式的正则表达式对象。 与正则表达式对象中使用的正则表达式模式不同,正则表达式引擎会在内部缓存静态方法调用中使用的模式所生成的操作码(opcodes)或已编译的公共中间语言(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 RegexLib2
{
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}+ |
与 Unicode 符号、货币类别中的一个或多个字符匹配。 |
\s* |
匹配零个或多个空白字符。 |
\d+ |
匹配一个或多个十进制数字。 |
已解释的、源代码生成的与已编译的正则表达式
未通过指定 Compiled 选项绑定到正则表达式引擎的正则表达式模式是已解释的。 实例化正则表达式对象时,正则表达式引擎会将正则表达式转换为一组作代码。 调用实例方法时,作代码将转换为 CIL,并由 JIT 编译器执行。 同样,当调用静态正则表达式方法且无法在缓存中找到正则表达式时,正则表达式引擎会将正则表达式转换为一组作代码,并将其存储在缓存中。 然后,它将这些作代码转换为 CIL,以便 JIT 编译器可以执行它们。 解释的正则表达式可降低启动时间,代价是执行时间变慢。 由于此过程,正则表达式最好用于少量的方法调用,或者当对正则表达式方法的确切调用次数不明但预计较少时。 随着方法调用的数量增加,启动时间降低带来的性能提升超过了执行速度较慢的情况。
正则表达式模式通过Compiled选项绑定到正则表达式引擎,并被编译。 因此,当实例化正则表达式对象或调用静态正则表达式方法且在缓存中找不到正则表达式时,正则表达式引擎会将正则表达式转换为中间的作代码集。 然后将这些代码转换为 CIL。 调用方法时,JIT 编译器将执行 CIL。 与解释的正则表达式相比,编译的正则表达式会增加启动时间,但执行单个模式匹配方法的速度会更快。 因此,编译正则表达式产生的性能优势与调用的正则表达式方法数成比例增加。
通过使用 Regex
属性装饰 GeneratedRegexAttribute 返回方法绑定到正则表达式引擎的正则表达式模式是源代码生成的。 插入编译器的源生成器会以 C# 代码的形式生成一个自定义的 Regex
派生实现,其逻辑类似于 RegexOptions.Compiled
在 CIL 中生成的内容。 可以获得RegexOptions.Compiled
的所有吞吐量性能优势(实际上更多)和Regex.CompileToAssembly
的启动优势,但没有CompileToAssembly
的复杂性。 发出的源是项目的一部分,这意味着它也易于查看和可调试。
总之,我们建议你:
- 当你使用特定正则表达式调用正则表达式方法相对不频繁时,请使用已解释的正则表达式。
- 如果你在 C# 中将 与编译时已知的参数一起使用,并且相对频繁地使用特定的正则表达式,请使用源代码生成的正则表达式。
Regex
- 如果你使用特定正则表达式相对频繁地调用正则表达式方法,并且你使用 .NET 6 或更早的版本,请使用已编译的正则表达式。
很难确定处于哪个确切阈值时,已解释的正则表达式执行速度较慢的代价会超过其启动时间缩短带来的收益。 也很难确定源生成或编译正则表达式的启动时间较慢的阈值,在何时会超过其执行速度更快所带来的收益。 阈值取决于各种因素,包括正则表达式的复杂性及其处理的特定数据。 若要确定哪些正则表达式为特定应用程序方案提供最佳性能,可以使用 Stopwatch 该类来比较其执行时间。
下面的示例比较了读取前 10 个句子时编译的、源生成的正则表达式的性能,以及阅读威廉 D. 古思里的 《大宪章》和其他地址文本中的所有句子时所解释的正则表达式的性能。 如示例中的输出所示,当只对正则表达式匹配方法进行 10 次调用时,解释的或源生成的正则表达式的性能优于已编译的正则表达式。 但是,在进行大量调用(在这种情况下,超过 13,000 次)时,编译的正则表达式可提供更好的性能。
const string Pattern = @"\b(\w+((\r?\n)|,?\s))*\w+[.?:;!]";
static readonly HttpClient s_client = new();
[GeneratedRegex(Pattern, RegexOptions.Singleline)]
private static partial Regex GeneratedRegex();
public async static Task RunIt()
{
Stopwatch sw;
Match match;
int ctr;
string text =
await s_client.GetStringAsync("https://www.gutenberg.org/cache/epub/64197/pg64197.txt");
// Read first ten sentences with interpreted regex.
Console.WriteLine("10 Sentences with Interpreted Regex:");
sw = Stopwatch.StartNew();
Regex int10 = new(Pattern, RegexOptions.Singleline);
match = int10.Match(text);
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($" {ctr} matches in {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(text);
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($" {ctr} matches in {sw.Elapsed}");
// Read first ten sentences with source-generated regex.
Console.WriteLine("10 Sentences with Source-generated Regex:");
sw = Stopwatch.StartNew();
match = GeneratedRegex().Match(text);
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($" {ctr} matches in {sw.Elapsed}");
// Read all sentences with interpreted regex.
Console.WriteLine("All Sentences with Interpreted Regex:");
sw = Stopwatch.StartNew();
Regex intAll = new(Pattern, RegexOptions.Singleline);
match = intAll.Match(text);
int matches = 0;
while (match.Success)
{
matches++;
// Do nothing with the match except get the next match.
match = match.NextMatch();
}
sw.Stop();
Console.WriteLine($" {matches:N0} matches in {sw.Elapsed}");
// Read all sentences with compiled regex.
Console.WriteLine("All Sentences with Compiled Regex:");
sw = Stopwatch.StartNew();
Regex compAll = new(Pattern,
RegexOptions.Singleline | RegexOptions.Compiled);
match = compAll.Match(text);
matches = 0;
while (match.Success)
{
matches++;
// Do nothing with the match except get the next match.
match = match.NextMatch();
}
sw.Stop();
Console.WriteLine($" {matches:N0} matches in {sw.Elapsed}");
// Read all sentences with source-generated regex.
Console.WriteLine("All Sentences with Source-generated Regex:");
sw = Stopwatch.StartNew();
match = GeneratedRegex().Match(text);
matches = 0;
while (match.Success)
{
matches++;
// Do nothing with the match except get the next match.
match = match.NextMatch();
}
sw.Stop();
Console.WriteLine($" {matches:N0} matches in {sw.Elapsed}");
return;
}
/* The example displays output similar to the following:
10 Sentences with Interpreted Regex:
10 matches in 00:00:00.0104920
10 Sentences with Compiled Regex:
10 matches in 00:00:00.0234604
10 Sentences with Source-generated Regex:
10 matches in 00:00:00.0060982
All Sentences with Interpreted Regex:
3,427 matches in 00:00:00.1745455
All Sentences with Compiled Regex:
3,427 matches in 00:00:00.0575488
All Sentences with Source-generated Regex:
3,427 matches in 00:00:00.2698670
*/
示例 \b(\w+((\r?\n)|,?\s))*\w+[.?:;!]
中使用的正则表达式模式定义如下表所示:
图案 | DESCRIPTION |
---|---|
\b |
在单词边界处开始匹配。 |
\w+ |
匹配一个或多个单词字符。 |
(\r?\n)|,?\s) |
匹配零个或一个回车符后跟一个换行符,或零个或一个逗号后跟一个空白字符。 |
(\w+((\r?\n)|,?\s))* |
匹配一个或多个单词字符的零个或多个事例,后跟零个或一个回车符和换行符,或后跟零个或一个逗号、一个空格字符。 |
\w+ |
匹配一个或多个单词字符。 |
[.?:;!] |
匹配句号、问号、冒号、分号或感叹号。 |
控制回溯
通常,正则表达式引擎使用线性进度在输入字符串中移动,并将其与正则表达式模式进行比较。 但是,当不确定限定符(如*
、+
和?
)在正则表达式模式中使用时,正则表达式引擎可能会放弃一部分已成功的部分匹配,并返回到先前保存的状态,以便搜索整个模式的成功匹配。 此过程称为回溯。
小窍门
有关回溯的详细信息,请参阅 正则表达式行为 和 回溯的详细信息。 有关回溯的详细讨论,请参阅 .NET 7 中的正则表达式改进 和 优化正则表达式性能 博客文章。
支持回溯赋予正则表达式更强的功能和灵活性。 它还赋予了正则表达式开发人员控制正则表达式引擎操作的责任。 由于开发人员通常不知道这一责任,因此他们滥用回溯或依赖过度回溯往往在降低正则表达式性能方面起着最重要的作用。 在最坏的情况下,对于输入字符串中的每个附加字符,执行时间可以加倍。 事实上,通过过度回溯,当输入几乎匹配正则表达式模式时,很容易导致类似于编程中的无限循环情况。 正则表达式引擎可能需要数小时甚至数天来处理相对较短的输入字符串。
通常,即使回溯对匹配来说并不重要,应用程序也会因使用回溯而付出性能损失。 例如,正则表达式 \b\p{Lu}\w*\b
匹配以大写字符开头的所有单词,如下表所示:
图案 | DESCRIPTION |
---|---|
\b |
在单词边界处开始匹配。 |
\p{Lu} |
匹配大写字符。 |
\w* |
匹配零个或多个单词字符。 |
\b |
在单词边界处结束匹配。 |
由于单词边界既与单词字符不同,也不是其子集,因此在匹配单词字符时,正则表达式引擎不可能跨越单词边界。 因此,对于此正则表达式而言,回溯对任何匹配的总体成功不会有任何贡献。 由于正则表达式引擎被强制为单词字符的每个成功的初步匹配保存其状态,因此它只会降低性能。
如果确定不需要回溯,可以通过以下方式禁用它:
通过设置 RegexOptions.NonBacktracking 选项(在 .NET 7 中引入)。 有关详细信息,请参阅 “非回溯模式”。
通过使用
(?>subexpression)
语言元素(称为原子组)。 以下示例使用两个正则表达式分析输入字符串。 第一个正则表达式\b\p{Lu}\w*\b
依赖于回溯。 第二个选项,\b\p{Lu}(?>\w*)\b
,禁用回溯。 如示例所示的输出,它们都生成相同的结果:using System; using System.Text.RegularExpressions; public class BackTrack2Example { 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]*
的子集。
在这些情况下,可通过移除嵌套限定符并将外部子表达式替换为零宽度预测先行和回顾断言来优化正则表达式性能。 先行和后行断言是定位点。 它们不会在输入字符串中移动指针,而是向前或向后移动指针,以检查是否满足指定的条件。 例如,部件号正则表达式可以重写为 ^[0-9A-Z][-.\w]*(?<=[0-9A-Z])\$$
. 此正则表达式模式的定义如下表所示:
图案 | DESCRIPTION |
---|---|
^ |
在输入字符串的开头开始匹配。 |
[0-9A-Z] |
匹配字母数字字符。 部件号必须至少包含此字符。 |
[-.\w]* |
匹配零个或多个任意单词字符、连字符或句号。 |
\$ |
匹配美元符号。 |
(?<=[0-9A-Z]) |
回顾作为结束的美元符号,以确保前一个字符是字母数字。 |
$ |
在输入字符串的末尾结束匹配。 |
以下示例演示如何使用此正则表达式来匹配包含可能的部件号的数组:
using System;
using System.Text.RegularExpressions;
public class BackTrack4Example
{
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 中的正则表达式语言包含以下可用于消除嵌套限定符的语言元素。 有关详细信息,请参阅 分组构造。
Language 元素 | 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
参数。使用代码(例如
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 TimeoutExample
{
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: {info.Item1:N0}");
Console.WriteLine($"Average Word Length: {info.Item2:N2} characters");
}
catch (IOException e)
{
Console.WriteLine($"IOException reading file '{title}'");
Console.WriteLine(e.Message);
}
catch (RegexMatchTimeoutException e)
{
Console.WriteLine($"The operation timed out after {e.MatchTimeout.TotalMilliseconds:N0} milliseconds");
}
}
}
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 对象的 Match.Groups 和 Group.Captures 集合的影响:
图案 | DESCRIPTION |
---|---|
\b |
在单词边界处开始匹配。 |
\w+ |
匹配一个或多个单词字符。 |
[;,]? |
匹配零个或一个逗号或分号。 |
\s? |
匹配零个或一个空白字符。 |
(\w+[;,]?\s?)+ |
匹配以下一个或多个事例:一个或多个单词字符,后跟一个可选逗号或分号,一个可选的空白字符。 此模式定义第一个捕获组,它是必需的,以便将重复多个单词字符的组合(即单词)后跟可选标点符号,直至正则表达式引擎到达句子末尾。 |
[.?!] |
匹配句号、问号或感叹号。 |
如以下示例所示,当找到匹配项时,GroupCollection 和 CaptureCollection 对象都会被填充上匹配项中的捕获。 在这种情况下,捕获组 (\w+[;,]?\s?)
存在,以便 +
可以向其应用限定符,这使得正则表达式模式能够匹配句子中的每个单词。 否则,它将匹配句子中的最后一个单词。
using System;
using System.Text.RegularExpressions;
public class Group1Example
{
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: '{match.Value}' at index {match.Index}.");
int grpCtr = 0;
foreach (Group grp in match.Groups)
{
Console.WriteLine($" Group {grpCtr}: '{grp.Value}' at index {grp.Index}.");
int capCtr = 0;
foreach (Capture cap in grp.Captures)
{
Console.WriteLine($" Capture {capCtr}: '{cap.Value}' at {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 Group2Example
{
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: '{match.Value}' at index {match.Index}.");
int grpCtr = 0;
foreach (Group grp in match.Groups)
{
Console.WriteLine($" Group {grpCtr}: '{grp.Value}' at index {grp.Index}.");
int capCtr = 0;
foreach (Capture cap in grp.Captures)
{
Console.WriteLine($" Capture {capCtr}: '{cap.Value}' at {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
中所有未命名或隐式捕获。 同时禁用任何未命名或隐式的嵌套捕获组进行的任何捕获。
线程安全性
类 Regex 本身是线程安全的,并且是不可变的(只读的)。 也就是说, Regex
可以在任何线程上创建对象并在线程之间共享;可以从任何线程调用匹配的方法,并且永远不会更改任何全局状态。
但是,由 Match
返回的结果对象(MatchCollection
和 Regex
)应在单个线程上使用。 尽管其中许多对象在逻辑上是不可变的,但是它们的实现可能会延迟计算某些结果以提高性能,因此,调用方必须序列化对这些对象的访问。
如果需要在多个线程上共享 Regex
结果对象,可以通过调用其同步的方法将这些对象转换为线程安全实例。 除枚举器外,所有正则表达式类都是线程安全的,也可以通过同步的方法转换为线程安全对象。
枚举器是唯一的例外。 必须序列化对集合枚举器的调用。 规则是,如果集合可以同时枚举在多个线程上,则应在枚举器遍历的集合的根对象上同步枚举器方法。
相关文章
标题 | DESCRIPTION |
---|---|
正则表达式行为的详细信息 | 检查 .NET 中正则表达式引擎的实现。 本文重点介绍正则表达式的灵活性,并解释了开发人员确保正则表达式引擎高效和可靠运行的责任。 |
回溯 | 介绍什么是回溯,以及它如何影响正则表达式性能,并检查提供回溯替代项的语言元素。 |
正则表达式语言 - 快速参考 | 介绍 .NET 中正则表达式语言的元素,并提供指向每个语言元素的详细文档的链接。 |