Menu

C# 正则表达式 Regex:匹配、替换、分割与分组示例

如何用 System.Text.RegularExpressions 在 C# 中使用正则表达式:IsMatch、Match 和 Matches,编号分组和命名分组,用替换模式和 lambda 进行 Replace,Split,RegexOptions,逐字字符串写模式,校验输入,超时,以及 .NET 7 的 GeneratedRegex 特性。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

正则表达式(regex)是一种描述文本的模式:“四个数字、一个短横线、两个数字”,“以大写字母开头的单词”,“方括号之间的任何内容”。在 C# 中,System.Text.RegularExpressions 中的 Regex 类可以用这样的模式来查找、提取、替换和分割文本。

IsMatch、Match 与 Matches

三个方法就能覆盖大多数搜索。IsMatch 回答是或否,Match 返回第一个匹配,Matches 返回所有匹配:

输出:

True
2026-03-14 at index 22
4 matches:
  1042
  2026
  1043
  2026
Success: False, Value: []

匹配失败的 Match 不会返回 null:它返回一个 Success 为 false、Value 为空的 Match。使用匹配结果之前,一定要先检查 Success。

查找四位数字的例子说明了模式会匹配到比预期更多的内容。\b\d{4}\b 的意思是“作为一个完整单词的恰好四个数字”,而日期里的年份也符合条件,因为 - 是一个单词边界。要把模式写得更精确,表达你真正的意思,例如 (?<=Order |order )\d+,或者使用下一节介绍的分组。

用逐字字符串写模式

正则语法到处都是反斜杠(\d、\s、\b),C# 字符串的转义也是。在普通字符串中,"\d" 甚至无法编译(CS1009,无法识别的转义序列),而 "\b" 会被编译成一个退格符,悄悄改变了模式。请用带 @ 的逐字字符串来写模式,在其中反斜杠就只是反斜杠:

var a = new Regex("\\d+\\.\\d{2}");   // escaped twice: hard to read
var b = new Regex(@"\d+\.\d{2}");     // verbatim: what the regex engine sees

在逐字字符串中,双引号要写成 ""。C# 11 的原始字符串字面量("""...""")连这一点都可以避免。

分组:提取匹配的各个部分

括号创建一个分组,匹配之后可以拿到每个分组的文本。分组按左括号出现的顺序从 1 开始编号;第 0 组是整个匹配。命名分组 (?<name>...) 更容易阅读,修改模式时也不会受影响:

输出:

2026-03-14
ERROR
payments
Card declined for order 1042
60 by 90, area 5400

[ 和 ] 在正则中有特殊含义(字符类),所以要匹配字面上的方括号,需要写 \[ 和 \]。. * + ? ( ) { } ^ $ | \ 也是如此。要在模式中按字面匹配用户提供的字符串,就先把它交给 Regex.Escape,它会帮你转义这些元字符:Regex.Escape("price (USD)") 返回 price\ \(USD\)(空格也会被转义,这没有坏处)。

只用来分组、不捕获的括号写作 (?:...)。它们能让分组编号保持整洁,速度也稍快一些。

Replace:替换模式与 lambda

Regex.Replace 会替换每一个匹配。在替换字符串中,$1 插入第 1 组,${name} 插入命名分组,$0 插入整个匹配。对于模板表达不了的情况,可以传入一个函数(通常是 lambda),它接收每个 Match 并返回对应的替换内容:

输出:

Invoiced 14/03/2026, paid 02/04/2026.
too many spaces
Card **** **** **** 1234
Tea 2.75, Scone 3.52, Jam 1.10

银行卡号的掩码用到了先行断言 (?=...):只有后面还跟着四个数字时,它才匹配当前这个数字,而且不会消耗后面的数字。环视((?=...)、(?!...)、(?<=...)、(?<!...))只检查上下文,不把上下文包含在匹配中,正因如此,一次 Replace 就能完整保留最后四位数字。

对于没有模式的普通文本,string.Replace 更简单也更快;当要替换的内容不固定时,再用 Regex.Replace。

Split

Regex.Split 在模式的每个匹配处进行分割,可以处理不固定的分隔符:

输出:

csharp / dotnet / regex / tutorial / beginner
Order Shipped Event Handler

如果模式中包含捕获分组,Regex.Split 会把捕获到的分隔符也放进结果里;不需要它们时,请使用 (?:...)。

RegexOptions

选项会改变整个模式的行为。用 | 组合它们:

输出:

1
2
error INFO Error
True

你会用到的选项:

  • IgnoreCase:不区分大小写的匹配(内联形式为 (?i))。
  • Multiline:^ 和 $ 匹配每一行的开头和结尾,而不只是整个字符串的开头和结尾。
  • Singleline:. 也能匹配 \n(默认情况下它匹配除换行符以外的任何字符)。
  • IgnorePatternWhitespace:忽略模式中的空格,# 开始一段注释,适合分多行书写的长模式。
  • CultureInvariant:与 IgnoreCase 一起使用时,比较不受当前区域文化规则的影响。
  • Compiled:把模式一次性编译为 IL,适合要使用很多次的正则表达式。

校验输入:锚点与邮箱问题

用于校验时,要用 ^ 和 $ 给模式加上锚点,否则只要输入的一部分匹配,校验就会通过:

输出:

PT-1000    loose=True  strict=True
pt-1000    loose=False strict=False
XPT-1000Y  loose=True  strict=False
PT-10      loose=False strict=False
ana@example.com   True
ana@example       False
ana @example.com  False
ana@@example.com  False

