一次搞定正则表达式
前言
在写这篇笔记之时,已经有很多与正则表达式有关的优秀博客了,我的学习也参考了不少优秀大佬的博客,甚至我觉得直接搬运他们写的博客还要好些,但自己动动手写一写的话可以学习理解的更加透彻,不仅要让自己看得懂,还得让自己写的来才行,所以在学习正则表达式时写下了这篇笔记。
概念
使用单个字符串
来描述、匹配一系列符合某个句法规则
的字符串
正则表达式的测试工具
- 推荐RegexBudd:
http://www.regexbuddy.com/
-
RegExBuilder:
http://www.redfernplace.com/software-projects/regex-builder/
-
在线测试工具:
http://tool.chinaz.com/regex/
-
在线练习网站:
https://codejiaonang.com/#/course/regex_chapter1/0/0
一、基础字符
元字符
最基础的东西,相当于英文的26个字母
代码/语法 | 说明 |
---|---|
. | 匹配除换行符以外的任意字符 |
\w | 匹配字母或数字或下划线或汉字[A-Z] 、 [a-z] 、[0-9] 、_
|
\s | 匹配任意的空白符 |
\d | 匹配数字 |
\b | 匹配单词的开始或结束,其他类型的数据也能匹配 |
^ | 匹配字符串的开始 |
$ | 匹配字符串的结束 |
补充一点方便理解:
代码/语法 | 说明 |
---|---|
| | 条件分支或,指明两项或多项之间的一个选择 |
( ) | 多个匹配,将括号里面的内容作为一组 |
[ ] | 单个匹配,满足其中一个匹配即可 |
0\d{2}-\d{8}|0\d{3}-\d{7}
这个表达式能匹配两种以连字号分隔的电话号码:一种是三位区号,8位本地号(如010-12345678),一种是4位区号,7位本地号(0376-2233445)
反义字符
与元字符相反的字符
代码/语法 | 说明 |
---|---|
\W | 匹配任意不是字母,S数字,下划线,汉字的字符 |
\S | 匹配任意不是空白符的字符 |
\D | 匹配任意非数字的字符 |
\B | 匹配不是单词开头或结束的位置 |
[^x] | 匹配除了x以外的任意字符 |
[^aeiou] | 匹配除了aeiou这几个字母以外的任意字符 |
限定字符
限定需要匹配的字符长度来达到重复匹配
代码/语法 | 说明 | 举例 |
---|---|---|
* | 重复零次或更多次 |
zo* 能匹配 z 以及 zoo 。* 等价于{0,}
|
+ | 重复一次或更多次 |
zo+ 能匹配 zo 以及 zoo ,但不能匹配 z ,+ 等价于{1,}
|
? | 重复零次或一次, |
do(es)? 可以匹配 do 或 does 中的do ,? 等价于{0,1}
|
{n} | 非负整数,重复n次 |
o{2} 不能匹配 Bob 中的 o ,但是能匹配 food 中的两个o
|
{n,} | 非负整数,重复n次或更多次 |
o{2,} 不能匹配 Bob 中的 o ,但能匹配 foooood 中的所有 o ,o{1,} 等价于 o+ ,o{0,} 则等价于 o*
|
{n,m} | 非负整数,重复n到m次 |
o{1,3} 将匹配 fooooood 中的前三个 o,o{0,1} 等价于 o?
|
正则表达式的写法很多,同样的限定可以有不同的写法,比如以下的例子:
[0-9] == \d
[a-z0-9A-Z] == \w
转义注释
要匹配元字时,就需要进行字符转义
代码/语法 | 说明 | 举例 |
---|---|---|
\ | 将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符 |
\. \* 会在匹配时转义为. *
|
(?#comment) | 嵌入在正则表达式中的注释,comment为注释内容 | 2[0-4]\d(?#200-249)|25[0-5](?#250-255)|[01]?\d\d?(?#0-199) |
**注意:**要包含注释的话,最好是启用“忽略模式里的空白符”选项,这样在编写表达式时能任意的添加空格,Tab,换行,而实际使用时这些都将被忽略。启用这个选项后,在#后面到这一行结束的所有文本都将被当成注释忽略掉。例如,我们可以前面的一个表达式写成这样来注释每一行(零宽断言):
(?<= # 断言要匹配的文本的前缀
<(\w+)> # 查找尖括号括起来的字母或数字(即HTML/XML标签)
) # 前缀结束
.* # 匹配任意文本
(?= # 断言要匹配的文本的后缀
<\/\1> # 查找尖括号括起来的内容:前面是一个"/",后面是先前捕获的标签
正则表达式中使用时需要转义的字符有:* . ? + $ ^ [ ] ( ) { } | \ /
二、进阶匹配
贪婪与懒惰
贪婪匹配:正则表达式中包含重复的限定符时,通常的行为是匹配尽可能多的字符
a.*b 尽可能多的去匹配a和b的中间内容
aabab ==> aabab
懒惰匹配:匹配任意数量的重复,但是在能使整个匹配成功的前提下匹配尽可能少的字符
a.*?b 尽可能少的去匹配a和b的中间内容
aabab ==> aab
懒惰匹配的一些组合方式:
代码/语法 | 说明 |
---|---|
*? | 重复任意次,但尽可能少重复 |
+? | 重复1次或更多次,但尽可能少重复 |
?? | 重复0次或1次,但尽可能少重复 |
{n,m}? | 重复n到m次,但尽可能少重复 |
{n,}? | 重复n次以上,但尽可能少重复 |
分组匹配
-
将子表达式进行做成子集,使
( )
进分组,方便对match的字符串进行划分 -
分组的命名:
(?<groupname>exp)
,exp就是表达式 -
剔除此分组:
(?:exp)
,匹配exp,但不捕获匹配的文本,也不给此分组分配组号,在使用regexbuddy查看组的时候就不会显示这个分组
后向引用
理解
即在规则中后面如果有和前面组规则相同的部分时可以直接引用前面组里的内容而不用再写一遍
使用
- 要匹配**“taobao taobao”,“home home”** 这样的情况如何处理
-
\b(\w+)\b\s+\1\b
,这里的(\w+)
是一个没有名字的组,没有名字的组会有一个默认的序号,所以我们使用\1
来引用这个组的内容 -
\b(?<Word>\w+)\b\s+\k<Word>\b
,这是命名情况的一种写法,即可以通过使用组的名字来后向引用
零宽断言
理解
**零宽:**具有占位效果的判断条件,既匹配时作为判定条件但不属于需要截取的判定内容
**断言:**可以理解为正则里的一个判断,只有在正则断言为真的时候才会向下执行匹配出我们想要的内容
使用
-
(?=exp)
零宽度正预测先行断言,自身出现的位置的后面能匹配表达式exp
,就是后置规则的意思
\b\w+(?=ing\b)
匹配对象
I'm singing while you're dancing.
匹配结果
sing danc
-
(?<=exp)
零宽度正回顾后发断言,自身出现的位置的前面能匹配表达式exp
,就是前制规则的意思
(?<=\bre)\w+\b
匹配对象
reading a book.
匹配结果
ading
注意:前置和后置的语法略微有所不同,前置(?=exp)
,后置(?<=exp)
负向零宽断言
理解
负向:零宽断言是指包含但不占位,负向就是相反的意思,即作为不包含且不占位的表达式,赛选出不包含exp内容的数据,且这个exp不占位,可以为空
使用
-
(?!exp)
,零宽度负预测先行断言,断言此位置的后面不能匹配表达式exp
查找这样的单词——它里面出现了字母q,但是q后面跟的不是字母u
\b\w*q[^u]\w*\b
可能存在q是字母的开头或结尾所以使用的是*,但实际上这个正则是有问题的,因为[^u]是占位的,他消费了一个字符,所以此时的q后面必须有一个不是u的字符才能匹配的出来,所以匹配不到q作为结尾单词的情况,例如单词:benq
\b\w*q(?!u)\w*\b
因为它只匹配一个位置,并不消费任何字符,所以也也能匹配到benq这样的情况
\d{3}(?!\d)
匹配三位数字,而且这三位数字的后面不能是数字
\b((?!abc)\w)+\b
匹配不包含连续字符串abc的单词
- 同理,我们可以用
(?<!exp)
零宽度正回顾后发断言来断言此位置的前面不能匹配表达式exp
(?<![a-z])\d{7}
匹配前面不是小写字母的七位数字
处理选项
在C#中,你可以使用Regex(String, RegexOptions)构造函数来设置正则表达式的处理选项。如:Regex regex = new Regex("\ba\w{6}\b", RegexOptions.IgnoreCase);
上面介绍了几个选项如忽略大小写,处理多行等,这些选项能用来改变处理正则表达式的方式。下面是.Net中常用的正则表达式选项:
名称 | 说明 |
---|---|
IgnoreCase(忽略大小写) | 匹配时不区分大小写。 |
Multiline(多行模式) | 更改^和$的含义,使它们分别在任意一行的行首和行尾匹配,而不仅仅在整个字符串的开头和结尾匹配。(在此模式下,$的精确含意是:匹配\n之前的位置以及字符串结束前的位置.) |
Singleline(单行模式) | 更改.的含义,使它与每一个字符匹配(包括换行符\n)。 |
IgnorePatternWhitespace(忽略空白) | 忽略表达式中的非转义空白并启用由#标记的注释。 |
RightToLeft(从右向左查找) | 匹配从右向左而不是从左向右进行。 |
ExplicitCapture(显式捕获) | 仅捕获已被显式命名的组。 |
ECMAScript(JavaScript兼容模式) | 使表达式的行为与它在JavaScript里的行为一致。 |
一个经常被问到的问题是:是不是只能同时使用多行模式和单行模式中的一种?答案是:不是。这两个选项之间没有任何关系,除了它们的名字比较相似(以至于让人感到疑惑)以外。
平衡组和递归匹配
有时我们需要匹配像( 100 * ( 50 + 15 ) )这样的可嵌套的层次性结构,这时简单地使用\(.+\)
则只会匹配到最左边的左括号和最右边的右括号之间的内容(这里我们讨论的是贪婪模式,懒惰模式也有下面的问题)。假如原来的字符串里的左括号和右括号出现的次数不相等,比如*( 5 / ( 3 + 2 ) ) )*,那我们的匹配结果里两者的个数也不会相等。有没有办法在这样的字符串里匹配到最长的,配对的括号之间的内容,这hi和我们就需要使用平衡组或递归匹配来完成我们的需求
**注意:**这里介绍的平衡组语法是由.Net Framework支持的;其它语言/库不一定支持这种功能,或者支持此功能但需要使用不同的语法
需要用到的语法结构:
-
(?'group'exp)
把捕获的内容命名为group,并压入堆栈(Stack) -
(?'-group'exp)
从堆栈上弹出最后压入堆栈的名为group的捕获内容,如果堆栈本来为空,则本分组的匹配失败 -
(?(group)yes|no)
如果堆栈上存在以名为group的捕获内容的话,继续匹配yes部分的表达式,否则继续匹配no部分 -
(?!)
零宽负向先行断言,由于没有后缀表达式,试图匹配总是失败
我们需要做的是每碰到了左括号,就在压入一个"Open",每碰到一个右括号,就弹出一个,到了最后就看看堆栈是否为空--如果不为空那就证明左括号比右括号多,那匹配就应该失败。正则表达式引擎会进行回溯(放弃最前面或最后面的一些字符),尽量使整个表达式得到匹配。
举例
<[^<>]*(((?'Open'<)[^<>]*)+((?'-Open'>)[^<>]*)+)*(?(Open)(?!))>
匹配对象
xx <aa <bbb> <bbb> aa> yy
匹配结果
<aa <bbb> <bbb> aa>
平衡组的一个最常见的应用就是匹配HTML,下面这个例子可以匹配嵌套的<div>标签:
<div[^>]*>[^<>]*(((?'Open'<div[^>]*>)[^<>]*)+((?'-Open'</div>)[^<>]*)+)*(?(Open)(?!))</div>.
为了方便理解所以调整了一下格式,如果是regexbuddy需要开启Free-spacing:
< #最外层的左括号
[^<>]* #最外层的左括号后面的不是括号的内容
(
(
(?'Open'<) #碰到了左括号,在黑板上写一个"Open"
[^<>]* #匹配左括号后面的不是括号的内容
)+
(
(?'-Open'>) #碰到了右括号,擦掉一个"Open"
[^<>]* #匹配右括号后面不是括号的内容
)+
)*
(?(Open)(?!)) #在遇到最外层的右括号前面,判断黑板上还有没有没擦掉的"Open";如果还有,则匹配失败
> #最外层的右括号
还有一个稍微简单一点的写法,因为第二层一共就三个不同的情况可以使用分组语法来完成,这样看起来更简单但是理解起来没有上面的方法直观,规则如下:
<
[^<>]*
(
(
(?'Open'<)
|
(?'-Open'>)
|
[^<>]*
)+
)+
(?(Open)(?!))
>
<[^<>]*(((?'Open'<)|(?'-Open'>)|[^<>]*)+)+(?(Open)(?!))>
其他语法
代码/语法 | 说明 |
---|---|
\a | 报警字符(打印它的效果是电脑嘀一声) |
\b | 通常是单词分界位置,但如果在字符类里使用代表退格 |
\t | 制表符,Tab |
\r | 回车 |
\v | 竖向制表符 |
\f | 换页符 |
\n | 换行符 |
\e | Escape |
\0nn | ASCII代码中八进制代码为nn的字符 |
\xnn | ASCII代码中十六进制代码为nn的字符 |
\unnnn | Unicode代码中十六进制代码为nnnn的字符 |
\cN | ASCII控制字符。比如\cC代表Ctrl+C |
\A | 字符串开头(类似^,但不受处理多行选项的影响) |
\Z | 字符串结尾或行尾(不受处理多行选项的影响) |
\z | 字符串结尾(类似$,但不受处理多行选项的影响) |
\G | 当前搜索的开头 |
\p{name} | Unicode中命名为name的字符类,例如\p{IsGreek} |
(?>exp) | 贪婪子表达式 |
(?-exp) | 平衡组 |
(?im-nsx:exp) | 在子表达式exp中改变处理选项 |
(?im-nsx) | 为表达式后面的部分改变处理选项 |
(?(exp)yes|no) | 把exp当作零宽正向先行断言,如果在这个位置能匹配,使用yes作为此组的表达式;否则使用no |
(?(exp)yes) | 同上,只是使用空表达式作为no |
(?(name)yes|no) | 如果命名为name的组捕获到了内容,使用yes作为表达式;否则使用no |
(?(name)yes) | 同上,只是使用空表达式作为no |
三、总结
关于正则表达式的使用场景很多,在不同的语法中正则表达式的一些细节是不一样的,正则表达式匹配需要的内容时要注意有没有漏掉或者多匹配到的。匹配同一个目标可以有多种不同的规则写法,只有多写多练,自己写的正则表达式才会更加的精简和精准。
一些常用正则表达式链接:
- 最全的常用正则表达式大全——包括校验数字、字符、一些特殊的需求等等 - zxin - 博客园 (cnblogs.com).
- (33条消息) 工作中常用正则表达式总结_huxiutao的博客-CSDN博客.
- 常用的正则表达式_正则表达式大全_脚本之家 (jb51.net).
参考:
- 正则表达式教程:30分钟让你精通正则表达式语法_C语言中文网 (biancheng.net).
- 正则表达式入门到进阶_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili..
- 正则表达式 – 教程 | 菜鸟教程 (runoob.com).
- 正则表达式全解析+常用正则表达式例子详解.