Skip to content

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);  // 100

6. 贪婪 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 标签内文本>(.*?)<标签间文本
URLhttps?://[\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/2024

3. 使用求值器(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}?

注意事项

  1. 使用 @ 逐字字符串——避免反斜杠转义(@"\d" 而非 "\\d"
  2. 编译高频使用的正则——RegexOptions.Compiled[GeneratedRegex] 源生成器
  3. 避免灾难性回溯——不要嵌套量词,必要时设置超时
  4. 非捕获分组提升性能——不需要提取时使用 (?:...)
  5. 正则不是万能的——解析 HTML/XML 应使用专用解析器
  6. 测试正则——使用在线工具(regex101.com)验证后再写入代码
  7. 注意中文匹配——使用 一-龥 匹配中文字符
  8. ^$ 行为受 Multiline 影响——匹配整个字符串开头/结尾还是每行开头/结尾

Released under the MIT License.