C# 正则表达式入门:解锁强大的文本处理能力
在软件开发中,处理文本是一项极其常见的任务。无论是验证用户输入(如邮箱、电话号码)、从日志文件中提取特定信息、解析配置文件,还是对字符串进行复杂的查找、替换和分割,我们都需要高效且灵活的工具。而正则表达式(Regular Expression,简称 Regex)正是这样一种强大到令人惊叹的文本处理利器。
对于 C# 开发者而言,.NET Framework 或 .NET Core/.NET 5+ 提供了完善的 System.Text.RegularExpressions
命名空间,使得在 C# 中使用正则表达式变得既简单又功能强大。本文将带你从零开始,系统地学习 C# 中的正则表达式,包括其基本语法、常用的模式构建、以及在 C# 代码中的实际应用。
文章会力求详细,包含大量解释和 C# 代码示例,帮助你彻底掌握这一技能。
什么是正则表达式?为什么学习它?
简单来说,正则表达式是一种用来描述或匹配一系列符合某个句法规则的字符串的单个字符串。它是一种用于文本模式匹配的强大“微型语言”。你可以把它想象成一种高级的通配符系统,但比传统的 *
和 ?
通配符要强大得多,能够处理远比简单模式复杂的匹配需求。
为什么学习正则表达式?
- 强大而灵活: 能够表达非常复杂的文本模式,例如“一个有效的电子邮件地址”、“一个包含特定标签的 HTML 字符串”、“所有双引号内的文本”等等。
- 高效: 许多正则表达式引擎(包括 .NET 的)都经过高度优化,能够快速地在大量文本中查找匹配项。
- 跨语言通用: 正则表达式的概念和大部分基本语法是通用的,一旦掌握,几乎可以在所有现代编程语言(Java, Python, JavaScript, PHP, C#, Ruby 等)中使用。
- 简化代码: 很多需要大量
if/else
判断或复杂循环才能完成的字符串处理任务,使用正则表达式可能只需要一行代码就能优雅地解决。
虽然正则表达式的语法初看起来有些晦涩,充满了各种特殊符号,但一旦理解了这些符号的含义和组合方式,你就会发现它带来的巨大便利。
正则表达式的基础概念与语法
学习正则表达式,首先要理解构成模式的各种基本元素。我们将从最基本的开始。
1. 字面字符(Literals)
大多数字符都匹配它们自身。例如,正则表达式 hello
会精确地匹配字符串中的 “hello”。
csharp
string pattern = "hello";
string input = "hello world";
// 这个模式将匹配 input 中的 "hello"
2. 特殊字符(Metacharacters)与转义
有些字符在正则表达式中有特殊的含义,它们不匹配自身,而是代表某种模式规则。这些特殊字符包括:.
, *
, +
, ?
, ^
, $
, |
, (
, )
, [
, {
, \
.
如果你想匹配这些特殊字符本身,你需要使用反斜杠 \
来进行转义。例如,要匹配一个字面意义上的点号 .
,你需要使用 \.
。要匹配一个反斜杠 \
本身,你需要使用 \\
。
“`csharp
// 匹配 “C:\Users\Public”
string pattern = @”C:\Users\Public”; // 在 C# 中使用逐字字符串 @”” 可以避免双写反斜杠
// 匹配 “价格是 $10.00″
string pattern2 = @”价格是 \$10.00”;
“`
在 C# 中,强烈建议使用 逐字字符串字面量(Verbatim String Literals),即在字符串前加上 @
符号(如 @"\d+"
),这样字符串内的反斜杠就不需要额外转义了。这使得正则表达式模式更易读,因为它与大多数在线正则测试工具中使用的格式一致。
csharp
// 匹配一个点号 '.'
string pattern = @"\."; // 只需要一个反斜杠
3. 任意字符(The Dot .
)
点号 .
是一个非常常用的特殊字符,它匹配除换行符 \n
外的 任意单个字符。
csharp
// 匹配 "cat", "cot", "cut" 等三个字母,中间是任意字符的词
string pattern = @"c.t";
4. 字符类(Character Classes []
)
字符类允许你匹配 一个 字符,该字符是方括号 []
内定义的集合中的任意一个。
[abc]
:匹配 ‘a’、’b’ 或 ‘c’ 中的任意一个字符。[0-9]
:匹配任意一个数字(0到9)。这是范围表示法。[a-z]
:匹配任意一个小写字母。[A-Z]
:匹配任意一个大写字母。[a-zA-Z]
:匹配任意一个字母(大写或小写)。[a-zA-Z0-9]
:匹配任意一个字母或数字。
你可以在字符类中使用连字符 -
表示范围,只要它不是第一个或最后一个字符。
否定字符类 ([^...]
)
如果在方括号内的第一个字符是脱字符 ^
,那么这个字符类将匹配 不在 集合中的任意单个字符。
[^0-9]
:匹配任意一个非数字字符。[^aeiou]
:匹配任意一个非元音字母的字符。
“`csharp
// 匹配一个数字
string pattern = @”[0-9]”;
// 匹配一个非数字
string pattern2 = @”[^0-9]”;
// 匹配一个字母
string pattern3 = @”[a-zA-Z]”;
“`
5. 预定义字符类(Shorthand Character Classes)
为了方便,正则表达式提供了一些预定义的字符类简写:
\d
: 匹配任意一个数字,等同于[0-9]
。\D
: 匹配任意一个非数字字符,等同于[^0-9]
。\w
: 匹配任意一个“字词字符”(word character),包括字母、数字和下划线_
,等同于[a-zA-Z0-9_]
。\W
: 匹配任意一个非字词字符,等同于[^a-zA-Z0-9_]
。\s
: 匹配任意一个空白字符(whitespace character),包括空格、制表符\t
、换行符\n
、回车符\r
等。\S
: 匹配任意一个非空白字符。
“`csharp
// 匹配一个或多个数字
string pattern = @”\d+”; // + 是量词,后面会讲到
// 匹配任意字词字符
string pattern2 = @”\w”;
// 匹配空白字符
string pattern3 = @”\s”;
“`
量词(Quantifiers)
量词用于指定一个模式元素(可以是单个字符、字符类或分组)应该重复出现的次数。
?
: 匹配前一个元素 零次或一次。例如colou?r
匹配 “color” 和 “colour”。*
: 匹配前一个元素 零次或多次。例如a*
匹配 “”, “a”, “aa”, “aaa”, …+
: 匹配前一个元素 一次或多次。例如a+
匹配 “a”, “aa”, “aaa”, … (但不匹配空字符串){n}
: 匹配前一个元素 恰好 n 次。例如\d{3}
匹配恰好三个数字。{n,}
: 匹配前一个元素 至少 n 次。例如\d{3,}
匹配三个或更多数字。{n,m}
: 匹配前一个元素 至少 n 次,至多 m 次。例如\d{3,5}
匹配三到五个数字。
csharp
// 匹配一个有效的(可能是可选的)区号 (xxx) 或 xxx-
// (?:...) 是非捕获分组,后面会讲到
string pattern = @"(?:\(\d{3}\)\s*|\d{3}-)?\d{3}-\d{4}";
// 这个模式尝试匹配常见的电话号码格式:
// (###) ###-#### 或 ###-###-#### 或 ###-####
// \s* 匹配零个或多个空白字符
// ? 使得 (?:...) 部分成为可选
贪婪与惰性量词(Greedy vs. Lazy Quantifiers)
默认情况下,量词是“贪婪的”(Greedy),它们会尽可能多地匹配字符。例如,模式 <.*>
在字符串 <b>bold</b><i>italic</i>
中会匹配整个 <b>bold</b><i>italic</i>
,而不是 <b>bold</b>
或 <i>italic</i>
。
如果你希望量词匹配 尽可能少 的字符(直到满足整个模式为止),可以在量词后面加上一个问号 ?
,使其变为“惰性的”(Lazy)或“非贪婪的”。
??
: 零次或一次,惰性*?
: 零次或多次,惰性+?
: 一次或多次,惰性{n,}?
: 至少 n 次,惰性{n,m}?
: n 到 m 次,惰性
使用惰性量词,模式 <.*?>
在字符串 <b>bold</b><i>italic</i>
中会分别匹配 <b>bold</b>
和 <i>italic</i>
。
“`csharp
string input = “bolditalic“;
string greedyPattern = @”<.>”;
string lazyPattern = @”<.?>”;
// Greedy match: bolditalic
// Lazy matches: bold, italic
“`
锚点(Anchors)
锚点不匹配具体的字符,而是匹配文本中的 位置。
^
: 匹配字符串的 开头。如果在多行模式下,它也匹配每一行的开头。$
: 匹配字符串的 结尾。如果在多行模式下,它也匹配每一行的结尾。\b
: 匹配一个 字词边界(Word Boundary)。字词边界是\w
(字词字符) 和\W
(非字词字符) 之间的位置,或者\w
字符与字符串开头/结尾之间的位置。例如,\bcat\b
只会匹配独立的单词 “cat”,而不会匹配 “catalog” 或 “concatenate” 中的 “cat”。\B
: 匹配一个 非字词边界。与\b
相反。例如,\Bcat\B
可以匹配 “concatenate” 中的 “cat”。
“`csharp
// 匹配以 “abc” 开头的字符串
string pattern = @”^abc”;
// 匹配以 “xyz” 结尾的字符串
string pattern2 = @”xyz$”;
// 匹配独立的单词 “the”
string pattern3 = @”\bthe\b”;
“`
分组与捕获(Grouping and Capturing ()
)
圆括号 ()
有两个主要用途:
- 分组: 将多个模式元素组合成一个单元,以便对其应用量词或进行其他操作。例如,
(abc)+
匹配一个或多个连续的 “abc” 序列(如 “abc”, “abcabc”)。 - 捕获: 默认情况下,括号会“捕获”它匹配到的文本,作为匹配结果的一部分,可以通过索引或名称访问。
csharp
// 匹配日期格式 DD/MM/YYYY,并捕获日、月、年
string pattern = @"(\d{2})/(\d{2})/(\d{4})";
在这个例子中:
* (\d{2})
是第一个捕获组,捕获两位数字(日)。
* (\d{2})
是第二个捕获组,捕获两位数字(月)。
* (\d{4})
是第三个捕获组,捕获四位数字(年)。
非捕获分组 ((?:...)
)
如果你只需要使用括号进行分组,而不需要捕获匹配的文本,可以使用非捕获分组 (?:...)
。这有助于提高性能(因为它不需要存储捕获的数据)并避免在结果中创建不必要的组。
csharp
// 匹配 "cat" 或 "dog",作为一个整体进行操作
string pattern = @"(?:cat|dog)s"; // 匹配 "cats" 或 "dogs"
命名捕获组 ((?<name>...)
)
在 C# 中,你可以为捕获组指定一个名称,这使得通过名称访问捕获结果更加直观。语法是 (?<GroupName>...)
或 (?'GroupName'...)
。
csharp
// 匹配日期格式 DD/MM/YYYY,并命名捕获日、月、年
string pattern = @"(?<day>\d{2})/(?<month>\d{2})/(?<year>\d{4})";
反向引用(Backreferences \n
)
在正则表达式模式本身中,你可以使用 \n
(其中 n 是捕获组的索引) 或 \k<GroupName>
来引用先前匹配的捕获组的内容。这允许你匹配重复的文本。
csharp
// 匹配重复的单词,例如 "hello hello" 或 "world world"
string pattern = @"\b(\w+)\s+\1\b";
// (\w+) 捕获一个或多个字词字符
// \s+ 匹配一个或多个空白字符
// \1 引用第一个捕获组 (\w+) 匹配的内容
选择(Alternation |
)
竖线 |
符号用作“或”运算符,允许你匹配多个可能的模式之一。
“`csharp
// 匹配 “cat” 或 “dog”
string pattern = @”cat|dog”;
// 匹配 “apple pie” 或 “banana pie”
string pattern2 = @”(apple|banana) pie”; // 使用分组来限定 | 的作用范围
“`
零宽度断言(Lookaround Assertions)
零宽度断言匹配一个位置,就像锚点一样,但它们会根据紧接在该位置之前或之后(而不是包含在匹配结果中)的文本来决定是否匹配。
- 肯定向前查找(Positive Lookahead
(?=...)
): 匹配一个位置,该位置后面紧跟着括号内的模式。 - 否定向前查找(Negative Lookahead
(?!...)
): 匹配一个位置,该位置后面 不 紧跟着括号内的模式。 - 肯定向后查找(Positive Lookbehind
(?<=...)
): 匹配一个位置,该位置前面紧跟着括号内的模式。 - 否定向后查找(Negative Lookbehind
(?<!...)
): 匹配一个位置,该位置前面 不 紧跟着括号内的模式。
lookbehind 的模式在某些正则表达式引擎中必须是固定长度的,但在 .NET 中支持可变长度 lookbehind。
“`csharp
// 匹配只有在后面跟着 “$” 符号的数字(但不包括 $ 符号)
string pattern = @”\d+(?= \$)”; // 注意空格需要转义或在字符类中 [\$]
// 匹配不在括号内的文本
string pattern2 = @”\b\w+\b(?!\s*))”; // 匹配单词,但如果后面跟着 0个或多个空白字符和一个右括号,则不匹配该单词
“`
正则表达式选项(Regex Options)
你可以通过设置选项来改变正则表达式的匹配行为。在 C# 中,这些选项由 RegexOptions
枚举提供。常用的选项包括:
RegexOptions.IgnoreCase
: 执行不区分大小写的匹配。RegexOptions.Multiline
: 使^
和$
匹配输入字符串中每一行的开头和结尾(而不仅仅是整个字符串的开头和结尾)。RegexOptions.Singleline
: 使点号.
匹配包括换行符\n
在内的所有字符(默认情况下点号不匹配换行符)。RegexOptions.IgnorePatternWhitespace
: 忽略模式中的非转义空白字符和#
后面的注释,这使得你可以格式化和注释复杂的正则表达式以提高可读性。RegexOptions.Compiled
: 将正则表达式编译到程序集中,这通常能提高重复使用同一正则表达式时的性能,但会增加首次使用时的启动开销。
csharp
// 不区分大小写匹配 "hello"
string pattern = @"hello";
Regex regex = new Regex(pattern, RegexOptions.IgnoreCase);
在 C# 中使用正则表达式
.NET 中使用正则表达式的核心是 System.Text.RegularExpressions
命名空间,特别是 Regex
类。Regex
类提供了静态方法(用于一次性操作)和实例方法(用于重复使用同一模式)。
引入命名空间
首先,确保在你的 C# 文件顶部引入命名空间:
csharp
using System.Text.RegularExpressions;
静态方法 vs. 实例方法
- 静态方法 (e.g.,
Regex.IsMatch(...)
): 适用于只使用一次或几次的模式。每次调用静态方法时,.NET 可能会编译或查找缓存的正则表达式,这会带来一些开销。 - 实例方法 (e.g.,
new Regex(...).IsMatch(...)
): 适用于需要频繁使用同一模式的情况。通过创建Regex
类的实例,并将RegexOptions.Compiled
选项传递给构造函数,可以将正则表达式编译到原生代码中,从而显著提高后续匹配操作的性能。
对于初学者和大多数常见场景,静态方法通常足够方便。当你处理大量文本或在性能关键的代码路径中重复使用同一模式时,考虑使用 Regex
实例并指定 RegexOptions.Compiled
。
常用 C# Regex 方法
Regex
类提供了许多有用的方法来执行不同的文本处理任务。
-
Regex.IsMatch(string input, string pattern)
/regexInstance.IsMatch(string input)
- 检查输入字符串是否包含与模式匹配的任何子字符串。
- 返回
bool
类型:如果找到匹配项则为true
,否则为false
。 - 适用于验证字符串格式。
“`csharp
string email = “[email protected]”;
// 非常简化的邮箱格式校验,仅作示例
string emailPattern = @”^\w+@\w+.\w+$”;bool isValidEmail = Regex.IsMatch(email, emailPattern);
Console.WriteLine($”‘{email}’ 是有效邮箱格式吗? {isValidEmail}”); // 输出: Truestring phoneNumber = “123-456-7890″;
string phonePattern = @”^\d{3}-\d{3}-\d{4}$”;
bool isValidPhone = Regex.IsMatch(phoneNumber, phonePattern);
Console.WriteLine($”‘{phoneNumber}’ 是有效电话号码格式吗? {isValidPhone}”); // 输出: Truestring invalidPhone = “1234567890”;
bool isValidInvalidPhone = Regex.IsMatch(invalidPhone, phonePattern);
Console.WriteLine($”‘{invalidPhone}’ 是有效电话号码格式吗? {isValidInvalidPhone}”); // 输出: False
“` -
Regex.Match(string input, string pattern)
/regexInstance.Match(string input)
- 在输入字符串中查找 第一个 与模式匹配的子字符串。
- 返回一个
Match
对象。如果找到匹配项,Match.Success
为true
,Match.Value
包含匹配的文本。如果没有找到,Match.Success
为false
,Match.Value
为空字符串。 Match
对象还提供了访问捕获组(Groups)和捕获(Captures)的属性。
“`csharp
string text = “日期是 01/15/2023 和 03/10/2024。”;
string datePattern = @”(?\d{2})/(? \d{2})/(? \d{4})”; Match firstMatch = Regex.Match(text, datePattern);
if (firstMatch.Success)
{
Console.WriteLine(“找到第一个日期:”);
Console.WriteLine($” 完整匹配: {firstMatch.Value}”);
Console.WriteLine($” 索引: {firstMatch.Index}”);
Console.WriteLine($” 长度: {firstMatch.Length}”);
Console.WriteLine($” 月份: {firstMatch.Groups[“month”].Value}”); // 使用命名捕获组
Console.WriteLine($” 日期: {firstMatch.Groups[“day”].Value}”);
Console.WriteLine($” 年份: {firstMatch.Groups[“year”].Value}”);// 也可以使用索引访问捕获组 (0 是整个匹配, 1 是第一个捕获组, ...) Console.WriteLine($" 月份 (索引): {firstMatch.Groups[1].Value}");
}
else
{
Console.WriteLine(“没有找到日期。”);
}
“` -
Regex.Matches(string input, string pattern)
/regexInstance.Matches(string input)
- 在输入字符串中查找 所有 与模式匹配的子字符串。
- 返回一个
MatchCollection
对象,这是一个Match
对象的集合。 - 适用于从文本中提取所有符合模式的数据。
“`csharp
string text = “所有日期: 01/15/2023, 03/10/2024, 12/01/2025.”;
string datePattern = @”(?\d{2})/(? \d{2})/(? \d{4})”; MatchCollection allMatches = Regex.Matches(text, datePattern);
Console.WriteLine($”找到 {allMatches.Count} 个日期:”);
foreach (Match match in allMatches)
{
Console.WriteLine($” 完整匹配: {match.Value}”);
Console.WriteLine($” 月份: {match.Groups[“month”].Value}”);
Console.WriteLine($” 日期: {match.Groups[“day”].Value}”);
Console.WriteLine($” 年份: {match.Groups[“year”].Value}”);
Console.WriteLine(” —“);
}
“` -
Regex.Replace(string input, string pattern, string replacement)
/regexInstance.Replace(string input, string replacement)
- 用指定的替换字符串替换输入字符串中所有与模式匹配的子字符串。
- 返回替换后的新字符串。
- 替换字符串可以使用
$n
或$<GroupName>
来引用捕获组的内容。$&
或$0
引用整个匹配项。$`` (backtick) 引用匹配项之前的文本。
$’(single quote) 引用匹配项之后的文本。
$+引用最后一个捕获组。
$_` 引用整个输入字符串。
“`csharp
string text = “我的电话是 123-456-7890 和 987-654-3210。”;
string phonePattern = @”(\d{3})-(\d{3})-(\d{4})”;
string replacement = “($1) $2-$3”; // 使用捕获组重组格式string formattedText = Regex.Replace(text, phonePattern, replacement);
Console.WriteLine($”原文本: {text}”);
Console.WriteLine($”替换后: {formattedText}”);
// 输出: 替换后: 我的电话是 (123) 456-7890 和 (987) 654-3210。// 另一个替换示例:将所有 HTML 标签内容替换为 “…”
string html = “Hello
World“;
string tagContentPattern = @”<.?>.?<\/.*?>”; // 匹配 HTML 标签及其内容 (使用惰性匹配)
string replacedHtml = Regex.Replace(html, tagContentPattern, “…”);
Console.WriteLine($”原 HTML: {html}”);
Console.WriteLine($”替换后: {replacedHtml}”);
// 输出: 替换后: … …
“` -
Regex.Split(string input, string pattern)
/regexInstance.Split(string input)
- 使用与模式匹配的子字符串作为分隔符,将输入字符串分割成一个字符串数组。
- 返回一个
string[]
数组。 - 功能类似于
string.Split()
,但可以使用正则表达式作为更灵活的分隔符。
“`csharp
string data = “apple, orange; banana|grape”;
// 使用逗号、分号或竖线作为分隔符,同时忽略可能存在的空白字符
string delimiterPattern = @”[,\s;|\s]+”; // 匹配逗号或分号或竖线,前后可能有空白字符,一次或多次string[] fruits = Regex.Split(data, delimiterPattern);
Console.WriteLine(“分割结果:”);
foreach (string fruit in fruits)
{
Console.WriteLine($”- {fruit}”);
}
// 输出:
// – apple
// – orange
// – banana
// – grape// 另一个示例:分割并保留分隔符 (通过捕获组)
string textWithDelimiters = “word1, word2; word3″;
string splitAndKeepPattern = @”([,;])\s*”; // 捕获逗号或分号,并匹配后面的0个或多个空白字符
string[] parts = Regex.Split(textWithDelimiters, splitAndKeepPattern);Console.WriteLine(“\n分割并保留分隔符结果:”);
foreach (string part in parts)
{
Console.WriteLine($”- ‘{part}'”);
}
// 输出:
// – ‘word1’
// – ‘,’
// – ‘ word2’
// – ‘;’
// – ‘ word3’
// 注意:Regex.Split 如果模式中有捕获组,会将捕获到的分隔符也包含在结果数组中。
“`
结合 C# 和正则表达式的实际例子
让我们通过一些更完整的例子来演示如何在 C# 中结合正则表达式进行实际的文本处理。
例子 1:验证用户名
要求:用户名必须包含字母、数字或下划线,长度在 3 到 15 个字符之间。
“`csharp
using System;
using System.Text.RegularExpressions;
public class ValidationExample
{
public static void Main(string[] args)
{
string username = “user_123”;
string invalidUsername = “us”; // Too short
string anotherInvalidUsername = “user with spaces”; // Contains space
// ^ 匹配字符串开头
// [\w_]+ 匹配一个或多个字词字符 (\w) 或下划线 (_)
// {3,15} 量词,匹配前面的 [\w_]+ 整体重复 3 到 15 次
// $ 匹配字符串结尾
string pattern = @"^[\w_]{3,15}$";
bool isValid1 = Regex.IsMatch(username, pattern);
Console.WriteLine($"'{username}' 有效? {isValid1}"); // True
bool isValid2 = Regex.IsMatch(invalidUsername, pattern);
Console.WriteLine($"'{invalidUsername}' 有效? {isValid2}"); // False
bool isValid3 = Regex.IsMatch(anotherInvalidUsername, pattern);
Console.WriteLine($"'{anotherInvalidUsername}' 有效? {isValid3}"); // False
}
}
“`
例子 2:从日志文件中提取错误信息
假设你有如下日志文本,想提取所有包含 “ERROR:” 的行。
“`csharp
using System;
using System.Text.RegularExpressions;
public class LogParsingExample
{
public static void Main(string[] args)
{
string logContent = @”
INFO: Application started successfully.
WARN: Configuration file not found. Using default settings.
ERROR: Database connection failed.
INFO: Processing user request.
ERROR: Unable to process payment for user ID 12345.
DEBUG: User authenticated.
“;
// ^ 匹配行开头 (需要 RegexOptions.Multiline)
// .* 匹配任意数量的任意字符 (除换行符)
// ERROR: 匹配字面字符串 "ERROR:"
// .* 匹配 ERROR: 之后到行尾的任意字符
// $ 匹配行结尾 (需要 RegexOptions.Multiline)
string pattern = @"^.*ERROR:.*$";
// 注意使用 RegexOptions.Multiline 选项
MatchCollection errorLines = Regex.Matches(logContent, pattern, RegexOptions.Multiline);
Console.WriteLine("提取的错误信息:");
foreach (Match match in errorLines)
{
Console.WriteLine(match.Value);
}
}
}
“`
例子 3:替换字符串中的敏感信息
假设你需要将文本中的所有 11 位手机号码替换为 [电话号码]
。
“`csharp
using System;
using System.Text.RegularExpressions;
public class ReplacementExample
{
public static void Main(string[] args)
{
string text = “请联系我,我的电话是 13812345678,或者我的座机 010-12345678,还有 13987654321。”;
// 匹配 11 位数字的模式
string phonePattern = @"\d{11}";
string replacement = "[电话号码]";
string processedText = Regex.Replace(text, phonePattern, replacement);
Console.WriteLine($"原文本: {text}");
Console.WriteLine($"替换后: {processedText}");
// 输出: 替换后: 请联系我,我的电话是 [电话号码],或者我的座机 010-12345678,还有 [电话号码]。
}
}
“`
例子 4:解析带键值对的字符串
假设你有格式为 key=value; key=value;...
的字符串,并想提取所有键值对。
“`csharp
using System;
using System.Text.RegularExpressions;
using System.Collections.Generic;
public class ParsingExample
{
public static void Main(string[] args)
{
string configString = “server=192.168.1.1; port=8080; timeout=3000;”;
// (?<key>\w+) 命名捕获组 key,匹配一个或多个字词字符
// = 匹配字面等号
// (?<value>[^;]+) 命名捕获组 value,匹配一个或多个非分号字符
// ; 匹配字面分号
// \s* 匹配 0 个或多个空白字符 (可选,为了健壮性)
string pattern = @"(?<key>\w+)=(?<value>[^;]+);\s*";
MatchCollection matches = Regex.Matches(configString, pattern);
Dictionary<string, string> config = new Dictionary<string, string>();
Console.WriteLine("解析的配置项:");
foreach (Match match in matches)
{
string key = match.Groups["key"].Value;
string value = match.Groups["value"].Value;
config[key] = value;
Console.WriteLine($"- {key} = {value}");
}
// 访问解析后的数据
if (config.ContainsKey("server"))
{
Console.WriteLine($"Server: {config["server"]}");
}
}
}
“`
正则表达式性能考虑
虽然正则表达式强大,但编写不当的模式可能会导致性能问题,尤其是处理大型输入文本时。一个常见的陷阱是“回溯失控”(Catastrophic Backtracking)。这通常发生在模式中使用了嵌套的量词,并且匹配失败时,引擎需要尝试大量的回溯路径。
例如,模式 (a+)+b
在输入字符串 aaaaaaaaaaaaaaaaaaaaaaaaaaaaac
上可能会变得非常慢。引擎会尝试匹配尽可能多的 a+
,然后回溯,再尝试匹配 a+
… 如此反复,直到最终匹配失败,回溯路径呈指数级增长。
避免回溯失控的一些方法:
- 避免嵌套量词: 如
(a+)+
。尝试重写模式,例如a+b
。 - 使用原子分组(Atomic Grouping
(?>...)
): 原子分组一旦匹配成功,就不会在其中进行回溯。例如,(?>a+)+b
。如果第一个(?>a+)
匹配了一堆a
,它不会回溯去尝试少匹配几个a
以便让外面的+
匹配更多次。 - 使用非捕获分组
(?:...)
: 如果不需要捕获,使用非捕获分组可以减少一些开销。 - 使用更精确的模式: 过于宽泛的模式
. *
经常是回溯失控的源头。尽量使用字符类或更具体的模式来限制匹配范围。 - 指定超时: 在处理来自不受信任来源的输入时,始终为
Regex
操作设置一个超时时间(通过构造函数或方法重载),以防止恶意输入的 ReDoS (Regular expression Denial of Service) 攻击。
“`csharp
// 设置一个 1 秒的超时
string potentiallySlowPattern = @”(a+)+b”;
string input = “aaaaaaaaaaaaaaaaaaaaaaaaaaaaac”; // 长的 a 序列
try
{
bool isMatch = Regex.IsMatch(input, potentiallySlowPattern, RegexOptions.None, TimeSpan.FromSeconds(1));
Console.WriteLine($”Match found? {isMatch}”);
}
catch (RegexMatchTimeoutException ex)
{
Console.WriteLine($”Regex match timed out: {ex.Pattern}”);
}
“`
最佳实践与学习资源
- 从简单开始: 不要试图一次写出完美的复杂模式。从匹配核心部分开始,逐步添加约束和细节。
- 使用在线测试工具:
regex101.com
,regexr.com
,rubular.com
等网站提供了非常方便的正则表达式测试环境,可以实时看到匹配结果、解释模式含义、甚至帮你调试。强烈推荐使用。 - 利用 C# 的逐字字符串 (
@""
): 这能让你的模式更清晰,减少反斜杠的困扰。 -
为复杂模式添加注释: 使用
RegexOptions.IgnorePatternWhitespace
选项可以在模式中添加空白和注释,提高可读性。“`csharp
string complexEmailPattern = @”
^ # 匹配字符串开头
[\w-.]+ # 用户名部分:字母、数字、下划线、连字符、点号,至少一个
@ # 匹配 @ 符号
([\w-]+.)+ # 域名部分:子域名(字母、数字、连字符,至少一个),后面跟一个点号,重复一次或多次
[\w-]{2,4} # 顶级域名:字母、数字、连字符,2到4个字符
$ # 匹配字符串结尾
“; // 注意这个模式比前面的简陋模式更健壮,但仍不是完全符合RFC规范的email模式bool isValid = Regex.IsMatch(“[email protected]”, complexEmailPattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase);
Console.WriteLine($”有效的邮箱格式 (复杂模式)? {isValid}”); // True
``
System.Text.RegularExpressions` 文档是权威的学习资源。
* **查阅官方文档:** .NET 的
* 练习,练习,再练习: 正则表达式的掌握很大程度上取决于实践。尝试解决不同的文本处理问题,你会越来越熟练。
总结
正则表达式是处理文本的强大工具,它提供了一种简洁而灵活的方式来描述和匹配复杂的字符串模式。通过本文,你应该已经了解了正则表达式的基本构成要素(字面字符、特殊字符、字符类)、控制匹配次数的方式(量词)、匹配位置的方式(锚点)、组织模式和提取信息的方式(分组与捕获)、以及进行选择和断言的方法。
更重要的是,你学会了如何在 C# 中使用 System.Text.RegularExpressions
命名空间提供的 Regex
类,通过 IsMatch
, Match
, Matches
, Replace
, Split
等方法来执行各种文本处理任务。同时,我们也探讨了性能方面的注意事项和一些编写正则表达式的最佳实践。
掌握正则表达式并将其与 C# 的强大功能相结合,将极大地提升你在处理字符串数据时的效率和能力。虽然初学时可能会感到挑战,但投入时间和精力去理解和实践,你将收获一个非常宝贵的技能。现在,开始你的正则表达式之旅吧!通过解决实际问题来巩固你所学的知识,你会发现这个工具的魅力所在。