C# Regex 基础教程:掌握文本匹配的强大工具
在软件开发中,我们经常需要处理文本数据。无论是验证用户输入(如邮箱格式、电话号码)、从日志文件中提取特定信息、解析配置文件,还是进行复杂的字符串查找和替换,都离不开强大的文本处理能力。而正则表达式(Regular Expression,简称 Regex)正是为此而生的利器。
Regex 是一种用于描述文本模式的语言。它使用一套特殊的字符组合来定义一个搜索模式,然后可以在字符串中查找、匹配、替换或分割符合这个模式的部分。虽然初看起来 Regex 语法可能有些晦涩,但一旦掌握了基础,你会发现它能极大地提高你的文本处理效率。
在 C# 中,Regex 的功能由 System.Text.RegularExpressions
命名空间提供。本教程将带你从零开始,逐步掌握 C# 中 Regex 的基础知识和常用方法。
1. 什么是正则表达式 (Regex)?为什么使用它?
什么是 Regex?
简单来说,正则表达式是一种强大的文本模式匹配工具。你可以把它想象成一种微型的、高度优化的编程语言,专门用于在字符串中查找符合特定规则的文本。这些规则通过一个字符串模式来定义,这个模式可以包含普通字符(匹配自身)和特殊字符(元字符,具有特殊含义)。
为什么使用 Regex?
- 灵活性和强大性: Regex 可以描述几乎任何复杂的文本模式,远超简单的字符串查找(如
string.Contains()
或string.IndexOf()
)。 - 效率: 对于复杂的模式匹配和处理,Regex 引擎通常比手动编写的字符串处理代码更高效。
- 简洁性: 尽管语法密集,但一个简洁的 Regex 模式可以替代几十甚至上百行复杂的条件判断和循环代码。
- 跨平台和语言: Regex 概念和大部分语法在多种编程语言和工具中是通用的(Perl, Python, Java, JavaScript, .NET, Linux Shell 等),学习成本可以跨越技术栈。
当然,Regex 也有缺点:
- 可读性差: 复杂的 Regex 模式对不熟悉的人来说非常难以理解和维护。
- 学习曲线: 元字符和语法规则众多,需要花时间学习和实践。
- 性能陷阱: 编写不当的 Regex(如贪婪匹配和回溯问题)可能导致性能急剧下降甚至崩溃(拒绝服务攻击的一种形式)。
尽管有这些挑战,但在需要进行模式匹配的场景下,Regex 仍然是首选工具。
2. C# 中的 Regex:System.Text.RegularExpressions
在 C# 中,Regex 的所有核心功能都封装在 System.Text.RegularExpressions
命名空间中。最主要的类是 Regex
。
要使用 Regex,你需要先引入这个命名空间:
csharp
using System.Text.RegularExpressions;
Regex
类提供了多种方法来进行模式匹配、查找、替换和分割操作。这些方法通常以静态方法或实例方法的形式提供。
3. 正则表达式基础语法 (模式语言)
Regex 模式是一个字符串。它由以下几种基本元素构成:
3.1. 字面字符 (Literal Characters)
大多数字符(如字母、数字)在 Regex 中代表它们自身。例如,模式 cat
会匹配字符串中的 “cat”。
3.2. 元字符 (Metacharacters)
元字符是 Regex 中具有特殊含义的字符。它们是构建复杂模式的关键。常见的元字符包括:
-
.
(点号): 匹配除换行符\n
之外的任意单个字符。(在 Singleline 模式下可以匹配包括\n
在内的所有字符)- 示例:
a.c
可以匹配 “abc”, “adc”, “axc” 等。
- 示例:
-
\
(反斜杠): 转义字符。用于取消元字符的特殊含义,或者表示一个特殊的字符序列。- 示例: 要匹配一个字面点号,你需要使用
\.
。要匹配一个字面反斜杠,你需要使用\\
。
- 示例: 要匹配一个字面点号,你需要使用
-
^
(脱字符): 匹配字符串的开始位置。(在 Multiline 模式下也可以匹配每一行的开始)- 示例:
^abc
只匹配以 “abc” 开头的字符串。
- 示例:
-
$
(美元符号): 匹配字符串的结束位置。(在 Multiline 模式下也可以匹配每一行的结束)- 示例:
abc$
只匹配以 “abc” 结尾的字符串。
- 示例:
-
|
(竖线): 或运算符。匹配|
左边或右边的模式。- 示例:
cat|dog
匹配 “cat” 或 “dog”。
- 示例:
-
()
(圆括号): 分组和捕获。- 分组: 将多个字符组合成一个逻辑单元,可以对其应用量词。示例:
(ab)+
匹配 “ab”, “abab”, “ababab” 等。 - 捕获: 默认情况下,括号内的匹配内容会被“捕获”,后续可以提取出来。
- 分组: 将多个字符组合成一个逻辑单元,可以对其应用量词。示例:
-
[]
(方括号): 字符类。匹配方括号内列出的任意一个字符。- 示例:
[aeiou]
匹配任意一个元音字母。 - 示例:
[0-9]
匹配任意一个数字(等同于\d
)。 - 示例:
[a-zA-Z]
匹配任意一个英文字母(大小写)。 - 可以使用
-
表示范围。 - 在
[]
内部,大多数元字符会失去特殊含义(除了^
,-
,]
和\
)。
- 示例:
-
[^]
(方括号带脱字符): 否定字符类。匹配方括号内未列出的任意一个字符。- 示例:
[^0-9]
匹配任意一个非数字字符(等同于\D
)。
- 示例:
3.3. 量词 (Quantifiers)
量词指定了前面的元素(单个字符、字符类或分组)出现的次数。
-
*
(星号): 匹配前面的元素零次或多次 (≥ 0)。- 示例:
a*
匹配 “”, “a”, “aa”, “aaa” 等。 - 示例:
[0-9]*
匹配任意长度的数字串,包括空串。
- 示例:
-
+
(加号): 匹配前面的元素一次或多次 (≥ 1)。- 示例:
a+
匹配 “a”, “aa”, “aaa” 等,但不匹配空串。 - 示例:
[0-9]+
匹配任意长度的数字串,但不包括空串。
- 示例:
-
?
(问号): 匹配前面的元素零次或一次 (0 或 1)。也用于惰性匹配。- 示例:
colou?r
匹配 “color” 或 “colour”。
- 示例:
-
{n}
: 精确匹配前面的元素n
次。- 示例:
a{3}
只匹配 “aaa”。 - 示例:
[0-9]{4}
匹配恰好四位的数字。
- 示例:
-
{n,}
: 匹配前面的元素至少n
次。- 示例:
a{2,}
匹配 “aa”, “aaa”, “aaaa” 等。 - 示例:
[0-9]{10,}
匹配至少十位的数字。
- 示例:
-
{n,m}
: 匹配前面的元素至少n
次,但不超过m
次。- 示例:
a{2,4}
匹配 “aa”, “aaa”, “aaaa”。
- 示例:
量词的贪婪性与惰性 (Greedy vs. Lazy)
默认情况下,量词是“贪婪”的,它们会尽可能多地匹配字符。例如,用模式 "<.*>"
去匹配字符串 "<b>bold</b><i>italic</i>"
,贪婪的 .*
会匹配 "bold</b><i>italic"
,整个模式匹配到 "<b>bold</b><i>italic</i>"
。
如果希望量词进行“惰性”匹配,即尽可能少地匹配字符,可以在量词后面加上 ?
。
*?
: 匹配零次或多次,但尽可能少。+?
: 匹配一次或多次,但尽可能少。??
: 匹配零次或一次,但尽可能少。{n,m}?
: 匹配n
到m
次,但尽可能少。{n,}?
: 匹配至少n
次,但尽可能少。
示例: 用模式 "<.*?>"
去匹配字符串 "<b>bold</b><i>italic</i>"
,惰性的 .*?
会先匹配到第一个 >
,然后尝试匹配下一个。结果会得到两个匹配项:"<b>bold</b>"
和 "<i>italic</i>"
。
3.4. 常用特殊字符序列 (Shorthands)
为了方便,Regex 提供了一些特殊的字符序列来代表常用的字符类:
\d
: 匹配任意一个数字 (0-9)。等同于[0-9]
。\D
: 匹配任意一个非数字字符。等同于[^0-9]
。\s
: 匹配任意一个空白字符(空格、制表符\t
、换行符\n
、回车符\r
、换页符\f
等)。\S
: 匹配任意一个非空白字符。\w
: 匹配任意一个“单词”字符(字母 A-Z, a-z,数字 0-9,以及下划线_
)。等同于[a-zA-Z0-9_]
。\W
: 匹配任意一个非“单词”字符。等同于[^a-zA-Z0-9_]
。\b
: 匹配一个单词边界。单词边界是指一个\w
字符和\W
字符之间的位置,或者\w
字符与字符串的开始/结束之间的位置。- 示例:
\bcat\b
只匹配独立的单词 “cat”,不匹配 “catalog” 或 “tomcat”。
- 示例:
\B
: 匹配一个非单词边界。- 示例:
\Bcat\B
会匹配 “catalog” 中的 “cat”,但不匹配独立的 “cat”。
- 示例:
3.5. 分组与捕获 (Grouping and Capturing)
圆括号 ()
除了用于量词分组外,还默认用于“捕获”。每个捕获组会将其匹配到的子字符串存储起来,可以在 C# 代码中单独访问。
(...)
: 捕获组。示例:(\d{3})-(\d{4})
匹配电话号码格式XXX-XXXX
,并捕获区号和后四位数字作为两个独立的组。(?:...)
: 非捕获组。只用于分组,不对匹配内容进行捕获。这可以提高一点性能,并避免在结果中出现不需要的组。示例:(?:ab)+
只匹配 “ab”, “abab” 等,但不会捕获 “ab”。
捕获组是按左括号出现的顺序从 1 开始编号的(0 号组代表整个匹配到的文本)。
3.6. 转义字符 \
在 C# 字符串中的使用
在 C# 中,字符串本身就使用反斜杠 \
作为转义字符(例如 \n
表示换行)。这意味着如果你想在 Regex 模式中表示一个字面的反斜杠 \
,你需要在 C# 字符串中写成 \\
。同样,如果你的 Regex 模式中包含 \.
(匹配字面点号),你在 C# 字符串中需要写成 \\.
。
这会导致大量的双反斜杠,降低可读性。为了避免这个问题,强烈建议使用 C# 的逐字字符串字面量,即在字符串前加上 @
符号。在逐字字符串中,反斜杠不再是 C# 的转义字符,你可以直接按照 Regex 语法书写模式。
- 不推荐:
"\\.\\*"
(匹配字面.
) - 推荐:
@"\.\*"
(匹配字面.
后跟字面*
)
在编写 Regex 模式时,始终优先考虑使用 @""
逐字字符串。
4. 在 C# 中使用 Regex 类的方法
Regex
类提供了多种静态方法和实例方法来执行 Regex 操作。通常,静态方法适用于只需要进行一次匹配或替换的场景,而实例方法(先创建 Regex
对象)在需要重复使用同一个模式时性能更好(因为模式只需要解析编译一次)。
4.1. Regex.IsMatch()
:检查字符串是否匹配模式
这是最简单的用法,只返回一个布尔值,表示整个字符串或字符串的某个部分是否符合指定的模式。
“`csharp
string input = “hello world 123″;
string pattern = @”\d+”; // 匹配一个或多个数字
// 静态方法
bool isMatch = Regex.IsMatch(input, pattern); // true (因为包含 123)
Console.WriteLine($”‘{input}’ 是否包含数字: {isMatch}”);
string input2 = “abc”;
bool isMatch2 = Regex.IsMatch(input2, pattern); // false
Console.WriteLine($”‘{input2}’ 是否包含数字: {isMatch2}”);
// 实例方法 (如果需要多次使用同一模式,创建实例更高效)
Regex regex = new Regex(pattern);
bool isMatch3 = regex.IsMatch(input); // true
Console.WriteLine($”‘{input}’ 是否包含数字 (实例方法): {isMatch3}”);
“`
IsMatch
默认情况下只要找到一个匹配项就返回 true
。如果想检查整个字符串是否完全符合模式,需要使用 ^
和 $
锚点。
“`csharp
string phonePattern = @”^\d{3}-\d{4}$”; // 匹配 “三位数字-四位数字” 且必须是整个字符串
string phone1 = “123-4567”;
string phone2 = “123-45678”;
string phone3 = “abc-defg”;
Console.WriteLine($”‘{phone1}’ 是否是有效的电话号码格式: {Regex.IsMatch(phone1, phonePattern)}”); // true
Console.WriteLine($”‘{phone2}’ 是否是有效的电话号码格式: {Regex.IsMatch(phone2, phonePattern)}”); // false (末尾多了一个数字)
Console.WriteLine($”‘{phone3}’ 是否是有效的电话号码格式: {Regex.IsMatch(phone3, phonePattern)}”); // false (非数字)
“`
4.2. Regex.Match()
:查找第一个匹配项
这个方法返回一个 Match
对象,代表在输入字符串中找到的第一个符合模式的匹配项。如果没有找到,则返回一个不成功的 Match
对象。
Match
对象的主要属性:
Success
: 布尔值,表示是否成功找到匹配项。Value
: 字符串,表示整个匹配到的文本。Index
: 整数,表示匹配项在输入字符串中的起始位置。Length
: 整数,表示匹配项的长度。Groups
: 一个GroupCollection
,包含捕获组的信息。第 0 个 Group 是整个匹配项。
“`csharp
string text = “The year is 2023, and the month is 11.”;
string pattern = @”\d+”; // 匹配一个或多个数字
Match firstMatch = Regex.Match(text, pattern);
if (firstMatch.Success)
{
Console.WriteLine(“找到第一个数字:”);
Console.WriteLine($” 匹配值: {firstMatch.Value}”); // 输出: 2023
Console.WriteLine($” 起始位置: {firstMatch.Index}”); // 输出: 13
Console.WriteLine($” 长度: {firstMatch.Length}”); // 输出: 4
}
else
{
Console.WriteLine(“没有找到数字。”);
}
// 使用捕获组的例子
string phoneText = “Contact info: 123-4567 or 987-6543″;
string phonePatternWithGroups = @”(\d{3})-(\d{4})”;
Match phoneMatch = Regex.Match(phoneText, phonePatternWithGroups);
if (phoneMatch.Success)
{
Console.WriteLine(“\n找到第一个电话号码:”);
Console.WriteLine($” 整个匹配: {phoneMatch.Value}”); // 输出: 123-4567
Console.WriteLine($” 捕获组 1 (区号): {phoneMatch.Groups[1].Value}”); // 输出: 123
Console.WriteLine($” 捕获组 2 (后四位): {phoneMatch.Groups[2].Value}”); // 输出: 4567
// 也可以通过名称访问捕获组,如果模式中使用了命名捕获组 (?<name>...)
// string patternWithNamedGroups = @"(?<AreaCode>\d{3})-(?<Number>\d{4})";
// Match namedMatch = Regex.Match(phoneText, patternWithNamedGroups);
// Console.WriteLine($" 命名组 AreaCode: {namedMatch.Groups["AreaCode"].Value}");
}
“`
4.3. Regex.Matches()
:查找所有匹配项
这个方法返回一个 MatchCollection
对象,它是一个包含所有成功匹配的 Match
对象的集合。你可以遍历这个集合来处理每一个匹配项。
“`csharp
string textWithNumbers = “Invoice numbers: 1001, 1002, 1005, 2003.”;
string pattern = @”\d+”;
MatchCollection allMatches = Regex.Matches(textWithNumbers, pattern);
Console.WriteLine(“\n找到所有数字:”);
if (allMatches.Count > 0)
{
foreach (Match match in allMatches)
{
Console.WriteLine($” 匹配值: {match.Value}, 位置: {match.Index}”);
}
// 输出:
// 匹配值: 1001, 位置: 17
// 匹配值: 1002, 位置: 23
// 匹配值: 1005, 位置: 29
// 匹配值: 2003, 位置: 35
}
else
{
Console.WriteLine(“没有找到匹配项。”);
}
// 使用捕获组并遍历所有匹配
string allPhones = “Phones: 123-4567, 987-6543, 555-1212.”;
string phonePatternWithGroups = @”(\d{3})-(\d{4})”;
MatchCollection phoneMatches = Regex.Matches(allPhones, phonePatternWithGroups);
Console.WriteLine(“\n找到所有电话号码:”);
foreach (Match match in phoneMatches)
{
Console.WriteLine($” 整个匹配: {match.Value}, 区号: {match.Groups[1].Value}, 号码: {match.Groups[2].Value}”);
}
// 输出:
// 整个匹配: 123-4567, 区号: 123, 号码: 4567
// 整个匹配: 987-6543, 区号: 987, 号码: 6543
// 整个匹配: 555-1212, 区号: 555, 号码: 1212
“`
4.4. Regex.Replace()
:替换匹配到的文本
这个方法用指定的替换字符串替换输入字符串中所有(或第一个)符合模式的部分。
Regex.Replace(input, pattern, replacement)
: 替换所有匹配项。Regex.Replace(input, pattern, replacement, count)
: 替换前count
个匹配项。
替换字符串可以使用捕获组的引用:
$0
: 引用整个匹配到的文本。$1
,$2
, …: 引用第 1, 2, … 个捕获组的匹配文本。${name}
: 引用名为name
的捕获组的匹配文本(如果使用了命名捕获组)。
“`csharp
string originalText = “Date: 2023/11/20. Time: 14:30.”;
string pattern = @”(\d{4})/(\d{2})/(\d{2})”; // 匹配 YYYY/MM/DD 格式
string replacement = “$3.$2.$1”; // 替换为 DD.MM.YYYY 格式
string replacedText = Regex.Replace(originalText, pattern, replacement);
Console.WriteLine($”原始文本: {originalText}”);
Console.WriteLine($”替换后文本: {replacedText}”); // 输出: Date: 20.11.2023. Time: 14:30.
// 替换数字为 [NUMBER]
string numbersText = “There are 3 apples and 5 oranges.”;
string numberPattern = @”\d+”;
string replacedNumbers = Regex.Replace(numbersText, numberPattern, “[NUMBER]”);
Console.WriteLine($”替换数字后: {replacedNumbers}”); // 输出: There are [NUMBER] apples and [NUMBER] oranges.
// 使用 MatchEvaluator 进行更复杂的替换
// MatchEvaluator 是一个委托 (Match match) => string
string censoredText = “Call me at 123-456-7890 or 987-654-3210.”;
string phonePatternToCensor = @”\d{3}-\d{3}-\d{4}”;
string censored = Regex.Replace(censoredText, phonePatternToCensor, (match) =>
{
// 获取匹配到的手机号
string phoneNumber = match.Value;
// 只保留后四位,前面用星号替换
return “–-” + phoneNumber.Substring(phoneNumber.Length – 4);
});
Console.WriteLine($”审查后文本: {censored}”); // 输出: Call me at –-7890 or –-3210.
“`
MatchEvaluator
委托允许你根据每个匹配到的具体内容动态生成替换字符串,提供了极大的灵活性。
4.5. Regex.Split()
:分割字符串
这个方法使用正则表达式模式作为分隔符来分割字符串,返回一个字符串数组。
“`csharp
string data = “apple, banana; cherry\tdate”;
// 使用逗号、分号或制表符作为分隔符,可能前后有空白
string separatorPattern = @”[,\;\s]+”;
string[] items = Regex.Split(data, separatorPattern);
Console.WriteLine(“\n分割后的项目:”);
foreach (string item in items)
{
Console.WriteLine($” – {item}”);
}
// 输出:
// – apple
// – banana
// – cherry
// – date
string messyData = ” item1 = value1 ; item2: value2 “;
// 分割基于等号或冒号,前后可能有空白
string messySeparatorPattern = @”\s[:=]\s“;
string[] parts = Regex.Split(messyData, messySeparatorPattern);
Console.WriteLine(“\n分割后的部分:”);
foreach (string part in parts)
{
Console.WriteLine($” – {part}”);
}
// 输出:
// – item1
// – value1
// – item2
// – value2
“`
5. RegexOptions:控制匹配行为
Regex
类的方法和构造函数通常接受一个可选的 RegexOptions
枚举参数,用于控制 Regex 引擎的行为。常用的选项包括:
RegexOptions.None
: 默认选项。RegexOptions.IgnoreCase
: 忽略大小写进行匹配。RegexOptions.Multiline
: 使^
和$
匹配每一行的开始和结束,而不仅仅是整个字符串的开始和结束。RegexOptions.Singleline
: 使.
匹配包括换行符在内的所有字符。RegexOptions.ExplicitCapture
: 禁用默认的捕获组,只有使用(?<name>...)
或(...)
明确标记的组才会被捕获。RegexOptions.Compiled
: 将正则表达式编译为 MSIL 代码,通常可以提高性能,但首次创建 Regex 对象时会有编译开销。适用于需要重复使用同一模式多次的场景。
可以将多个选项通过位或运算符 |
组合使用。
“`csharp
string quote = “To be, or not to be, that is the question.”;
string pattern = @”^to be”; // 匹配以 “to be” 开头
// 默认(区分大小写,^匹配字符串开头)
bool matchDefault = Regex.IsMatch(quote, pattern); // false (‘T’ != ‘t’)
Console.WriteLine($”默认匹配: {matchDefault}”);
// 忽略大小写
bool matchIgnoreCase = Regex.IsMatch(quote, pattern, RegexOptions.IgnoreCase); // true (‘T’ == ‘t’)
Console.WriteLine($”忽略大小写匹配: {matchIgnoreCase}”);
// 多行模式示例
string multilineText = “Line 1\nLine 2\nLine 3″;
string startOfLinePattern = @”^\w+”; // 匹配行开头的单词字符序列
// 默认(^匹配字符串开头)
MatchCollection defaultMatches = Regex.Matches(multilineText, startOfLinePattern);
Console.WriteLine(“\n默认模式匹配行开头:”);
foreach (Match m in defaultMatches) Console.WriteLine(m.Value); // 输出: Line
// 多行模式(^匹配每一行开头)
MatchCollection multilineMatches = Regex.Matches(multilineText, startOfLinePattern, RegexOptions.Multiline);
Console.WriteLine(“\n多行模式匹配行开头:”);
foreach (Match m in multilineMatches) Console.WriteLine(m.Value);
// 输出:
// Line
// Line
// Line
“`
6. 正则表达式实践与调试
编写正确的正则表达式需要大量的练习和调试。以下是一些建议:
- 从小处着手: 不要试图一步写出一个复杂的模式。先写一个匹配最基本部分的模式,然后逐步添加规则。
- 使用在线测试工具: 许多在线工具(如 regex101.com, regexr.com, regextester.com)可以让你输入模式和测试字符串,实时查看匹配结果和解释。这对于学习和调试 Regex 至关重要。它们通常也能可视化地展示模式的匹配过程。
- 分解复杂模式: 对于非常复杂的模式,可以考虑使用 C# 中的多个 Regex 操作,或者使用 Regex 的注释模式
RegexOptions.IgnorePatternWhitespace
并用#
添加注释(这需要将模式写在多行,并开启相应选项)。 - 理解回溯: 了解 Regex 引擎的工作原理,特别是回溯(backtracking),可以帮助你写出更高效且避免意外匹配的模式。尽管这是更高级的话题,但认识到某些模式(如重复的捕获组内使用量词,或者
(a|a)*
这样的模式)可能导致灾难性的性能问题(Catastrophic Backtracking)是很重要的。 - 测试边缘情况: 考虑输入字符串为空、只有空白、包含特殊字符、边界值等情况,确保你的模式在各种情况下都能正确工作。
7. 总结与展望
本教程介绍了 C# Regex 的基础知识,包括:
- Regex 的基本概念和用途。
System.Text.RegularExpressions
命名空间和Regex
类。- 核心 Regex 语法元素:字面字符、元字符、字符类、量词、锚点、分组、特殊字符序列。
- C# 中使用
Regex.IsMatch
,Regex.Match
,Regex.Matches
,Regex.Replace
,Regex.Split
方法进行 Regex 操作。 RegexOptions
控制匹配行为。- 编写和调试 Regex 的实践建议。
正则表达式是一个功能强大但需要练习才能掌握的工具。掌握了这些基础知识,你就可以开始在你的 C# 项目中有效地使用 Regex 来解决各种文本处理问题。
这只是 Regex 的冰山一角。还有许多更高级的概念,如零宽断言(Lookarounds)、条件匹配、反向引用(Backreferences)在替换字符串中的使用、更深入的性能优化等。随着你的经验增长,你可以继续深入学习这些主题,进一步提升你的文本处理能力。
现在,拿起你的 C# 编辑器和 Regex 测试工具,开始实践吧!通过不断尝试和调试不同的模式,你将越来越熟悉这门强大的模式匹配语言。