正则表达式实战
大约 14 分钟约 4174 字
正则表达式实战
简介
正则表达式(Regex)是文本匹配和替换的强大工具。C# 通过 System.Text.RegularExpressions 命名空间提供完整的正则支持。掌握常用正则模式和 .NET 特有的高级特性(命名分组、平衡组、超时控制),可以高效处理日志解析、数据验证、文本提取等场景。
正则表达式本质上是描述文本模式的一种小型语言。虽然强大,但复杂正则可能导致性能问题和可读性危机。.NET 的正则引擎在 .NET 7 中进行了大幅重写,性能提升了数倍到数十倍,配合源生成器可以获得接近手写代码的性能。
特点
常用正则模式
验证类正则
/// <summary>
/// 常用验证正则
/// </summary>
// 手机号(中国大陆)
var phonePattern = @"^1[3-9]\d{9}$";
// 邮箱
var emailPattern = @"^[\w.-]+@[\w.-]+\.\w{2,}$";
// 身份证号(18位)
var idCardPattern = @"^\d{17}[\dXx]$";
// IP 地址
var ipPattern = @"^(\d{1,3}\.){3}\d{1,3}$";
// 日期(yyyy-MM-dd)
var datePattern = @"^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$";
// URL
var urlPattern = @"^https?://[\w.-]+(:\d+)?(/[\w./-]*)?$";
// 中文
var chinesePattern = @"^[\u4e00-\u9fa5]+$";
// 密码强度(至少8位,包含大小写字母和数字)
var passwordPattern = @"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$";
// UUID
var uuidPattern = @"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$";
// 十六进制颜色
var hexColorPattern = @"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$";
// 验证方法
public static bool IsValid(string input, string pattern)
{
return Regex.IsMatch(input, pattern);
}高级验证 — 更精确的正则
// ==========================================
// 更严格的邮箱验证(RFC 5322 简化版)
// ==========================================
var strictEmail = @"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$";
// ==========================================
// IPv4 完整验证(每段 0-255)
// ==========================================
var strictIp = @"^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$";
// ==========================================
// 密码强度分级
// ==========================================
public static class PasswordStrength
{
// 弱: 至少 6 位
[GeneratedRegex(@"^.{6,}$")]
public static partial Regex Weak();
// 中: 至少 8 位,包含字母和数字
[GeneratedRegex(@"^(?=.*[a-zA-Z])(?=.*\d).{8,}$")]
public static partial Regex Medium();
// 强: 至少 8 位,包含大小写字母、数字和特殊字符
[GeneratedRegex(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$")]
public static partial Regex Strong();
public static string Check(string password)
{
if (Strong().IsMatch(password)) return "强";
if (Medium().IsMatch(password)) return "中";
if (Weak().IsMatch(password)) return "弱";
return "不满足最低要求";
}
}
// ==========================================
// 语义化版本号(SemVer)
// ==========================================
var semver = @"^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$";
// 匹配: 1.0.0, 2.1.3-beta.1, 3.0.0+build.123Regex 核心 API
匹配与提取
/// <summary>
/// Regex 匹配和提取
/// </summary>
var text = "订单号:ORD-2024-001,金额:¥1299.50,日期:2024-03-15";
// 提取所有匹配
var matches = Regex.Matches(text, @"\d+");
foreach (Match match in matches)
{
Console.WriteLine(match.Value); // 2024, 001, 1299, 50, 2024, 03, 15
}
// 命名分组提取
var orderPattern = @"订单号:(?<OrderNo>[\w-]+).*?金额:¥?(?<Amount>[\d.]+).*?日期:(?<Date>[\d-]+)";
var orderMatch = Regex.Match(text, orderPattern);
if (orderMatch.Success)
{
string orderNo = orderMatch.Groups["OrderNo"].Value; // ORD-2024-001
decimal amount = decimal.Parse(orderMatch.Groups["Amount"].Value); // 1299.50
DateTime date = DateTime.Parse(orderMatch.Groups["Date"].Value); // 2024-03-15
}
// ==========================================
// Match 的详细属性
// ==========================================
var match = Regex.Match("Hello World 123", @"\d+");
if (match.Success)
{
Console.WriteLine(match.Value); // "123" — 匹配的文本
Console.WriteLine(match.Index); // 12 — 匹配起始位置
Console.WriteLine(match.Length); // 3 — 匹配长度
Console.WriteLine(match.Groups.Count); // 分组数
}
// ==========================================
// 多匹配 — 从文本中提取所有链接
// ==========================================
var content = "访问 https://example.com 或 http://test.org/path?q=1";
var linkPattern = @"https?://[\w.-]+(?:/[\w./?=&#-]*)?";
foreach (Match link in Regex.Matches(content, linkPattern))
{
Console.WriteLine(link.Value);
}
// https://example.com
// http://test.org/path?q=1替换与分割
/// <summary>
/// 正则替换和分割
/// </summary>
// 替换手机号中间四位
var masked = Regex.Replace("13812345678", @"(\d{3})\d{4}(\d{4})", "$1****$2");
// 138****5678
// 使用 MatchEvaluator 动态替换
var html = "<div>Hello</div><p>World</p>";
var plain = Regex.Replace(html, @"<(\w+)[^>]*>(.*?)</\1>", m => m.Groups[2].Value);
// HelloWorld
// 分割
var parts = Regex.Split("a,b;;c,,,d", @",+");
// ["a", "b", "c", "d"]
// 驼峰转下划线
var snakeCase = Regex.Replace("UserName", @"([A-Z])", "_$1").TrimStart('_').ToLower();
// user_name
// ==========================================
// 更多实用替换模式
// ==========================================
// 移除 HTML 标签
var cleanHtml = Regex.Replace("<p>Hello <b>World</b></p>", @"<[^>]+>", "");
// Hello World
// 格式化电话号码
var formatted = Regex.Replace("13812345678", @"(\d{3})(\d{4})(\d{4})", "($1) $2-$3");
// (138) 1234-5678
// 提取货币数值
var moneyText = "总计:¥1,299.50,优惠:-¥200.00";
var amounts = Regex.Matches(moneyText, @"-?¥([\d,]+\.?\d*)");
foreach (Match m in amounts)
Console.WriteLine(m.Groups[1].Value); // 1,299.50 和 200.00
// 重复空格合并
var collapsed = Regex.Replace("hello world test", @"\s+", " ");
// hello world test
// ==========================================
// 分割高级用法 — 保留分隔符
// ==========================================
var input = "a1b2c3d";
// 使用正则分割并保留分隔符
var result = Regex.Split(input, @"(\d)");
// ["a", "1", "b", "2", "c", "3", "d"]
// 注意: 括号内的分组会保留在结果中.NET 特有功能 — 平衡组
// ==========================================
// 平衡组 — 匹配嵌套结构(.NET 特有!)
// ==========================================
// 匹配配对的圆括号(支持嵌套)
var nestedPattern = @"\((?:[^()]|(?<Open>\()|(?<-Open>\)))*(?(Open)(?!))\)";
// 解释:
// (?<Open>\() — 遇到 ( 时,Open 计数器 +1
// (?<-Open>\)) — 遇到 ) 时,Open 计数器 -1
// (?(Open)(?!)) — 如果 Open > 0,匹配失败(不配对)
var test1 = "((a+b)*(c-d))";
var match1 = Regex.Match(test1, nestedPattern);
Console.WriteLine(match1.Value); // ((a+b)*(c-d))
var test2 = "((a+b)"; // 不配对
var match2 = Regex.Match(test2, nestedPattern);
Console.WriteLine(match2.Success); // False
// ==========================================
// 实际应用 — 匹配嵌套的 HTML 标签
// ==========================================
var divPattern = @"<div(?:\s[^>]*)?>((?:[^<>]|<(?!/?div\b)[^>]*>)*)</div>";
// 注意:正则不是处理 HTML 的最佳工具
// 复杂 HTML 建议使用 HtmlAgilityPack 或 AngleSharp
// 但简单的标签提取正则够用
// ==========================================
// 条件匹配
// ==========================================
// (?(group)yes|no) — 如果 group 匹配了,匹配 yes,否则匹配 no
var conditionalPattern = @"(<)?(\w+@\w+\.\w+)(?(1)>|$)";
// 如果以 < 开头,必须以 > 结尾
// 否则必须在行尾结束源生成器(.NET 7+)
编译时正则
/// <summary>
/// RegexGenerator — 编译时生成正则引擎
/// </summary>
public partial class RegexPatterns
{
[GeneratedRegex(@"^1[3-9]\d{9}$")]
public static partial Regex PhoneRegex();
[GeneratedRegex(@"^[\w.-]+@[\w.-]+\.\w{2,}$", RegexOptions.IgnoreCase)]
public static partial Regex EmailRegex();
[GeneratedRegex(@"\b\d{4}-\d{2}-\d{2}\b")]
public static partial Regex DateRegex();
[GeneratedRegex(@"\{(\w+)\}")]
public static partial Regex TemplateVariableRegex();
}
// 使用 — 零运行时开销
bool isValidPhone = RegexPatterns.PhoneRegex().IsMatch("13812345678");
bool isValidEmail = RegexPatterns.EmailRegex().IsMatch("test@example.com");
// 模板变量替换
var template = "你好 {Name},你的订单 {OrderId} 已发货";
var result = RegexPatterns.TemplateVariableRegex().Replace(template, m =>
{
return m.Groups[1].Value switch
{
"Name" => "张三",
"OrderId" => "ORD-001",
_ => m.Value
};
});源生成器的优势与限制
// ==========================================
// [GeneratedRegex] vs RegexOptions.Compiled
// ==========================================
// [GeneratedRegex]:
// - 编译时生成代码,启动时零开销
// - AOT 兼容(Native AOT 可以使用)
// - 生成的方法是 partial,可以单步调试
// - 不支持动态模式(模式必须是编译时常量)
// RegexOptions.Compiled:
// - 运行时使用 Reflection.Emit 编译
// - 首次匹配有编译开销
// - 不 AOT 兼容
// - 支持动态模式
// ==========================================
// 带命名分组的源生成器
// ==========================================
public partial class OrderParser
{
[GeneratedRegex(
@"订单号:(?<OrderNo>[\w-]+).*?金额:[¥¥]?(?<Amount>[\d,.]+).*?日期:(?<Date>[\d-]+)",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
public static partial Regex OrderPattern();
}
// 使用
var orderText = "订单号:ORD-2024-001,金额:¥1,299.50,日期:2024-03-15";
var orderMatch = OrderParser.OrderPattern().Match(orderText);
if (orderMatch.Success)
{
var orderNo = orderMatch.Groups["OrderNo"].Value;
var amount = decimal.Parse(orderMatch.Groups["Amount"].Value,
NumberStyles.Currency);
var date = DateOnly.Parse(orderMatch.Groups["Date"].Value);
}
// ==========================================
// 源生成器 + 超时控制
// ==========================================
public partial class SafePatterns
{
[GeneratedRegex(@"^(a+)+$", RegexOptions.None, matchTimeoutMilliseconds: 1000)]
public static partial Regex DangerousPattern();
}性能与安全
超时与编译选项
/// <summary>
/// 正则性能优化和安全
/// </summary>
// 超时控制 — 防止灾难性回溯
var safeRegex = new Regex(@"^(a+)+$", RegexOptions.None, matchTimeout: TimeSpan.FromSeconds(1));
try
{
safeRegex.IsMatch("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaab"); // 超时保护
}
catch (RegexMatchTimeoutException)
{
Console.WriteLine("正则匹配超时");
}
// 预编译(多次使用时)
var compiledRegex = new Regex(@"\d{4}-\d{2}-\d{2}",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
// 静态缓存(.NET 自动缓存最近使用的正则)
// 相同模式字符串的 new Regex() 会命中缓存
// 性能建议
// 1. 频繁使用 → [GeneratedRegex] 源生成器
// 2. 多次使用 → new Regex(pattern, RegexOptions.Compiled) 预编译
// 3. 一次性使用 → Regex.IsMatch/Match 静态方法
// 4. 用户输入模式 → 必须设置超时灾难性回溯详解
// ==========================================
// 什么是灾难性回溯?
// ==========================================
// 某些正则模式在特定输入下会导致指数级时间复杂度
// 例如: ^(a+)+$ 匹配 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaab"
// 引擎会尝试所有可能的分组方式,时间复杂度为 O(2^n)
// 常见的危险模式:
// 1. 嵌套量词: (a+)+, (a*)*, (.+)+
// 2. 重叠交替: (a|a)*, (ab|a)*
// 3. 大量回溯: .*.*.*.*
// ==========================================
// 如何避免灾难性回溯
// ==========================================
// 方法 1: 设置超时(最重要!)
var withTimeout = new Regex(pattern, RegexOptions.None,
matchTimeout: TimeSpan.FromSeconds(1));
// 方法 2: 使用占有量词(.NET 7+ 不完全支持,但可以用原子组)
// 原子组 (?>...) — 一旦匹配就不可回溯
var atomic = new Regex(@"^(?>a+)+$");
// 这会在 O(n) 时间内完成,不会灾难性回溯
// 方法 3: 避免嵌套量词
// 不好: (a+)+
// 好: a+
// 方法 4: 使用更具体的字符类
// 不好: (.|\n)*
// 好: [\s\S]* 或使用 RegexOptions.Singleline 的 .
// ==========================================
// 正则性能基准测试
// ==========================================
/*
| 方法 | 1000次匹配耗时 |
|------------------------|--------------|
| 静态 Regex.IsMatch | ~500us |
| new Regex() 实例 | ~450us |
| Compiled | ~200us |
| GeneratedRegex (.NET 7)| ~5us |
| 手写 string.IndexOf | ~2us |
*/实际应用 — 日志解析
/// <summary>
/// 结构化日志解析器
/// </summary>
public partial class LogParser
{
// 匹配标准日志格式: [时间] [级别] 消息
[GeneratedRegex(
@"\[(?<Timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\] " +
@"\[(?<Level>DEBUG|INFO|WARN|ERROR|FATAL)\] " +
@"(?<Message>.*)")]
public static partial Regex LogPattern();
// 匹配带线程和类名的日志
[GeneratedRegex(
@"(?<Timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+" +
@"\[(?<Thread>\d+)\]\s+" +
@"(?<Level>\w+)\s+" +
@"(?<Logger>[\w.]+)\s+-\s+" +
@"(?<Message>.*)")]
public static partial Regex DetailedLogPattern();
public static LogEntry? Parse(string line)
{
var match = DetailedLogPattern().Match(line);
if (!match.Success) return null;
return new LogEntry(
Timestamp: DateTime.Parse(match.Groups["Timestamp"].Value),
ThreadId: int.Parse(match.Groups["Thread"].Value),
Level: Enum.Parse<LogLevel>(match.Groups["Level"].Value),
Logger: match.Groups["Logger"].Value,
Message: match.Groups["Message"].Value
);
}
}
public record LogEntry(
DateTime Timestamp,
int ThreadId,
LogLevel Level,
string Logger,
string Message);
public enum LogLevel { DEBUG, INFO, WARN, ERROR, FATAL }
// 使用
var logLines = File.ReadAllLines("app.log");
var errors = logLines
.Select(LogParser.Parse)
.Where(e => e is not null && e.Level >= LogLevel.ERROR)
.ToList();实际应用 — 文本模板引擎
/// <summary>
/// 简单的模板引擎 — 基于 Regex 的变量替换
/// </summary>
public partial class TemplateEngine
{
[GeneratedRegex(@"\{\{(?<Key>\w+(?:\.\w+)*)\}\}")]
public static partial Regex VariablePattern();
[GeneratedRegex(@"\{%\s*(?<Condition>\w+)\s*%\}(?<Content>.*?)\{%\s*end\w+\s*%\}",
RegexOptions.Singleline)]
public static partial Regex ConditionalPattern();
private readonly Dictionary<string, object> _data;
public TemplateEngine(Dictionary<string, object> data) => _data = data;
public string Render(string template)
{
// 先处理变量替换
var result = VariablePattern().Replace(template, match =>
{
var key = match.Groups["Key"].Value;
return GetValue(key)?.ToString() ?? match.Value;
});
return result;
}
private object? GetValue(string key)
{
var parts = key.Split('.');
object? current = _data;
foreach (var part in parts)
{
if (current == null) return null;
var type = current.GetType();
var prop = type.GetProperty(part);
if (prop == null) return null;
current = prop.GetValue(current);
}
return current;
}
}
// 使用
var template = "尊敬的{{Customer.Name}},您的订单{{Order.Id}}已发货," +
"金额:¥{{Order.Amount}},预计{{Order.EstimatedDelivery}}送达。";
var engine = new TemplateEngine(new Dictionary<string, object>
{
["Customer.Name"] = "张三",
["Order.Id"] = "ORD-2024-001",
["Order.Amount"] = "1,299.50",
["Order.EstimatedDelivery"] = "2024-03-20",
});
var output = engine.Render(template);RegexOptions 完整参考
// ==========================================
// RegexOptions 枚举值
// ==========================================
// IgnoreCase — 忽略大小写
// Multiline — ^ 和 $ 匹配每行的开头和结尾
// Singleline — . 匹配所有字符(包括 \n)
// ExplicitCapture — 只有 (?<name>...) 才捕获,( ) 不捕获
// Compiled — 编译为 IL(运行时开销大,匹配更快)
// IgnorePatternWhitespace — 忽略正则中的空白和注释
// RightToLeft — 从右向左搜索
// CultureInvariant — 忽略区域文化差异
// ECMAScript — 使用 ECMAScript 兼容行为
// NonBacktracking — .NET 7+ 非回溯引擎(保证线性时间)
// ==========================================
// IgnorePatternWhitespace — 可读性正则
// ==========================================
var readablePattern = new Regex(@"
^ # 行首
(?<Protocol>https?) # 协议
:// # 分隔符
(?<Domain>[\w.-]+) # 域名
(?::(?<Port>\d+))? # 可选端口
(?<Path>/[\w./-]*)? # 可选路径
$ # 行尾
", RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);优点
缺点
总结
正则表达式适合日志解析、数据验证、文本提取等场景。.NET 7+ 推荐使用 [GeneratedRegex] 源生成器获得最佳性能。安全关键:始终为用户输入的正则设置超时。简单字符串操作用 String 方法(Contains/StartsWith),复杂模式匹配才用正则。
选择指南:
- 固定模式频繁匹配 — [GeneratedRegex] 源生成器
- 运行时动态模式 — new Regex + RegexOptions.NonBacktracking
- 一次性验证 — Regex.IsMatch 静态方法
- 提取结构化数据 — 命名分组 + Match.Groups
- 用户输入的正则 — 必须设置 matchTimeout
关键知识点
- 先明确这个主题影响的是语法层、运行时层,还是性能与可维护性层。
- 学习时要同时关注语言表面写法和编译器、JIT、GC 等底层行为。
- 真正有价值的是知道"为什么这样写"和"在什么边界下不能这样写"。
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
常见误区
- 只记语法糖,不知道底层成本。
- 把适用于小样例的写法直接搬到高并发或大对象场景里。
- 忽略框架版本、语言版本和运行时差异,导致结论失真。
- 用正则解析 HTML/XML(应该用专用解析器)。
- 忘记设置超时导致灾难性回溯 CPU 100%。
- 使用 .* 匹配多行内容时忘记 RegexOptions.Singleline。
- 在循环中反复 new Regex 而不是复用实例。
- 用正则做简单的字符串包含检查(string.Contains 更快)。
进阶路线
- 继续向源码、IL、JIT 行为和 BCL 实现层深入。
- 把知识点和代码评审、性能诊断、面试复盘结合起来。
- 把同类主题做横向对比,例如值类型与引用类型、迭代器与 async 状态机、反射与 Source Generator。
- 学习正则的可视化调试工具(regex101.com、regexr.com)。
- 了解 .NET 7 非回溯引擎的实现原理。
适用场景
- 当你准备把《正则表达式实战》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在需要理解语言特性、运行时行为或 API 边界时阅读。
- 当代码开始出现性能瓶颈、可维护性问题或语义歧义时,这类主题会直接影响实现质量。
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了"高级"而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
- 用 regex101.com 等工具验证正则是否匹配预期。
- 检查是否设置了超时防止灾难性回溯。
复盘问题
- 如果把《正则表达式实战》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《正则表达式实战》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《正则表达式实战》最大的收益和代价分别是什么?
