1正则表达式是什么
正则表达式是一种描述文本模式的搜索公式,像更精确的通配符,用来找出、验证或替换字符串里的内容,比如判断邮箱合不合法、从文字里提取日期。C# 里由 System. 命名空间提供。
视频里用扫描台演示:纸带是字符串,读头是模式,规则全对上的那段就是一个匹配。真实引擎里没有读头在滑,它在每个位置按规则试,试不通就退回来换一种试法。
模式由九类构建块组成,下面逐类看。例子来自一个测试程序:M(编号, 模式, 输入…) 找出所有匹配,R(编号, 模式, 替换串, 输入) 做一次替换,结果写在注释里。
2字符转义
反斜杠让普通字母变出特殊含义(\n 是换行),或让特殊字符变回原样(\. 是真的点号)。C# 字符串里反斜杠本身也是转义字符,直接写 "\d" 会报错:
| 7 | Console.WriteLine("\d"); | // 普通字符串里,\d 不是 C# 认识的转义 |
Program.cs(7,26): error CS1009: 无法识别的转义序列
| 7 | Console.WriteLine("\\d"); | // 改法一:两个反斜杠,打印 \d |
| 8 | Console.WriteLine(@"\d"); | // 改法二(推荐):@ 逐字字符串,反斜杠原样保留,打印 \d |
转义表里还有 \t(Tab)、\r(回车)、\n(换行)等控制字符,Windows 的换行通常是 \r\n 两个字符;\040、\x20、 是用编码写的同一个空格。转义表最后一行的模式方括号没闭合,要去掉多出的半截才能用:
| 66 | M("E13a", @"\d+[\+-x\*]\d+\d+[\+-x\*\d+", "(2+2) * 3*9"); | // 方括号没闭合:抛出 RegexParseException(正则解析异常) |
| 67 | M("E13", @"\d+[\+-x\*]\d+", "(2+2) * 3*9"); | // 去掉多出的半截:找到 2+2 和 3*9 |
3字符类
方括号里任意一个字符,开头加 ^ 表示“不在里面的”,减号表示范围;默认区分大小写:
| 73 | M("C03", @"[b-d]irds", "Birds Cirds Dirds", "birds cirds dirds"); | // b-d 是 b、c、d:大写的 0 个,小写的 3 个 |
. 匹配除换行外任意一个字符,\p{Lu} 按 Unicode(给世界各国文字统一编号的标准)类别匹配大写字母。常用简写 \w(单词字符)、\s(空白)、\d(数字),大写的是反义。\w 不只是 [a-zA-Z0-9_],汉字也算;\d 不只是 [0-9],全角数字也算:
| 77 | M("C07", @"\w", "Room#1", "正则_1"); | // 正则_1 四个全匹配:汉字也算单词字符 |
| 81 | M("C11", @"\d", "4 = IV", "45"); | // \d 认全角数字:45 找到 2 个 |
| 82 | M("C11e", @"[0-9]", "45"); | // [0-9] 不认:0 个 |
4定位点
定位点不匹配字符,只匹配一个位置,宽度是零:
| 85 | M("A01", @"^\d{3}", "567-777-"); | // ^ 开头:找到 567 |
| 86 | M("A02", @"-\d{4}$", "8-12-2012"); | // $ 末尾:找到 -2012 |
| 91 | M("A07", @"er\b", "never", "verb"); | // \b 单词边界:never 末尾的 er 匹配,verb 中间的不算 |
\A、\Z、\z 是整个字符串的开头和末尾(\z 末尾多个换行就不算),\G 要紧接上一个匹配。\b 不只是单词和空格之间,标点、字符串两头也算。
5分组构造
圆括号括出子表达式,用来捕获或限定范围。普通括号从 1 开始编号,第 0 组是整个匹配;(?<名字>) 是命名组,(?:) 只分组不保存。
| 94 | M("G01", @"(\w)\1", "deep"); | // (\w) 是第 1 组,\1 再匹配一次同样的字符:找到 ee |
(?=)、(?!)、(?<=)、(?<!) 是零宽断言,只检查前后、不吃字符;(?>) 是原子组,成功后不再回头重试。下面这个实例用的是后发断言:
| 8 | string input = "1851 1999 1950 1905 2003"; | // 五个年份 |
| 9 | string pattern = @"(?<=19)\d{2}\b"; | // 前面挨着 19,后面两个数字再加单词边界 |
99 50 05
19 本身不在结果里,因为断言只检查,不算进匹配。
6限定符
| 105 | M("Q01", @"\d*\.\d", ".0", "19.9", "219.9"); | // * 零次或多次:.0 前面没有数字也匹配 |
| 106 | M("Q02", @"be+", "been", "bent"); | // + 一次或多次:bee、be |
| 107 | M("Q03", @"rai?n", "ran", "rain"); | // ? 零次或一次:ran、rain 都匹配 |
{n} 恰好 n 次,{n,} 至少 n 次,{n,m} n 到 m 次。限定符默认贪婪(能多吃就多吃),后面加 ? 变懒惰(能少吃就少吃):
| 115 | M("Q11", @"\d{2,}?", "166", "29", "1930"); | // 懒惰:166 得到 16;1930 拆成 19、30 |
{n,m}? 也一样,每次只取最少的位数。初学时先掌握 *、+、?、{n} 就够了。
7反向引用、备用构造
反向引用再匹配一次已捕获的内容:\1 按编号,\k<名字> 按组名(尖括号里不能有空格,写成 \k< char> 会抛出 RegexParseException)。备用构造用 | 表示“或”,还能写成条件形式 (?(条件)是|否):
| 122 | M("O01", @"th(e|is|at)", "this is the day. "); | // th 后跟 e、is、at 之一:找到 this、the |
8替换、杂项构造
替换串里用 $ 引用捕获到的内容:
| 126 | R("R01", @"\b(\w+)(\s)(\w+)\b", "$3$2$1", "one two"); | // 第 3、2、1 组倒过来拼,得到 two one |
还有 ${名字}、$$(真正的 $)、$&(整个匹配)等。$& 那一行实测得到 $1.:模式在末尾又匹配到一个空串,也被替换了一次,所以末尾多两个星号。杂项构造里,(?i) 从这里往后忽略大小写,(?#…) 是内联注释。
9Regex 类的常用方法
文件顶部写 using System.,同一个模式:一个或多个数字。
| 8 | Regex r = new Regex(@"\d+"); | // 用模式创建 Regex 对象 |
| 9 | Console.WriteLine(r.IsMatch("abc123")); | // IsMatch:有没有能匹配的,True(常用来验证表单) |
| 10 | Console.WriteLine(r.IsMatch("123abc", 3)); | // 从下标 3 开始找,后面只剩字母,False |
| 11 | Console.WriteLine(Regex.IsMatch("abc123", @"\d+")); | // 静态写法,不用先创建对象,True |
| 12 | foreach (Match m in r.Matches("a1b22c333")) | // Matches:找出所有匹配,每个是一个 Match 对象 |
| 13 | Console.WriteLine(m.Value); | // 依次打印 1、22、333 |
| 14 | Console.WriteLine(r.Replace("a1b22c333", "#")); | // Replace:所有匹配换成 #,得到 a#b#c# |
| 15 | string[] parts = r.Split("a1b22c333"); | // Split:按匹配把字符串切开 |
| 16 | Console.WriteLine(parts.Length + " 段:" + string.Join(",", parts)); | // 4 段:a,b,c, 末尾数字后切出一个空字符串 |
查看完整输出(8 行)
True False True 1 22 333 a#b#c# 4 段:a,b,c,
10三个实例
前两个实例里的 showMatch 是例子自带的辅助方法:先打印模式,再用 Matches 逐个打印匹配到的单词。
| 22 | showMatch(str, @"\bS\S*"); | // 单词边界 + 大写 S + 任意非空白:S 开头的单词 |
Matching words that start with 'S': The Expression: \bS\S* Splendid Suns
| 22 | showMatch(str, @"\bm\S*e\b"); | // m 开头、e 结尾的单词:make、maze、manage、measure |
| 10 | string input = "Hello World "; | // 中间三个空格,末尾也有三个 |
| 11 | string pattern = "\\s+"; | // 普通字符串写两个反斜杠:一个或多个连续空白 |
| 12 | string replacement = " "; | // 换成一个空格 |
| 13 | Regex rgx = new Regex(pattern); | // 创建 Regex 对象 |
| 14 | string result = rgx.Replace(input, replacement); | // 得到 "Hello World ":末尾三个空格也换成了一个 |
11GeneratedRegex
.NET 7 起可以用 [GeneratedRegex] 特性(方括号标签,后面会讲):它是类库带的源生成器(编译时自动替你写代码的工具),不是新语法,在编译阶段就把正则生成成 C# 代码。方法必须是 partial(分部:一个类或方法分几处写,另一半由编译器补上)方法,所在类型也要加 partial,否则报 CS0260。
| 7 | partial class Program | // 所在的类必须加 partial |
| 8 | { | // Program 类开始 |
| 9 | // 使用 [GeneratedRegex] 特性,编译器自动生成实现 | |
| 10 | [GeneratedRegex(@"\d{4}-\d{2}-\d{2}")] | // 写上模式:日期 |
| 11 | private static partial Regex DateRegex(); | // 只写声明,调用 DateRegex() 得到 Regex 对象 |
Main 里用 DateRegex(). 找出两个日期,再用另一个忽略大小写的 StartsWithSRegex() 判断 Splendid:
找到日期:2024-06-18 找到日期:2025-01-01 True
传统写法里,静态 Regex. 会缓存最近用过的模式(默认 15 个),同一个模式不会每次都重新解析;RegexOptions. 运行更快,但启动慢、占内存,AOT 发布(提前编译成原生程序)时不起作用。GeneratedRegex 最快、兼容 AOT,但模式必须是编译时常量:
| 8 | [GeneratedRegex(pattern)] | // pattern 是普通静态变量,不是常量 |
Program.cs(9,33): error CS8795: 分部方法“Program.NumRegex()”必须具有实现部分,因为它具有可访问性修饰符。 Program.cs(8,20): error CS0182: 特性实参必须是特性形参类型的常量表达式、typeof 表达式或数组创建表达式
建议:模式固定又是 .NET 7 以上,优先 GeneratedRegex;模式要运行时拼出来,只能用 new Regex;初学先用传统写法练熟。
| 9 | Regex r = new Regex(word + @"\w*"); | // word 是变量 "cat":拼出 cat\w*,找到 3 个单词 |
12实例:验证邮箱
| 7 | [GeneratedRegex(@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", RegexOptions.IgnoreCase)] | // ^ 到 $:整个字符串都要符合规则 |
tom@example.com: 有效 bad-email: 无效 li.lei@school.edu.cn: 有效 missing@dot: 无效
模式要求 @ 前是字母、数字或 .,@ 后是域名、一个真正的点号、至少两个字母。教程里的两个邮箱是占位文字,这里换成了示例地址。bad-email 没有 @,missing@dot 的 @ 后面没有点号。
13小结
- 字符类管什么字符,限定符管几次,定位点管位置;圆括号分组捕获,替换时用
$引用。 - 用
Regex类匹配、替换、切分,模式写成@逐字字符串;模式固定又看重性能,用 GeneratedRegex。
下一节:程序运行时出了错,比如除以零、文件找不到,怎么把它接住,不让程序直接崩溃?
