03-正则表达式(Regular Expression)
正则表达式(Regex)用于匹配、查找和替换符合特定模式的文本。C# 通过 System.Text.RegularExpressions 命名空间提供正则支持。
一、基本用法
csharp
using System.Text.RegularExpressions;
string text = "Hello, my email is user@example.com and admin@test.org";
string pattern = @"\w+@\w+\.\w+";
// 判断是否匹配
bool isMatch = Regex.IsMatch(text, pattern);
Console.WriteLine(isMatch); // True
// 获取第一个匹配
Match match = Regex.Match(text, pattern);
if (match.Success)
{
Console.WriteLine(match.Value); // user@example.com
Console.WriteLine($"索引:{match.Index}"); // 匹配起始位置
Console.WriteLine($"长度:{match.Length}"); // 匹配长度
}
// 获取所有匹配
MatchCollection matches = Regex.Matches(text, pattern);
Console.WriteLine($"找到 {matches.Count} 个匹配");
foreach (Match m in matches)
{
Console.WriteLine(m.Value); // user@example.com, admin@test.org
}二、正则表达式元字符大全
1. 基本元字符
| 字符 | 说明 | 示例 | 匹配 |
|---|---|---|---|
. | 任意字符(除换行) | a.b | "aab"、"acb" |
\d | 数字 [0-9] | \d{3} | 三位数字 |
\D | 非数字 | \D+ | 非数字字符序列 |
\w | 单词字符 [a-zA-Z0-9_] | \w+ | 单词 |
\W | 非单词字符 | \W | 空格、符号等 |
\s | 空白字符(空格、制表符、换行) | \s+ | 空白序列 |
\S | 非空白字符 | \S+ | 非空字符序列 |
\b | 单词边界 | \bword\b | 完整单词 "word" |
\B | 非单词边界 | \Bword | 以 word 结尾的单词 |
^ | 字符串/行开头 | ^Hello | 以 Hello 开头 |
$ | 字符串/行结尾 | end$ | 以 end 结尾 |
2. 量词
| 字符 | 说明 | 示例 | 匹配 |
|---|---|---|---|
* | 零次或多次(贪婪) | a* | ""、"a"、"aa" |
+ | 一次或多次(贪婪) | a+ | "a"、"aa" |
? | 零次或一次 | a? | "" 或 "a" |
{n} | 恰好 n 次 | \d{3} | 3 位数字 |
{n,} | 至少 n 次 | \d{2,} | 2 位及以上数字 |
{n,m} | n 到 m 次 | \d{2,4} | 2~4 位数字 |
*? | 零次或多次(非贪婪) | a*? | 尽可能少匹配 |
+? | 一次或多次(非贪婪) | a+? | 至少一次但尽量少 |
3. 字符类
| 字符 | 说明 | 示例 |
|---|---|---|
[abc] | 匹配 a、b 或 c | [aeiou] 匹配元音字母 |
[^abc] | 匹配除 a、b、c 外的字符 | [^0-9] 匹配非数字 |
[a-z] | 小写字母范围 | [a-zA-Z] 匹配所有字母 |
[0-9] | 数字范围 | [0-9a-fA-F] 匹配十六进制字符 |
4. 分组和引用
| 字符 | 说明 | 示例 |
|---|---|---|
(pattern) | 捕获分组 | (\d{3})-(\d{4}) |
(?<name>pattern) | 命名分组 | (?<year>\d{4}) |
(?:pattern) | 非捕获分组 | (?:\d{3})(不捕获,仅分组) |
\1 | 反向引用第一个分组 | (\w)\1 匹配重复字符 |
\k<name> | 反向引用命名分组 | (?<q>['"]).*\k<q> |
5. 零宽断言(Lookaround)
| 字符 | 说明 | 示例 |
|---|---|---|
(?=pattern) | 正向先行断言 | \d(?=px) 匹配后面是 px 的数字 |
(?!pattern) | 负向先行断言 | \d(?!px) 匹配后面不是 px 的数字 |
(?<=pattern) | 正向后发断言 | (?<=\$)\d+ 匹配 $ 后面的数字 |
(?<!pattern) | 负向后发断言 | (?<!\$)\d+ 匹配不是 $ 后面的数字 |
csharp
// 零宽断言示例
string text = "价格: $100, 折扣: 20%, 数量: 5个";
// 正向先行断言:匹配后面是 % 的数字
Match m1 = Regex.Match(text, @"\d+(?=%)");
Console.WriteLine(m1.Value); // 20
// 负向先行断言:匹配后面不是 px 的数字
Match m2 = Regex.Match(text, @"\d+(?!%|个)");
Console.WriteLine(m2.Value); // 100(匹配到 100 而不是 20 或 5)
// 正向后发断言:匹配 $ 后面的数字
Match m3 = Regex.Match(text, @"(?<=\$)\d+");
Console.WriteLine(m3.Value); // 1006. 贪婪 vs 非贪婪
csharp
string text = "<div>内容1</div><span>内容2</span>";
// 贪婪模式(默认):匹配尽可能多的字符
Match greedy = Regex.Match(text, @"<.+>");
Console.WriteLine(greedy.Value); // <div>内容1</div><span>内容2</span>
// 非贪婪模式:匹配尽可能少的字符
Match lazy = Regex.Match(text, @"<.+?>");
Console.WriteLine(lazy.Value); // <div>
// 非贪婪示例:提取所有标签
MatchCollection tags = Regex.Matches(text, @"<.+?>");
foreach (Match tag in tags)
Console.WriteLine(tag.Value); // <div>, </div>, <span>, </span>三、常用正则表达式参考
验证类
| 用途 | 正则表达式 | 说明 |
|---|---|---|
| 手机号(中国) | ^1[3-9]\d{9}$ | 11 位,1 开头,第二位 3-9 |
| 邮箱 | ^[\w.-]+@[\w.-]+\.\w{2,4}$ | 标准邮箱格式 |
| IP 地址 | ^(\d{1,3}\.){3}\d{1,3}$ | IPv4 格式 |
| URL | ^https?://[\w./?=&-]+$ | HTTP/HTTPS 链接 |
| 身份证(18位) | ^\d{17}[\dXx]$ | 最后一位可以是数字或 X |
| 强密码 | ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$ | 至少8位,含大小写字母、数字、特殊字符 |
| 日期(YYYY-MM-DD) | ^\d{4}-\d{2}-\d{2}$ | 日期格式 |
| 中文 | ^[一-龥]+$ | 纯中文字符 |
| 邮政编码 | ^\d{6}$ | 6 位数字 |
| QQ 号 | ^[1-9]\d{4,10}$ | 5-11 位数字,不能以 0 开头 |
提取类
| 用途 | 正则表达式 | 提取内容 |
|---|---|---|
| HTML 标签 | <[^>]+> | <div>、<br/> |
| HTML 标签内文本 | >(.*?)< | 标签间文本 |
| URL | https?://[\w./?=&-]+ | URL 链接 |
| 纯数字 | -?\d+(\.\d+)? | 整数和小数 |
| @ 提及 | @\w+ | @user、@张三 |
| # 话题 | #\w+# | #话题# |
| 金额 | [$¥]\d+(\.\d{2})? | $99.99、¥100 |
四、分组和捕获
1. 基本捕获分组
csharp
string text = "Name: 张三, Age: 25, City: 北京";
string pattern = @"Name: (\w+), Age: (\d+), City: (\w+)";
Match match = Regex.Match(text, pattern);
if (match.Success)
{
Console.WriteLine($"完整匹配:{match.Value}"); // 完整字符串
Console.WriteLine($"分组数量:{match.Groups.Count}"); // 4(索引0是完整匹配)
for (int i = 0; i < match.Groups.Count; i++)
{
Console.WriteLine($"Groups[{i}] = {match.Groups[i].Value}");
}
// Groups[1] = 张三
// Groups[2] = 25
// Groups[3] = 北京
}2. 命名分组
csharp
string text = "Name: 张三, Age: 25, City: 北京";
string pattern = @"Name: (?<name>\w+), Age: (?<age>\d+), City: (?<city>\w+)";
Match match = Regex.Match(text, pattern);
if (match.Success)
{
Console.WriteLine($"姓名:{match.Groups["name"].Value}"); // 张三
Console.WriteLine($"年龄:{match.Groups["age"].Value}"); // 25
Console.WriteLine($"城市:{match.Groups["city"].Value}"); // 北京
}3. 非捕获分组
csharp
string text = "2024-10-01 2025-01-15";
// 捕获分组——会保存匹配内容
string pattern1 = @"(\d{4})-\d{2}-\d{2}";
MatchCollection matches1 = Regex.Matches(text, pattern1);
foreach (Match m in matches1)
Console.WriteLine(m.Groups[1].Value); // 2024, 2025
// 非捕获分组 (?:...) ——不保存,性能更好
string pattern2 = @"(?:\d{4})-\d{2}-\d{2}";
MatchCollection matches2 = Regex.Matches(text, pattern2);
foreach (Match m in matches2)
Console.WriteLine(m.Groups.Count); // 只有 1 个分组(整个匹配)4. 反向引用
csharp
// 匹配重复的单词
string text = "hello hello world";
string pattern = @"\b(\w+)\s+\1\b"; // \1 引用第一个分组
Match match = Regex.Match(text, pattern);
Console.WriteLine(match.Success); // True
Console.WriteLine(match.Value); // hello hello
// 匹配成对标签
string html = "<div>内容</div><p>文本</p><div>其他</span>";
string tagPattern = @"<(?<tag>\w+)>.*?</\k<tag>>";
MatchCollection tags = Regex.Matches(html, tagPattern);
foreach (Match m in tags)
Console.WriteLine(m.Value); // <div>内容</div>, <p>文本</p>五、替换文本
1. 基本替换
csharp
string text = "手机号:13800138000,请勿泄露";
// 手机号脱敏:138****8000
string result = Regex.Replace(text, @"(\d{3})\d{4}(\d{4})", "$1****$2");
Console.WriteLine(result); // 手机号:138****8000,请勿泄露2. 使用命名分组替换
csharp
string text = "2024-10-01";
string result = Regex.Replace(text,
@"(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})",
"${month}/${day}/${year}");
Console.WriteLine(result); // 10/01/20243. 使用求值器(MatchEvaluator)
csharp
// 价格打九折
string text = "价格: $100, $200, $300";
string result = Regex.Replace(text, @"\$\d+", m =>
{
int price = int.Parse(m.Value.TrimStart('$'));
return $"${price * 0.9:F0}"; // 打九折
});
Console.WriteLine(result); // 价格: $90, $180, $270
// 更复杂的替换:将 Markdown 链接转为 HTML
string md = "访问 [Google](https://google.com) 或 [Bing](https://bing.com)";
string html = Regex.Replace(md, @"\[(.+?)\]\((.+?)\)", "<a href=\"$2\">$1</a>");
Console.WriteLine(html);
// 访问 <a href="https://google.com">Google</a> 或 <a href="https://bing.com">Bing</a>替换模式中的特殊字符
| 字符 | 说明 |
|---|---|
$1、$2 | 按索引引用捕获分组 |
${name} | 按名称引用命名分组 |
$$ | 替换为 $ 字符本身 |
$& | 替换为整个匹配 |
| ``` $`` | 替换为匹配前的文本 |
$' | 替换为匹配后的文本 |
六、RegexOptions 详解
| 选项 | 说明 | 用途 |
|---|---|---|
IgnoreCase | 忽略大小写匹配 | Regex.IsMatch("Hello", "hello", IgnoreCase) → True |
Multiline | 多行模式,^ 和 $ 匹配每行开头/结尾 | 逐行匹配时使用 |
Singleline | 单行模式,. 匹配换行符 | 跨行匹配时使用 |
Compiled | 编译为正则程序集(首次稍慢,后续极快) | 频繁使用的正则 |
IgnorePatternWhitespace | 忽略模式中的空格和注释 | 复杂正则加注释 |
ExplicitCapture | 仅显式命名或编号的分组才捕获 | 提高性能 |
RightToLeft | 从右向左匹配 | 特殊场景 |
csharp
string text = "Hello\nWorld\nHELLO";
// 默认:区分大小写
bool caseSensitive = Regex.IsMatch(text, "hello"); // False
// 忽略大小写
bool caseInsensitive = Regex.IsMatch(text, "hello", RegexOptions.IgnoreCase); // True
// 多行匹配:^ $ 匹配每行的开头结尾
MatchCollection multiLine = Regex.Matches(text, @"^\w+$", RegexOptions.Multiline);
foreach (Match m in multiLine)
Console.WriteLine(m.Value); // Hello, World, HELLO
// 单行匹配:. 匹配换行符
Match singleLine = Regex.Match(text, @"Hello.*HELLO", RegexOptions.Singleline);
Console.WriteLine(singleLine.Success); // True(跨行匹配)
// 组合多个选项
Regex combined = new Regex(@"^\w+$",
RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.Compiled);七、编译正则表达式
csharp
// 频繁使用时,创建 Regex 对象并编译
Regex emailRegex = new Regex(@"^[\w.-]+@[\w.-]+\.\w{2,4}$",
RegexOptions.Compiled);
// 多次使用——比静态 Regex.IsMatch 快得多
bool r1 = emailRegex.IsMatch("test@example.com");
bool r2 = emailRegex.IsMatch("user@domain.org");
bool r3 = emailRegex.IsMatch("invalid");
// 源生成器(.NET 7+,编译时生成最优代码)
[GeneratedRegex(@"^[\w.-]+@[\w.-]+\.\w{2,4}$")]
private static partial Regex EmailRegex();
// 使用源生成器版本——零运行时开销
bool isValid = EmailRegex().IsMatch("test@example.com");静态方法 vs 实例性能对比
| 使用方式 | 首次使用 | 后续使用 | 推荐场景 |
|---|---|---|---|
Regex.IsMatch(text, pattern) | 慢(解析+编译) | 慢(每次都解析) | 偶尔使用 |
new Regex(pattern) | 慢(实例化时解析) | 中等 | 中等频率 |
new Regex(pattern, Compiled) | 最慢(编译) | 最快 | 高频使用 |
[GeneratedRegex] 源生成器 | 最快(编译时生成) | 最快 | 任意频率(推荐) |
八、性能优化与灾难性回溯
灾难性回溯
csharp
// ❌ 危险:嵌套量词导致灾难性回溯
string badPattern = @"(<.*>)+"; // 不要用于复杂字符串!
// 输入较长时会导致 CPU 100%,几乎死锁
// ✅ 安全:使用非贪婪匹配
string safePattern = @"(<.*?>)";
// ✅ 更安全:使用更精确的匹配
string precisePattern = @"<[^>]*>";性能优化建议
csharp
// 1. 使用非捕获分组 (?:...) 代替捕获分组 (...)
// 不需要提取时使用非捕获
Regex.Match(text, @"(?:\d{3})-(?:\d{4})");
// 2. 使用 RegexOptions.ExplicitCapture
// 只捕获显式命名的分组
Regex.Match(text, pattern, RegexOptions.ExplicitCapture);
// 3. 避免 .* 和 .+ 开头
// ❌ 不好的:开头就匹配任意字符
// ✅ 好的:尽量具体 ^\d{3}
// 4. 使用 Regex.IsMatch 进行快速检查
// 如果只需要判断是否存在,IsMatch 比 Match 快
// 5. 设置超时防止灾难性回溯(.NET 5+)
try
{
bool match = Regex.IsMatch(text, pattern, RegexOptions.None, TimeSpan.FromSeconds(1));
}
catch (RegexMatchTimeoutException)
{
Console.WriteLine("匹配超时");
}九、综合案例
案例一:日志文件解析器
csharp
public class LogEntry
{
public DateTime Timestamp { get; set; }
public string Level { get; set; }
public string Component { get; set; }
public string Message { get; set; }
}
public static class LogParser
{
// 日志格式:[2024-10-01 14:30:25] [ERROR] [Database] 连接超时
private static readonly Regex LogPattern = new Regex(
@"\[(?<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] " +
@"\[(?<level>\w+)\] " +
@"\[(?<component>\w+)\] " +
@"(?<message>.*)",
RegexOptions.Compiled);
public static List<LogEntry> ParseLog(string logText)
{
var entries = new List<LogEntry>();
foreach (Match match in LogPattern.Matches(logText))
{
entries.Add(new LogEntry
{
Timestamp = DateTime.Parse(match.Groups["time"].Value),
Level = match.Groups["level"].Value,
Component = match.Groups["component"].Value,
Message = match.Groups["message"].Value
});
}
return entries;
}
public static Dictionary<string, int> CountByLevel(List<LogEntry> logs)
{
return logs.GroupBy(l => l.Level)
.ToDictionary(g => g.Key, g => g.Count());
}
}
// 使用
string logs = @"
[2024-10-01 14:30:25] [ERROR] [Database] 连接超时
[2024-10-01 14:30:26] [INFO] [Auth] 用户登录成功
[2024-10-01 14:30:27] [WARN] [Memory] 使用率超过 80%
[2024-10-01 14:30:28] [ERROR] [Database] 查询失败";
var parsed = LogParser.ParseLog(logs);
var stats = LogParser.CountByLevel(parsed);
foreach (var kv in stats)
Console.WriteLine($"{kv.Key}: {kv.Value} 条");
// ERROR: 2 条
// INFO: 1 条
// WARN: 1 条案例二:数据提取与验证工具
csharp
public static class DataExtractor
{
// 提取所有 URL
public static List<string> ExtractUrls(string text)
{
var pattern = @"https?://[\w./?=&-]+";
return Regex.Matches(text, pattern)
.Select(m => m.Value)
.ToList();
}
// 验证强密码
public static bool IsStrongPassword(string password)
{
// 至少8位,包含大小写字母、数字、特殊字符
string pattern = @"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$";
return Regex.IsMatch(password, pattern);
}
// 去除 HTML 标签
public static string StripHtml(string html)
{
return Regex.Replace(html, @"<[^>]+>", "").Trim();
}
// 驼峰命名转下划线
public static string CamelToSnake(string input)
{
return Regex.Replace(input, @"([a-z])([A-Z])", "$1_$2").ToLower();
}
// 下划线命名转驼峰
public static string SnakeToCamel(string input)
{
return Regex.Replace(input, @"_([a-z])", m => m.Groups[1].Value.ToUpper());
}
// 提取所有 @ 提及
public static List<string> ExtractMentions(string text)
{
return Regex.Matches(text, @"@\w+")
.Select(m => m.Value.TrimStart('@'))
.ToList();
}
// 过滤表情符号
public static string RemoveEmojis(string text)
{
return Regex.Replace(text, @"[\u{1F600}-\u{1F64F}" +
@"\u{1F300}-\u{1F5FF}" +
@"\u{1F680}-\u{1F6FF}" +
@"\u{2600}-\u{26FF}]", "");
}
// 格式化金额:1234567 → 1,234,567.00
public static string FormatMoney(decimal amount)
{
return amount.ToString("N2");
}
}
// 测试
Console.WriteLine(DataExtractor.CamelToSnake("FirstName")); // first_name
Console.WriteLine(DataExtractor.SnakeToCamel("first_name")); // firstName
Console.WriteLine(DataExtractor.StripHtml("<p>Hello <b>World</b></p>")); // Hello World
Console.WriteLine(DataExtractor.IsStrongPassword("Abc123!@")); // True案例三:配置文件解析器
csharp
public class ConfigParser
{
private readonly Regex sectionPattern = new Regex(@"^\[(?<section>\w+)\]$");
private readonly Regex keyValuePattern = new Regex(@"^(?<key>\w+)\s*=\s*(?<value>.+)$");
private readonly Regex commentPattern = new Regex(@"^\s*[;#]");
public Dictionary<string, Dictionary<string, string>> Parse(string configText)
{
var result = new Dictionary<string, Dictionary<string, string>>();
string currentSection = "General";
result[currentSection] = new Dictionary<string, string>();
foreach (string line in configText.Split('\n'))
{
string trimmed = line.Trim();
if (string.IsNullOrEmpty(trimmed) || commentPattern.IsMatch(trimmed))
continue;
Match sectionMatch = sectionPattern.Match(trimmed);
if (sectionMatch.Success)
{
currentSection = sectionMatch.Groups["section"].Value;
if (!result.ContainsKey(currentSection))
result[currentSection] = new Dictionary<string, string>();
continue;
}
Match kvMatch = keyValuePattern.Match(trimmed);
if (kvMatch.Success)
{
result[currentSection][kvMatch.Groups["key"].Value] =
kvMatch.Groups["value"].Value;
}
}
return result;
}
}
// 使用
var parser = new ConfigParser();
string config = @"
; 数据库配置
[Database]
Server = localhost
Port = 3306
Name = testdb
# 日志配置
[Logging]
Level = INFO
File = app.log
";
var result = parser.Parse(config);
Console.WriteLine(result["Database"]["Server"]); // localhost
Console.WriteLine(result["Logging"]["Level"]); // INFO核心知识点总结
正则表达式核心 API
| 方法 | 说明 | 返回值 |
|---|---|---|
Regex.IsMatch() | 判断是否匹配 | bool |
Regex.Match() | 获取第一个匹配 | Match |
Regex.Matches() | 获取所有匹配 | MatchCollection |
Regex.Replace() | 替换匹配文本 | string |
Regex.Split() | 按匹配位置分割 | string[] |
分组类型
| 类型 | 语法 | 用途 |
|---|---|---|
| 捕获分组 | (...) | 提取并编号 |
| 命名分组 | (?<name>...) | 按名称提取 |
| 非捕获分组 | (?:...) | 仅分组,不捕获 |
| 零宽断言正向 | (?=...) / (?!...) | 条件匹配,不消耗字符 |
| 零宽断言反向 | (?<=...) / (?<!...) | 条件匹配,不消耗字符 |
贪婪 vs 非贪婪
| 量词 | 贪婪 | 非贪婪 |
|---|---|---|
| 零次或多次 | * | *? |
| 一次或多次 | + | +? |
| 可选 | ? | ?? |
| 指定次数 | {n,m} | {n,m}? |
注意事项
- 使用 @ 逐字字符串——避免反斜杠转义(
@"\d"而非"\\d") - 编译高频使用的正则——
RegexOptions.Compiled或[GeneratedRegex]源生成器 - 避免灾难性回溯——不要嵌套量词,必要时设置超时
- 非捕获分组提升性能——不需要提取时使用
(?:...) - 正则不是万能的——解析 HTML/XML 应使用专用解析器
- 测试正则——使用在线工具(regex101.com)验证后再写入代码
- 注意中文匹配——使用
一-龥匹配中文字符 ^和$行为受 Multiline 影响——匹配整个字符串开头/结尾还是每行开头/结尾


