C# Regex 基础教程 – wiki基地


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}?: 匹配 nm 次,但尽可能少。
  • {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 测试工具,开始实践吧!通过不断尝试和调试不同的模式,你将越来越熟悉这门强大的模式匹配语言。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部