这个邮箱模式只检查格式:一段不含空格和 @ 的内容,一个 @,以及一个带点的域名。这是有意为之的。邮箱地址真正的语法(RFC 5322)允许带引号的本地部分、注释以及 IP 字面量形式的域名;试图覆盖这些的模式会长达几百个字符,却仍然会拒绝人们实际在用的合法地址。而且语法完全正确的地址也可能退信。先检查格式,再发送一封确认邮件。如果你不想自己写模式,System.Net.Mail.MailAddress 也提供了另一种格式检查。

性能:静态方法、实例与编译

静态方法(Regex.IsMatch(input, pattern))会解析模式,并把它保存在一个小缓存中(默认 15 个模式),所以用同一个模式重复调用的开销很小。对于在热点循环中使用的模式,创建一个 Regex 实例并保存在 static readonly 字段中;如果它要运行成千上万次,再加上 RegexOptions.Compiled,用更慢的启动换取更快的匹配。

超时与灾难性回溯

有些模式在特定输入下需要指数级的时间。经典的例子是 ^(a+)+$ 这种嵌套量词去匹配 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!":引擎会尝试把这些 a 分配给两个 + 的每一种方式,最后才放弃。在 Web 服务器上,用户发来的一个这样的输入就能让一个 CPU 核心一直满载(即“ReDoS”攻击)。凡是处理不可信输入的正则表达式,都要设置超时:

var pattern = new Regex(@"^(\w+\s?)*$", RegexOptions.None, TimeSpan.FromMilliseconds(100));
try
{
    bool ok = pattern.IsMatch(userInput);
}
catch (RegexMatchTimeoutException)
{
    // treat as invalid input
}

每个静态方法也都有一个接收超时参数的重载。.NET 7 还新增了 RegexOptions.NonBacktracking,这个引擎保证线性时间,代价是失去一些功能(环视、反向引用)。

GeneratedRegex(.NET 7)

.NET 7 新增了一个源代码生成器,它在编译时把模式转换成普通的 C# 代码。你能获得 Compiled 的速度,却没有启动开销;生成的代码可以在 IDE 中阅读,而且支持裁剪和 Native AOT:

public static partial class Patterns
{
    [GeneratedRegex(@"^[A-Z]{2}-\d{4}$", RegexOptions.IgnoreCase)]
    public static partial Regex ProductCode();
}

bool ok = Patterns.ProductCode().IsMatch("pt-1000");   // True

在 .NET 7 及以后版本中,对于编译时就已知的模式,推荐使用这种写法。

常见错误

  • 校验时缺少锚点。 没有 ^...$ 时,只要输入中包含一个匹配,IsMatch 就会接受它。
  • 用普通字符串写模式。 "\b" 是退格符,而不是单词边界。请使用 @"..."。
  • 特殊字符没有转义。 . 匹配任何字符;把 3.50 当作模式时,它也会匹配 3x50。用 \. 转义,用户输入则用 Regex.Escape 转义。
  • 贪婪量词。 用 <.*> 匹配 <b>bold</b> 会匹配整个字符串。请使用惰性的 .*? 或否定字符类 [^>]*。
  • 处理不可信输入时不设超时。 一个嵌套量词就可能让请求卡死。
  • 用正则表达式处理结构化格式。 带引号的 HTML、JSON 和 CSV 需要的是解析器,而不是模式。

常见问题

C# 中如何使用正则表达式?

添加 using System.Text.RegularExpressions;,然后调用 Regex 上的静态方法:Regex.IsMatch(input, pattern) 返回一个 bool,Regex.Match 返回第一个匹配,Regex.Matches 返回所有匹配,Regex.Replace 进行替换,Regex.Split 进行分割。把模式写成逐字字符串 @"\d+",这样反斜杠就能原样交给正则引擎。

C# 中如何从正则匹配结果中获取分组的值?

用括号把你想要的部分括起来,然后读取 match.Groups[1].Value(分组从 1 开始编号;第 0 组是整个匹配)。命名分组更清晰:(?<year>\d{4}) 用 match.Groups["year"].Value 来读取。请先检查 match.Success,因为匹配失败时分组是空的,而不是 null。

C# 中如何用正则表达式替换文本?

Regex.Replace(input, pattern, replacement) 会替换每一个匹配。替换字符串可以引用分组:$1 表示编号分组,${name} 表示命名分组,$0 表示整个匹配。如果需要模板无法表达的逻辑,可以传入一个 lambda:Regex.Replace(text, @"\d+", m => (int.Parse(m.Value) * 2).ToString())。

C# 正则表达式如何忽略大小写?

传入 RegexOptions.IgnoreCase:Regex.IsMatch(input, "error", RegexOptions.IgnoreCase)。多个选项用 | 组合,例如 RegexOptions.IgnoreCase | RegexOptions.Multiline。你也可以在模式内部用 (?i) 打开它。

C# 中如何用正则表达式校验邮箱地址?

像 ^[^@\s]+@[^@\s]+\.[^@\s]+$ 这样的模式能发现明显的输入错误(缺少 @、含有空格、域名里没有点),对表单来说通常就足够了。正则表达式无法真正验证一个地址:完整的语法允许各种实用模式都处理不了的形式,而语法上合法的地址也可能并不存在。先检查基本格式,再发送一封验证邮件来确认。

Coddy 编程语言插图

和 Coddy 一起学编程

开始