30 分钟正则表达式教程






4.96/5 (707投票s)
2004年12月18日
20分钟阅读

4906965

26851
使用 Expresso 在 30 分钟内学习如何使用正则表达式。
使用 Expresso 学习 .NET 正则表达式
你是否曾好奇正则表达式究竟是什么,并想快速获得基本了解?我的目标是让你在 30 分钟内掌握正则表达式的基本知识并开始使用。实际上,正则表达式并不像它们看起来那么复杂。最好的学习方法是开始编写和实验。在你的第一个半小时后,你应该了解一些基本构造,并能够在你的程序或网页中设计和使用正则表达式。对于那些着迷的人,有许多优秀的资源可以进一步你的学习。
正则表达式到底是什么鬼?
我相信你熟悉使用“通配符”进行模式匹配。例如,如果你想在一个 Windows 目录中找到所有 Microsoft Word 文件,你会搜索“*.doc
”,你知道星号被解释为一个通配符,可以匹配任何字符序列。正则表达式只是这种能力的一个复杂扩展。
在编写处理文本的程序或网页时,经常需要查找与复杂模式匹配的字符串。正则表达式就是为了描述这些模式而发明的。因此,正则表达式只是模式的简写代码。例如,模式“\w+”是一种简洁的说法,表示“匹配任何非空字母数字字符字符串”。.NET 框架提供了一个强大的类库,可以轻松地将正则表达式包含在你的应用程序中。使用此库,你可以轻松地搜索和替换文本,解码复杂的标头,解析语言或验证文本。
学习正则表达式神秘语法的最好方法是先从示例开始,然后尝试创建自己的正则表达式。本教程介绍了正则表达式的基础知识,提供了许多包含在 Expresso 库文件中的示例。Expresso 可以用来尝试这些示例并试验你自己的正则表达式。
让我们开始吧!
一些简单示例
搜索猫王
假设你把所有空闲时间都花在扫描文件上,寻找猫王仍然活着的证据。你可以用以下正则表达式进行搜索
1.elvis
查找 elvis这是一个完全有效的正则表达式,用于搜索精确的字符序列。在 .NET 中,你可以轻松设置选项以忽略字符的大小写,因此此表达式将匹配“Elvis”、“ELVIS”或“eLvIs”。不幸的是,它也会匹配单词“pelvis”的最后五个字母。我们可以改进表达式如下
2. \belvis\b
将 elvis 作为一个完整单词查找
现在事情变得更有趣了。“\b
”是一个特殊代码,表示“匹配任何单词的开头或结尾位置”。这个表达式将只匹配拼写为“elvis”的完整单词,不分大小写。
假设你想查找所有包含“elvis”后跟“alive”的行。句点或点“.
”是一个特殊代码,匹配除换行符以外的任何字符。星号“*
”表示根据需要重复前一个术语多次以确保匹配。因此,“.*
”表示“匹配任意数量的除换行符以外的字符”。现在构建一个表达式来表示“搜索单词‘elvis’,后面在同一行跟着单词‘alive’”就变得很简单了。
3. \belvis\b.*\balive\b
查找包含“elvis”后跟“alive”的文本
仅用几个特殊字符,我们就开始构建强大的正则表达式,它们对人类来说已经变得难以阅读了。
我们再试一个例子。
确定电话号码的有效性
假设你的网页收集客户的七位电话号码,并且你想验证电话号码的格式是否正确,“xxx-xxxx”,其中每个“x”都是一个数字。以下表达式将搜索文本以查找这样的字符串
4. \b\d\d\d-\d\d\d\d
查找七位电话号码
每个“\d
”表示“匹配任何单个数字”。“-
”没有特殊含义,按字面解释,匹配连字符。为了避免烦人的重复,我们可以使用一种表示相同含义的简写符号
5. \b\d{3}-\d{4}
更好地查找七位电话号码
“{3}
”在“\d
”之后表示“重复前面的字符三次”。
我们来学习如何测试这个表达式。
Expresso
如果你觉得正则表达式不难阅读,你可能是一个白痴学者或来自另一个星球的访客。对于任何人来说,包括那些经常使用正则表达式的人,语法都可能令人望而生畏。这使得错误很常见,并产生了对一个简单工具来构建和测试表达式的需求。存在许多这样的工具,但我偏爱我自己的 Expresso,它最初是在 CodeProject 上发布的。这里显示的是 2.0 版本。对于更高版本,请访问 Ultrapico 网站。
要开始使用,请安装 Expresso 并从 Windows 程序菜单中选择教程。每个示例都可以通过标记为“表达式库”的选项卡进行选择。
图 1. Expresso 运行示例 5
首先选择第一个示例,“1. 查找猫王”。点击“运行匹配”并查看右侧的 TreeView。注意有几个匹配项。点击每个以显示匹配在示例文本中的位置。运行第二个和第三个示例,注意单词“pelvis”不再匹配。最后,运行第四个和第五个示例;两者都应匹配文本中的相同数字。尝试删除开头的“\b
”,并注意部分邮政编码匹配了电话号码的格式。
.NET 正则表达式基础
让我们探讨一下 .NET 正则表达式的一些基础知识。
特殊字符
你应该了解一些具有特殊含义的字符。你已经认识了“\b
”、“.
”、“*
”和“\d
”。要匹配任何空白字符,如空格、制表符和换行符,请使用“\s
”。同样,“\w
”匹配任何字母数字字符。
我们再试几个例子
6. \ba\w*\b
查找以字母 a 开头的单词
这是通过搜索单词开头 (\b),然后是字母“a”,然后是任意数量的字母数字字符重复 (\w*),然后是单词结尾 (\b) 来实现的。
7. \d+
查找重复的数字字符串
这里,“+”类似于“*”,只是它要求至少重复一次。
8. \b\w{6}\b
查找六个字母的单词
在 Expresso 中尝试这些,并通过发明你自己的表达式来开始实验。下面是一些具有特殊含义的字符表
|
匹配除换行符外的任何字符 |
|
匹配任何字母数字字符 |
|
匹配任何空白字符 |
|
匹配任何数字 |
|
匹配单词的开头或结尾 |
|
匹配字符串的开头 |
|
匹配字符串的结尾 |
表 1. 正则表达式常用特殊字符
最初
特殊字符“^
”和“$
”用于查找必须从文本开头和/或文本结尾开始的东西。这对于验证整个文本必须与模式匹配的输入特别有用。例如,要验证七位电话号码,你可能会使用
9. ^\d{3}-\d{4}$
验证七位电话号码
这与示例 (5) 相同,但强制填充整个文本字符串,匹配文本之前或之后没有其他内容。通过在 .NET 中设置“Multiline”选项,“^
”和“$
”的含义会改变,以匹配单行的开头和结尾,而不是整个文本字符串。Expresso 示例使用此选项。
转义字符
如果你真的想匹配其中一个特殊字符,例如“^
”或“$
”,就会出现问题。使用反斜杠来消除特殊含义。因此,“\^
”、“\.
”和“\\
”分别匹配字面字符“^
”、“.
”和“\
”。
重复
你已经看到“{3}
”和“*
”可以用来表示单个字符的重复。稍后,你将看到如何使用相同的语法来重复整个子表达式。有几种其他方式可以指定重复,如下表所示
|
重复任意次数 |
|
重复一次或多次 |
|
重复零次或一次 |
|
重复 n 次 |
|
至少重复 n 次,但不超过 m 次 |
|
至少重复 n 次 |
表 2. 常用量词
我们再试几个例子
10. \b\w{5,6}\b
查找所有五六个字母的单词
11. \b\d{3}\s\d{3}-\d{4}
查找十位电话号码
12. \d{3}-\d{2}-\d{4}
社会安全号码
13. ^\w*
行或文本中的第一个单词
尝试最后一个示例,有和没有设置“Multiline”选项,该选项会改变“^
”的含义。
字符类
查找字母数字、数字和空白字符很简单,但如果我们想从其他字符集中查找任何字符怎么办?这可以通过在方括号中列出所需字符来轻松完成。因此,“[aeiou]
”匹配任何元音,“[.?!]
”匹配句子末尾的标点符号。在此示例中,请注意“.
”和“?
”在方括号内会失去其特殊含义,并被按字面解释。我们还可以指定字符范围,因此“[a-z0-9]
”表示“匹配任何小写字母或任何数字”。
让我们尝试一个更复杂的表达式来搜索电话号码。
14. \(?\d{3}[) ]\s?\d{3}[- ]\d{4}
一个十位电话号码
这个表达式将查找多种格式的电话号码,例如“(800) 325-3535”或“650 555 1212”。“\(?
”搜索零个或一个左括号,“[) ]
”搜索一个右括号或一个空格。“\s?”搜索零个或一个空白字符。不幸的是,它也会找到像“650) 555-1212”这样的情况,其中括号不平衡。下面,你将看到如何使用替代方案来消除这个问题。
取反
有时我们需要搜索不是某个易于定义的字符类成员的字符。下表显示了如何指定此项。
|
匹配任何非字母数字字符 |
|
匹配任何非空白字符 |
|
匹配任何非数字字符 |
|
匹配非单词开头或结尾的位置 |
|
匹配任何非 x 的字符 |
|
匹配任何非 aeiou 中字符的字符 |
表 3. 如何指定你不想要的内容
15. \S+
所有不包含空白字符的字符串
稍后,我们将看到如何使用“先行断言”和“后行断言”来搜索更复杂模式的缺失。
替代方案
要在几种替代方案之间进行选择,如果其中任何一个被满足则允许匹配,请使用竖线“|
”符号来分隔替代方案。例如,邮政编码有两种形式,一种是 5 位数字,另一种是 9 位数字加连字符。我们可以用这个表达式找到其中一种
16. \b\d{5}-\d{4}\b|\b\d{5}\b
五位和九位邮政编码
使用替代方案时,顺序很重要,因为匹配算法会首先尝试匹配最左边的替代方案。如果在这个例子中顺序颠倒,表达式将只找到 5 位邮政编码而找不到 9 位邮政编码。我们可以使用替代方案来改进十位电话号码的表达式,允许区号由空白或括号分隔
17. (\(\d{3}\)|\d{3})\s?\d{3}[- ]\d{4}
十位电话号码,更好的方法
分组
括号可用于划定子表达式,以允许重复或其他特殊处理。例如
18. (\d{1,3}\.){3}\d{1,3}
一个简单的 IP 地址查找器
表达式的第一部分搜索一个一到三位数字,后跟一个字面句点“\.
”。这被括在括号中并使用“{3}
”量词重复三次,然后是相同的表达式,但没有尾随句点。
不幸的是,这个例子允许 IP 地址包含任意的一、二或三位数字,它们之间用句点分隔,尽管一个有效的 IP 地址不能有大于 255 的数字。如果能算术比较捕获的数字 N 来强制 N<256 就好了,但这仅靠正则表达式是不可能的。下一个例子测试基于起始数字的各种替代方案,以通过模式匹配来保证数字的有限范围。这表明即使寻找一个简单描述的模式,表达式也可能变得笨拙。
19. ((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)
IP 查找器
Expresso 分析器视图
图 2. Expresso 的分析器视图显示了示例 17
Expresso 有一个功能,可以将表达式以树状结构图示化,解释每个部分的含义。在调试表达式时,这有助于定位引起问题的部分。通过选择示例 (17) 并点击“分析”按钮来尝试此操作。选择树中的节点并展开它们以探索此正则表达式的结构,如图所示。高亮显示节点后,你还可以使用“部分匹配”或“排除匹配”按钮,仅使用正则表达式的高亮部分或排除高亮部分来运行匹配。
当子表达式用括号分组时,匹配子表达式的文本可用于计算机程序中或正则表达式本身内的进一步处理。默认情况下,组从左到右读取时按顺序编号,从 1 开始。这种自动编号可以在 Expresso 的骨架视图中或成功匹配后显示的结果中看到。
“反向引用”用于搜索先前被组捕获的匹配文本的重复出现。例如,“\1
”表示“匹配被组 1 捕获的文本”。这是一个例子
20. \b(\w+)\b\s*\1\b
查找重复的单词
这是通过在组 1“(\w+)
”中捕获一个至少包含一个字母数字字符的字符串来工作的,但仅当它以单词开头和结尾时。然后它查找任意数量的空白“\s*
”,后跟捕获文本“\1
”的重复,并在单词结尾处结束。
可以通过指定明确的名称或数字来覆盖组的自动编号。在上面的例子中,我们不是将组写成“(\w+)
”,而是可以将其写成“(?<Word>\w+)
”来命名这个捕获组“Word
”。对该组的反向引用写成“\k<Word>
”。试试这个例子
21. \b(?<Word>\w+)\b\s*\k<Word>\b
在命名组中捕获重复单词
在 Expresso 中测试此项并展开匹配结果以查看命名组的内容。
使用括号,有许多特殊用途的语法元素可用。其中最常见的总结在此表中
捕获 | |
|
匹配 exp 并将其捕获到自动编号组中 |
|
匹配 exp 并将其捕获到名为 name 的组中 |
|
匹配 exp,但不捕获它 |
环视 | |
|
匹配前缀 exp 之前的任何位置 |
|
匹配后缀 exp 之后的任何位置 |
|
匹配后缀 exp 未找到的任何位置之后 |
|
匹配前缀 exp 未找到的任何位置之前 |
注释 | |
|
注释 |
表 4. 常用组构造
我们已经讨论了前两个。第三个“(?:
exp)
”不改变匹配行为,它只是不将其捕获到命名或编号组中,不像前两个。
正向环视
接下来的四个是所谓的先行断言或后行断言。它们查找在当前匹配之前或之后出现的内容,但不将其包含在匹配中。重要的是要理解这些表达式匹配一个位置,例如“^
”或“\b
”,并且永远不匹配任何文本。因此,它们被称为“零宽断言”。最好通过示例说明
“(?=
exp)
”是“零宽正向先行断言”。它匹配文本中位于给定后缀之前的位置,但不将后缀包含在匹配中
22. \b\w+(?=ing\b)
以“ing”结尾的单词的开头
“(?<=
exp)
”是“零宽正向后行断言”。它匹配前缀之后的位置,但不将前缀包含在匹配中
23. (?<=\bre)\w+\b
以“re”开头的单词的结尾
这是一个可以重复使用的示例,用于将逗号插入到三位数字组中
24. (?<=\d)\d{3}\b
单词末尾的三位数字,前面是数字
这是一个同时查找前缀和后缀的例子
25. (?<=\s)\w+(?=\s)
由空白字符包围的字母数字字符串
负向环视
之前,我展示了如何搜索不是特定字符或字符类成员的字符。如果我们只是想验证一个字符不存在,但又不想匹配任何东西怎么办?例如,如果我们在搜索单词,其中字母“q”后面没有字母“u”怎么办?我们可以尝试
26. \b\w*q[^u]\w*\b
包含“q”后跟“非 u”的单词
运行示例,你会发现当“q”是单词的最后一个字母时(例如“Iraq”),它会失败。这是因为“[^q]”总是匹配一个字符。如果“q”是单词的最后一个字符,它将匹配后面的空白字符,所以在示例中,表达式最终会匹配两个完整的单词。负向环视解决了这个问题,因为它匹配一个位置,不消耗任何文本。与正向环视一样,它也可以用于匹配任意复杂子表达式的位置,而不仅仅是一个字符。我们现在可以做得更好
27. \b\w*q(?!u)\w*\b
搜索“q”后面没有“u”的单词
我们使用了“零宽负向先行断言”,即“(?!exp)”,它只有在后缀“exp”不存在时才成功。这是另一个例子
28. \d{3}(?!\d)
三位数字后面没有其他数字
类似地,我们可以使用“(?<!
exp)"
,即“零宽负向后行断言”,来搜索文本中前缀“exp”不存在的位置
29. (?<![a-z ])\w{7}
不以字母或空格开头的七个字母数字字符串
这里是另一个使用环视的例子
30. (?<=<(\w+)>).*(?=<\/\1>)
HTML 标签之间的文本
这通过后行断言搜索 HTML 标签,并通过先行断言搜索相应的结束标签,从而捕获中间文本,但排除两个标签。
请评论
括号的另一个用途是使用“(?#comment)”语法包含注释。一个更好的方法是设置“忽略模式空白”选项,它允许在表达式中插入空白,然后在表达式使用时将其忽略。设置此选项后,文本每行末尾井号“#”后面的任何内容都将被忽略。例如,我们可以像这样格式化前面的示例
31. HTML 标签之间的文本,带注释
(?<= # 搜索前缀,但排除它
<(\w+)> # 匹配尖括号内的字母数字标签
) # 结束前缀
.* # 匹配任何文本
(?= # 搜索后缀,但排除它
<\/\1> # 匹配前面捕获的带“/”前缀的标签
) # 结束后缀
贪婪与惰性
当正则表达式的量词可以接受一定范围的重复(如“.*
”)时,其正常行为是匹配尽可能多的字符。考虑以下正则表达式
32. a.*b
以 a 开头并以 b 结尾的最长字符串
如果用它来搜索字符串“aabab”,它将匹配整个字符串“aabab”。这被称为“贪婪”匹配。有时,我们更喜欢“惰性”匹配,其中找到使用最少重复次数的匹配。表 2 中的所有量词都可以通过添加问号“?
”变成“惰性”量词。因此,“*?
”表示“匹配任意数量的重复,但使用最少数量的重复,仍然导致成功匹配”。现在让我们尝试示例 (32) 的惰性版本
33. a.*?b
以 a 开头并以 b 结尾的最短字符串
如果我们将其应用于相同的字符串“aabab”,它将首先匹配“aab”,然后匹配“ab”。
|
重复任意次数,但尽可能少 |
|
重复一次或多次,但尽可能少 |
|
重复零次或一次,但尽可能少 |
|
至少重复 n 次,但不超过 m 次,但尽可能少 |
|
至少重复 n 次,但尽可能少 |
表 5. 惰性量词
我们漏掉了什么?
我已经描述了一组丰富的元素,可用于开始构建正则表达式;但我遗漏了一些内容,这些内容总结在下表中。其中许多在项目文件中都有额外的示例。示例编号显示在本表的左栏中。
# |
语法 |
描述 |
|
贝尔字符 | |
|
通常是单词边界,但在字符类中表示退格键 | |
|
选项卡 | |
34 |
|
回车符 |
|
垂直制表符 | |
|
换页符 | |
35 |
|
新行 |
|
转义 | |
36 |
|
ASCII 八进制代码为 nnn 的字符 |
37 |
|
十六进制代码为 nn 的字符 |
38 |
|
Unicode 为 nnnn 的字符 |
39 |
|
控制 N 字符,例如回车符 (Ctrl-M) 是 \cM |
40 |
|
字符串的开头(类似于 ^,但不依赖于多行选项) |
41 |
|
字符串的结尾或字符串结尾的 \n 之前(忽略多行) |
|
字符串的结尾(忽略多行) | |
42 |
|
当前搜索的开头 |
43 |
|
Unicode 类别名为 name 的任何字符,例如 \p{IsGreek} |
|
贪婪子表达式,也称为非回溯子表达式。它只匹配一次,然后不参与回溯。 | |
44 |
|
平衡组。这很复杂但很强大。它允许在下推/弹出栈上操作命名捕获组,例如,可以用于搜索匹配的括号,这在没有正则表达式的情况下是不可能的。请参阅项目文件中的示例。 |
45 |
|
更改子表达式 exp 的正则表达式选项 |
46 |
|
更改封闭组其余部分的正则表达式选项 |
|
子表达式 exp 被视为零宽正向先行断言。如果在此点匹配,则子表达式 yes 成为下一个匹配,否则使用 no。 | |
|
与上面相同,但 no 表达式为空 | |
|
这与前面案例的语法相同。如果 name 是有效的组名,则如果命名组成功匹配,则匹配 yes 表达式,否则匹配 no 表达式。 | |
47 |
|
与上面相同,但 no 表达式为空 |
表 6. 我们遗漏的所有内容。左栏显示了项目文件中说明此构造的示例编号。
结论
我们已经提供了许多示例来说明 .NET 正则表达式的基本特征,强调了使用 Expresso 等工具通过示例进行测试、实验和学习。如果你对此感兴趣,有许多在线资源可以帮助你进一步学习。你可以从 Ultrapico 网站开始搜索。如果你想读一本书,我推荐 Jeffrey Friedl 的最新版《精通正则表达式》。
Code Project 上还有许多不错的文章,包括以下教程