C#正则表达式全面详解:从基础到高级应用
正则表达式是处理文本模式匹配的强大工具,在数据验证、文本提取、格式转换等场景中发挥着重要作用。C# 通过System.Text.RegularExpressions
命名空间提供了完整的正则表达式支持,本文将全面介绍 C# 中正则表达式的使用方法,从基础语法到高级特性,帮助开发者掌握这一实用技术。
一、正则表达式基础
1. 什么是正则表达式
正则表达式(Regular Expression)是一种用于描述文本模式的字符串,它可以精确地定义需要匹配的文本格式。在 C# 中,正则表达式通常用于:
- 数据验证(如邮箱、手机号、身份证号)
- 文本搜索与提取(如从日志中提取特定信息)
- 文本替换与格式化(如批量修改文本格式)
- 复杂字符串拆分
2. C# 正则表达式核心类
C# 中处理正则表达式的核心类位于System.Text.RegularExpressions
命名空间:
- Regex:正则表达式的主要类,提供匹配、替换、拆分等操作
- Match:表示单个匹配结果
- MatchCollection:表示多个匹配结果的集合
- GroupCollection:表示匹配中的分组集合
- CaptureCollection:表示捕获的子字符串集合
使用前需引入命名空间:
using System.Text.RegularExpressions;
二、Regex 类的基本用法
1. 验证匹配(IsMatch 方法)
Regex.IsMatch
方法用于检查输入字符串是否与正则表达式匹配,返回布尔值:
// 基本用法
string pattern = @"^d{4}-d{2}-d{2}$"; // 日期格式:yyyy-MM-dd
string input1 = "2023-10-05";
string input2 = "2023/10/05";
bool isMatch1 = Regex.IsMatch(input1, pattern);
bool isMatch2 = Regex.IsMatch(input2, pattern);
Console.WriteLine($"input1匹配结果:{isMatch1}"); // True
Console.WriteLine($"input2匹配结果:{isMatch2}"); // False
// 带选项的匹配
// 忽略大小写
bool caseInsensitiveMatch = Regex.IsMatch("Hello", @"hello", RegexOptions.IgnoreCase);
Console.WriteLine($"忽略大小写匹配:{caseInsensitiveMatch}"); // True
2. 单次匹配(Match 方法)
Regex.Match
方法返回输入字符串中与正则表达式匹配的第一个结果(Match 对象):
string text = "邮箱:user@example.com,电话:13800138000";
string emailPattern = @"w+@w+.w+"; // 简单的邮箱正则
Match match = Regex.Match(text, emailPattern);
if (match.Success)
{
Console.WriteLine($"找到匹配:{match.Value}"); // 输出:user@example.com
Console.WriteLine($"匹配位置:{match.Index}"); // 输出:3(从0开始的索引)
Console.WriteLine($"匹配长度:{match.Length}"); // 输出:16
}
else
{
Console.WriteLine("未找到匹配");
}
3. 多次匹配(Matches 方法)
Regex.Matches
方法返回输入字符串中所有与正则表达式匹配的结果(MatchCollection 集合):
string text = "订单号:ORD-12345,订单号:ORD-67890,订单号:ORD-54321";
string orderPattern = @"ORD-d{5}"; // 匹配ORD-后跟5位数字
MatchCollection matches = Regex.Matches(text, orderPattern);
Console.WriteLine($"找到{matches.Count}个匹配:");
foreach (Match m in matches)
{
Console.WriteLine(m.Value);
}
// 输出:
// ORD-12345
// ORD-67890
// ORD-54321
三、正则表达式语法基础
C# 正则表达式语法基于标准正则表达式,并支持一些.NET 特定扩展。以下是常用语法元素:
1. 字符匹配
语法 | 说明 | 示例 | 匹配 |
---|---|---|---|
普通字符 | 匹配自身 | abc | “abc” |
. | 匹配任意单个字符(除换行符) | a.c | “abc”、“a1c” |
d | 匹配数字(0-9) | d{3} | “123”、“456” |
D | 匹配非数字 | D | “a”、“#”、" " |
w | 匹配字母、数字或下划线 | w+ | “name”、“user123” |
W | 匹配非单词字符 | W | “@”、“#”、" " |
s | 匹配空白字符(空格、制表符等) | asb | “a b”、“atb” |
S | 匹配非空白字符 | S+ | “text”、“123” |
2. 重复匹配
语法 | 说明 | 示例 | 匹配 |
---|---|---|---|
* | 匹配前面的元素 0 次或多次 | ab* | “a”、“ab”、“abb” |
+ | 匹配前面的元素 1 次或多次 | ab+ | “ab”、“abb” |
? | 匹配前面的元素 0 次或 1 次 | ab? | “a”、“ab” |
{n} | 匹配前面的元素恰好 n 次 | a{2} | “aa” |
{n,} | 匹配前面的元素至少 n 次 | a{2,} | “aa”、“aaa” |
{n,m} | 匹配前面的元素至少 n 次,最多 m 次 | a{2,3} | “aa”、“aaa” |
3. 定位符
语法 | 说明 | 示例 | 匹配 |
---|---|---|---|
^ | 匹配字符串的开始 | ^abc | “abc” 在字符串开头 |
$ | 匹配字符串的结束 | abc$ | “abc” 在字符串结尾 |
b | 匹配单词边界 | bwordb | “word” 作为独立单词 |
B | 匹配非单词边界 | BwordB | “word” 在单词中间 |
4. 字符类
语法 | 说明 | 示例 | 匹配 |
---|---|---|---|
[abc] | 匹配 a、b 或 c | [abc] | “a”、“b”、“c” |
[^abc] | 匹配除 a、b、c 外的任意字符 | [^abc] | “d”、“1”、“@” |
[a-z] | 匹配 a 到 z 的任意小写字母 | [a-z] | “a”、“m”、“z” |
[A-Z] | 匹配 A 到 Z 的任意大写字母 | [A-Z] | “A”、“M”、“Z” |
[0-9] | 匹配 0 到 9 的任意数字(等同于 d) | [0-9] | “0”、“5”、“9” |
[a-zA-Z0-9] | 匹配字母或数字(类似 w) | [a-zA-Z0-9] | “a”、“B”、“3” |
5. 逻辑运算符
语法 | 说明 | 示例 | 匹配 |
---|---|---|---|
` | ` | 逻辑或 | `ab |
() | 分组 | (ab)+ | “ab”、“abab” |
(?:) | 非捕获分组(仅用于分组,不捕获结果) | (?:ab)+ | 同上,但不捕获分组 |
四、分组与捕获
分组是正则表达式中非常强大的功能,允许我们从匹配的文本中提取特定部分。
1. 基本分组捕获
string text = "张三,25,男;李四,30,女";
// 匹配姓名、年龄、性别,使用逗号分隔
string pattern = @"(w+),(d+),(w+)";
MatchCollection matches = Regex.Matches(text, pattern);
foreach (Match m in matches)
{
Console.WriteLine($"完整匹配:{m.Value}");
// 遍历捕获的分组(Groups[0]是整个匹配,从1开始是实际分组)
for (int i = 1; i < m.Groups.Count; i++)
{
Console.WriteLine($"分组{i}:{m.Groups[i].Value}");
}
Console.WriteLine("---");
}
// 输出:
// 完整匹配:张三,25,男
// 分组1:张三
// 分组2:25
// 分组3:男
// ---
// 完整匹配:李四,30,女
// 分组1:李四
// 分组2:30
// 分组3:女
// ---
2. 命名分组
使用(?<name>...)
语法可以创建命名分组,提高代码可读性:
string text = "订单:ORD-12345,金额:99.99元,日期:2023-10-05";
// 命名分组:orderId、amount、date
string pattern = @"订单:(?<orderId>ORD-d+),金额:(?<amount>d+.d+)元,日期:(?<date>d{4}-d{2}-d{2})";
Match match = Regex.Match(text, pattern);
if (match.Success)
{
// 通过分组名访问
Console.WriteLine($"订单号:{match.Groups["orderId"].Value}");
Console.WriteLine($"金额:{match.Groups["amount"].Value}");
Console.WriteLine($"日期:{match.Groups["date"].Value}");
}
// 输出:
// 订单号:ORD-12345
// 金额:99.99
// 日期:2023-10-05
3. 非捕获分组
使用(?:...)
创建非捕获分组,仅用于分组逻辑,不保存捕获结果:
string text = "cat cats dog dogs";
// 非捕获分组(?:s)?表示可选的s
string pattern = @"(cat|dog)(?:s)?";
foreach (Match m in Regex.Matches(text, pattern))
{
Console.WriteLine($"匹配:{m.Value},分组1:{m.Groups[1].Value}");
}
// 输出:
// 匹配:cat,分组1:cat
// 匹配:cats,分组1:cat
// 匹配:dog,分组1:dog
// 匹配:dogs,分组1:dog
4. 捕获集合
当分组包含重复量词时,Captures
属性可以获取所有中间捕获结果:
string text = "aaaabbb";
// 匹配连续的a和连续的b
string pattern = @"(w+?)1+";
Match match = Regex.Match(text, pattern);
if (match.Success)
{
Console.WriteLine($"完整匹配:{match.Value}");
Console.WriteLine($"分组1的值:{match.Groups[1].Value}");
Console.WriteLine("所有捕获:");
foreach (Capture capture in match.Groups[1].Captures)
{
Console.WriteLine($" {capture.Value} at {capture.Index}");
}
}
// 输出:
// 完整匹配:aaaa
// 分组1的值:a
// 所有捕获:
// a at 0
// a at 1
// a at 2
// a at 3
五、替换操作
正则表达式的替换功能可以基于模式匹配来修改文本,非常适合批量处理文本。
1. 基本替换(Replace 方法)
string text = "手机号:13800138000,另一个手机号:13900139000";
// 匹配手机号,将中间4位替换为*
string pattern = @"(d{3})d{4}(d{4})";
string replacement = "$1****$2"; // $1表示第一个分组,$2表示第二个分组
string result = Regex.Replace(text, pattern, replacement);
Console.WriteLine(result);
// 输出:手机号:138****8000,另一个手机号:139****9000
2. 带回调的替换
Regex.Replace
方法可以接受一个MatchEvaluator
委托,实现复杂的替换逻辑:
string text = "价格:100,折扣后:80;价格:200,折扣后:150";
// 匹配价格数字
string pattern = @"d+";
// 使用回调函数将价格增加10%
string result = Regex.Replace(text, pattern, match =>
{
if (int.TryParse(match.Value, out int price))
{
return (price * 1.1).ToString("0.00");
}
return match.Value;
});
Console.WriteLine(result);
// 输出:价格:110.00,折扣后:88.00;价格:220.00,折扣后:165.00
3. 条件替换
结合分组可以实现条件替换逻辑:
string text = "张三:男,25;李四:女,30;王五:男,35";
// 匹配姓名:性别,年龄
string pattern = @"(?<name>w+):(?<gender>男|女),(?<age>d+)";
// 根据性别添加不同的前缀
string result = Regex.Replace(text, pattern, match =>
{
string gender = match.Groups["gender"].Value;
string prefix = gender == "男" ? "先生" : "女士";
return $"{match.Groups["name"].Value}{prefix}({match.Groups["age"].Value}岁)";
});
Console.WriteLine(result);
// 输出:张三先生(25岁);李四女士(30岁);王五先生(35岁)
六、拆分操作
Regex.Split
方法可以根据正则表达式匹配来拆分字符串,比string.Split
更灵活。
1. 基本拆分
string text = "苹果,香蕉;橙子|葡萄";
// 匹配逗号、分号或竖线作为分隔符
string pattern = @"[,;|]";
string[] fruits = Regex.Split(text, pattern);
Console.WriteLine("拆分结果:");
foreach (string fruit in fruits)
{
Console.WriteLine(fruit);
}
// 输出:
// 苹果
// 香蕉
// 橙子
// 葡萄
2. 保留分隔符
通过分组可以在拆分结果中保留分隔符:
string text = "第一章 介绍n1.1 背景n1.2 目的n第二章 方法n2.1 步骤";
// 匹配章节标题(数字+章)或小节标题(数字.数字)
string pattern = @"(d+章)|(d+.d+)";
string[] parts = Regex.Split(text, pattern);
Console.WriteLine("拆分结果:");
foreach (string part in parts)
{
if (!string.IsNullOrEmpty(part))
{
Console.WriteLine($"[{part}]");
}
}
// 输出:
// [第一章]
// 介绍
//
// [1.1]
// 背景
//
// [1.2]
// 目的
//
// [第二章]
// 方法
//
// [2.1]
// 步骤
七、正则表达式选项
RegexOptions
枚举提供了多种选项来控制正则表达式的行为:
// 常用的RegexOptions选项
RegexOptions options = RegexOptions.IgnoreCase | RegexOptions.Multiline;
// 1. IgnoreCase:忽略大小写
bool caseInsensitive = Regex.IsMatch("Hello", "hello", RegexOptions.IgnoreCase);
// 2. Multiline:多行模式,^匹配每行开头,$匹配每行结尾
string multiLineText = "line1nLine2nLINE3";
MatchCollection multiLineMatches = Regex.Matches(multiLineText, @"^lined", RegexOptions.IgnoreCase | RegexOptions.Multiline);
// 3. Singleline:单行模式,.匹配包括换行符在内的所有字符
string singleLineText = "first linensecond line";
bool singleLineMatch = Regex.IsMatch(singleLineText, @"first.*second", RegexOptions.Singleline);
// 4. Compiled:编译正则表达式为IL代码,提高多次使用的性能
Regex compiledRegex = new Regex(@"d+", RegexOptions.Compiled);
// 5. IgnorePatternWhitespace:忽略模式中的空白,允许添加注释
string patternWithComments = @"
d{3} # 区号
-? # 可选的连字符
d{8} # 8位号码
";
bool phoneMatch = Regex.IsMatch("010-12345678", patternWithComments, RegexOptions.IgnorePatternWhitespace);
各选项的主要作用:
选项 | 作用 |
---|---|
IgnoreCase | 忽略大小写 |
Multiline | 多行模式,^ 和$ 匹配每行的开头和结尾 |
Singleline | 单行模式,. 匹配所有字符(包括换行符) |
Compiled | 编译正则表达式,提高重复使用的性能 |
IgnorePatternWhitespace | 忽略模式中的空白,允许注释(# 后的内容) |
RightToLeft | 从右向左搜索 |
ExplicitCapture | 只捕获显式命名的分组 |
八、性能优化与最佳实践
1. 编译正则表达式
对于频繁使用的正则表达式,使用RegexOptions.Compiled
可以显著提高性能:
// 不编译的正则表达式(解释执行)
var regex1 = new Regex(@"d{3}-d{8}");
// 编译的正则表达式(转换为IL代码)
var regex2 = new Regex(@"d{3}-d{8}", RegexOptions.Compiled);
// 性能测试
string testText = "电话:010-12345678,手机:13800138000";
Stopwatch stopwatch = new Stopwatch();
// 测试非编译版本
stopwatch.Start();
for (int i = 0; i < 100000; i++)
{
regex1.IsMatch(testText);
}
stopwatch.Stop();
Console.WriteLine($"非编译版本:{stopwatch.ElapsedMilliseconds}ms");
// 测试编译版本
stopwatch.Restart();
for (int i = 0; i < 100000; i++)
{
regex2.IsMatch(testText);
}
stopwatch.Stop();
Console.WriteLine($"编译版本:{stopwatch.ElapsedMilliseconds}ms");
注意:编译正则表达式会增加初始化时间和内存占用,适合重复使用的场景
2. 缓存正则表达式
对于重复使用的正则表达式,应避免重复创建Regex
对象:
// 不好的做法:每次调用都创建新的Regex对象
bool IsValidEmailBad(string email)
{
return Regex.IsMatch(email, @"^[w-]+(.[w-]+)*@([w-]+.)+[a-zA-Z]{2,7}$");
}
// 好的做法:缓存Regex对象
private static readonly Regex _emailRegex = new Regex(
@"^[w-]+(.[w-]+)*@([w-]+.)+[a-zA-Z]{2,7}$",
RegexOptions.Compiled);
bool IsValidEmailGood(string email)
{
return _emailRegex.IsMatch(email);
}
3. 设置超时时间
防止恶意或复杂的正则表达式导致性能问题:
try
{
// 设置超时时间为1秒
var regex = new Regex(@"^(d+)+$", RegexOptions.None, TimeSpan.FromSeconds(1));
// 这种模式对"12345678901234567890a"等输入可能导致长时间匹配
bool isMatch = regex.IsMatch("12345678901234567890a");
}
catch (RegexMatchTimeoutException)
{
Console.WriteLine("匹配超时");
}
4. 避免过度复杂的正则表达式
复杂的正则表达式难以维护且性能可能不佳,可考虑:
- 将复杂模式拆分为多个简单正则表达式
- 结合代码逻辑处理,而非完全依赖正则表达式
- 使用命名分组提高可读性
九、常用正则表达式示例
1. 数据验证
// 1. 邮箱验证
string emailPattern = @"^[w-]+(.[w-]+)*@([w-]+.)+[a-zA-Z]{2,7}$";
bool IsValidEmail(string email) => Regex.IsMatch(email, emailPattern);
// 2. 手机号验证(中国大陆)
string phonePattern = @"^1[3-9]d{9}$";
bool IsValidPhone(string phone) => Regex.IsMatch(phone, phonePattern);
// 3. 身份证号验证(18位)
string idCardPattern = @"^d{6}(18|19|20)d{2}(0[1-9]|1[0-2])(0[1-9]|[12]d|3[01])d{3}[dXx]$";
bool IsValidIdCard(string id) => Regex.IsMatch(id, idCardPattern);
// 4. URL验证
string urlPattern = @"^(https?://)?([da-z.-]+).([a-z.]{2,6})([/w .-]*)*/?$";
bool IsValidUrl(string url) => Regex.IsMatch(url, urlPattern);
// 5. 密码强度验证(至少8位,包含大小写字母、数字和特殊字符)
string passwordPattern = @"^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[^da-zA-Z]).{8,}$";
bool IsStrongPassword(string password) => Regex.IsMatch(password, passwordPattern);
2. 文本提取
// 1. 从HTML中提取所有链接
string html = @"<a href='https://example.com'>示例</a><a href='https://test.com'>测试</a>";
string linkPattern = @"href=['""]([^'""]+)['""]";
MatchCollection linkMatches = Regex.Matches(html, linkPattern);
foreach (Match m in linkMatches)
{
Console.WriteLine("链接:" + m.Groups[1].Value);
}
// 2. 从日志中提取错误信息
string logText = @"2023-10-05 10:00: INFO 启动成功
2023-10-05 10:01: ERROR 连接失败:无法连接到数据库
2023-10-05 10:02: INFO 重试连接
2023-10-05 10:03: ERROR 连接失败:超时";
string errorPattern = @"(d{4}-d{2}-d{2} d{2}:d{2}): ERROR (.*)";
foreach (Match m in Regex.Matches(logText, errorPattern))
{
Console.WriteLine($"[{m.Groups[1].Value}] 错误:{m.Groups[2].Value}");
}
// 3. 提取字符串中的所有数字
string textWithNumbers = "订单1:金额100元,订单2:金额200元";
MatchCollection numberMatches = Regex.Matches(textWithNumbers, @"d+");
foreach (Match m in numberMatches)
{
Console.WriteLine("数字:" + m.Value);
}
3. 文本格式化
// 1. 格式化电话号码
string phone = "01012345678";
string formattedPhone = Regex.Replace(phone, @"^(d{3})(d{8})$", "$1-$2");
// 结果:010-12345678
// 2. 格式化日期(将20231005转换为2023-10-05)
string date = "20231005";
string formattedDate = Regex.Replace(date, @"^(d{4})(d{2})(d{2})$", "$1-$2-$3");
// 结果:2023-10-05
// 3. 驼峰命名转下划线命名
string camelCase = "userNameAgeEmail";
string snakeCase = Regex.Replace(camelCase, @"([A-Z])", "_$1").ToLower();
// 结果:user_name_age_email
十、高级特性
1. 平衡组(匹配嵌套结构)
C# 正则表达式支持平衡组,可用于匹配嵌套结构(如括号、HTML 标签等):
// 匹配嵌套的括号
string nestedText = "a(b(c)d)e(f)g";
string balancedPattern = @"
( # 匹配左括号
( # 开始捕获内容
(?> # 非回溯组
[^()]+ # 匹配非括号字符
| # 或者
(?<Open> ( ) # 遇到左括号,Open计数器+1
| # 或者
(?<-Open> ) ) # 遇到右括号,Open计数器-1
)* # 重复
(?(Open) # 如果Open计数器>0(有未闭合的括号)
(?!) # 不匹配
| # 否则
) # 匹配右括号
)
";
Match match = Regex.Match(nestedText, balancedPattern, RegexOptions.IgnorePatternWhitespace);
if (match.Success)
{
Console.WriteLine("匹配的嵌套结构:" + match.Value); // 输出:(b(c)d)
}
2. 条件匹配
基于前面的分组是否匹配来决定后续匹配:
// 条件匹配:如果有区号则必须有连字符
string pattern = @"(?:(d{3})-)?d{8}|(?(1)d{8})";
// 解释:
// (?:(d{3})-)? 可选的3位区号+连字符
// d{8} 8位号码
// | 或者
// (?(1)d{8}) 如果前面捕获了区号,则必须匹配8位号码
bool match1 = Regex.IsMatch("12345678", pattern); // True(无区号)
bool match2 = Regex.IsMatch("010-12345678", pattern); // True(有区号和连字符)
bool match3 = Regex.IsMatch("01012345678", pattern); // False(有区号但无连字符)
3. 正则表达式的惰性匹配与贪婪匹配
string text = "<tag>内容1</tag><tag>内容2</tag>";
// 贪婪匹配(默认):尽可能匹配最长的字符串
string greedyPattern = @"<tag>.*</tag>";
Match greedyMatch = Regex.Match(text, greedyPattern);
Console.WriteLine("贪婪匹配:" + greedyMatch.Value); // 输出:<tag>内容1</tag><tag>内容2</tag>
// 惰性匹配:尽可能匹配最短的字符串
string lazyPattern = @"<tag>.*?</tag>";
Match lazyMatch = Regex.Match(text, lazyPattern);
Console.WriteLine("惰性匹配:" + lazyMatch.Value); // 输出:<tag>内容1</tag>
十一、总结
正则表达式是 C# 中处理文本的强大工具,通过System.Text.RegularExpressions
命名空间提供的类,可以实现复杂的文本模式匹配、提取、替换和拆分操作。本文介绍了:
- 正则表达式的基础语法和核心类
- 匹配、替换、拆分等基本操作
- 分组捕获和命名分组的使用
- 正则表达式选项和性能优化
- 常见应用场景和示例
使用正则表达式时,应注意:
- 避免过度复杂的模式,保持可读性
- 对于频繁使用的正则表达式,使用
RegexOptions.Compiled
提高性能 - 适当使用超时设置,防止恶意输入导致的性能问题
- 复杂验证逻辑可结合代码和正则表达式实现
掌握正则表达式将极大提高文本处理效率,无论是数据验证、日志分析还是文本转换,都能发挥重要作用。在实际开发中,建议结合具体需求选择合适的正则表达式,并充分测试各种边界情况。