通过网站 https://regex101.com/ 可以测试正则表达式的匹配结果及匹配过程.
本文章抛开各个编程语言实现差异, 仅做正则本身的介绍, 会尽量将正则这玩意说明白, 使得你看完这边文章后对正则基本可以运用自如.
温馨提示, 这篇文章会比较长, 大致浏览即可. 正确的方式是收藏起来, 等到使用正则的时候翻看
语法介绍
在平常进行字符串匹配的时候如何做呢? 比如希望从字符串Hello Word
中匹配到第一个单词, 我们就会拿着Hello
子串进行匹配.
正则表达式的表示与其相同, 区别是在子串匹配时每个字符都会进行原样匹配, 而正则表达式中会存在一些特殊符号, 这些符号会代表一些特殊的含义, 如a+
的意思是匹配任意多个连续的a
字符, 是不是十分简单?
如此说来, 要使用正则表达式, 关键点就在于了解其中的特殊字符上.
分类 | 字符 | 含义 |
---|---|---|
单字符 | . | 任意字符(换行符除外) |
\d | 任意数字 | |
\D | 非数字 | |
\w | 字母数字下划线 | |
\W | 非字母数字下划线 | |
$ | 字符串结束位置 | |
^ | 字符串开始位置 | |
空白符 | \s | 任意空白字符 |
\S | 非空白字符(包括空格) | |
\r | 回车 | |
\n | 换行 | |
\f | 换页 | |
\t | 制表符 | |
\v | 垂直制表符 | |
量词 | {m} | 前面内容出现 m 次 |
{m,} | >=m次 | |
{m,n} | m-n 次 | |
* | 同 {0,} | |
+ | 同 {1,} | |
? | 同 {0, 1} | |
范围选择 | | | 或. eg: ab|cd msg: ab 或 cd |
[…] | 单字符多选一. eg: [abcd] msg: a或b或c或d |
|
[a-c] | ASCII 表范围. eg: [a-z] msg: 所有小写字母 若其中的 - 是需要匹配的单字符, 需使用\- 进行转义 |
|
[^…] | 取反. [] 中标识的字符外的任意字符 |
|
以上所有均可以任意嵌套使用, 如:
https?|ftp
: 可匹配http https ftp
高级用法
分组
有这样一个正则表达式 ab{3}
, 它会匹配字符串 abbb
. 如果我们想要匹配字符串 ababab
, 如何在正则表达式中指定令ab
重复3次呢?
用程序员的通俗思维想一下, 没错, 加括号. (ab){3}
的意思就是匹配字符串 ababab
. 而这, 就是分组.
有的小朋友会问题了, 这不就是指定下优先级嘛, 和分组有什么关系? 为什么叫分组呢? 别急, 往下看
在正则表达式中, 分组有如下作用:
- 在表达式后面可以进行引用
- 在匹配结果中, 会将匹配的分组同时提取出来
正则引用分组
在正则表达式中, 可以通过 \1
来引用分组. 其中的数字是分组的编号, 从1开始. 从左往右依次递增.
比如正则表达式 (ab)(cd)\2\1
会匹配字符串 abcdcdab
同时会在结果中将2个分组提取出来.
这里注意, 在有些编程语言的实现上, 通过$
符号引用分组(比如 js), 用的时候再搜就行.
不引用分组
有的时候我们只是希望将多个字符合并, 并不需要引用分组. 这时可以通过 (?:...)
来指定不需要引用的分组.
嵌套分组
对于比较复杂的场景, 会存在括号嵌套括号的情况, 此时分组编号是全局的. 简单说, 左括号是第几个, 分组编号就是几.
比如正则表达式 (a(bc)d)\2\1
会匹配字符串 abcdbcabcd
分组应用场景
简单介绍几种可能预见的应用场景:
匹配重复单词
比如正则表达式: (\w+) \1
文本替换
比如这段python
代码:
import re
test_str = 'hello, hujingnb is good, haha'
pattern = r'(\w+) is good'
repl = r'\1 is bad'
# hello, hujingnb is bad, haha
print(re.sub(pattern, repl, test_str))
比如常用的sublime
工具中, 也可通过类似操作进行文本替换.
匹配模式
在匹配的时候, 可以通过设置(?i)
来修改匹配模式为忽略大小写, 使用方式如下:
(?i)hello
: 放在正则表达式最前面, 整个正则表达式均为忽略大小写模式h(?i)hello
: 放在正则表达式中间, 即从某处开始, 改为忽略大小写模式. 注意, 不是所有语言均可用((?i)hello) \1
: 放在分组开头, 标识分某个分组改为忽略大小写模式. 注意, 不是所有语言均可用
可以调整的匹配模式如下, 匹配模式格式均为(?<model>)
, 使用方法相同, 多个模式可以放在一起使用, 如 (?is)
:
a
: 测试 仅匹配 ASCII 字符, unicode 编码字符不进行匹配i
: 测试 忽略大小写m
: 测试 多行模式. 修改^$
的行为, 改为匹配每一行的开头结尾n
: 测试 开启后,(...)
这种普通分组不会做为分组存在, 仅(?<name>...)
这种命名分组会进行捕获s
: 测试.
可以匹配任意符号, 包括换行符u
: 测试 匹配完整的 unicode 编码, 默认行为, 基本不需要设置U
: 测试 懒惰模式. 开启懒惰模式, 在此模式下, 量词后面加?
为恢复贪婪模式x
: 测试 详细模式. 将正则表达式中的所有空格及换行均忽略, 且每行#
后为注释内容. 匹配规则中的空格可使用\
转义 (或者放到分组中使用, 也可以通过[ ]
使用)
注意: 大部分语言都可以直接在正则表达式中修改匹配模式, 但部分语言不行, 比如:
js
:/<正则>/<model>
边界匹配
在正则使用中, 可能会想要进行位置匹配, 但并不希望匹配内容出现在结果中. 于是就出现了这样一组符号, 仅用于匹配位置, 比如前面出现过的 ^
和 $
用于匹配边界的符号有如下几种:
符号 | demo | 含义 |
---|---|---|
^ |
匹配 不匹配 | 匹配字符串的开始位置 |
$ |
匹配 不匹配 | 匹配字符串的结束位置 |
\b |
匹配 | 匹配单词边界, 边界包括 空格.- 等等符号, 注意_ 不是单词边界 |
\B |
匹配 不匹配 | 匹配非单词边界, 与\b 相反 |
\A |
匹配 不匹配 ^ 差异 |
匹配字符串的开始位置, 与^ 相似. 但多行模式下, ^ 的行为会改为匹配行开始位置, \A 行为不会改变 |
\Z |
匹配 $ 差异 |
匹配字符串的结束位置, 与$ 相似. 同样, 在多行模式下, 行为不会改变 |
(?=...) |
匹配 不匹配 | 前向肯定断言. 简单说, 右边匹配 ... |
(?!...) |
匹配 不匹配 | 前向否定断言. 简单说, 右边不匹配 ... |
(?<=...) |
匹配 不匹配 | 后向肯定断言. 简单说, 左边匹配 ... |
(?<!...) |
匹配 不匹配 | 后向否定断言. 简单说, 左边不匹配 ... |
扩展语法
所有扩展语法格式均为为(?...)
. (反过来不成立)
注意, 扩展语法并不是所有编程语言都支持的, 在使用前可前往网站测试是否支持.
命名分组
使用编号引用分组的方式并不友好, 甚至有时候改了下正则表达式, 后面编号都要改一遍. 因此, 我们可以给分组起个名.
(?P<xxx>...)
: 给分组起名为xxx
(?P=xxx)
: 引用xxx
分组
比如正则表达式 (?P<reg_name>ab)(?P=reg_name)
会匹配字符串 abab
注意 命名分组也会占用分组的编号哦, 也就是说 (?P<reg_name>ab)(?P=reg_name)
和 (?P<reg_name>ab)\1
效果是一样的.
分支判断
根据前面是否匹配到分组信息, 来使得后面能够有不同的匹配行为.
语法为: (?(<group_id>/<group_name>)<yes-pattern>|<no-pattern>)
, 前面指定分组编号或者名字, 如果分组存在, 则使用 <yes-patterm>
进行匹配, 否则使用 <no-pattern>
进行匹配.
比如这个例子, ^(<)?(\w+)(?(1)>|$)$
, 可以匹配到字符串 <aaa>
和 aaa
, 也就是<
开头的必须由 >
结尾.
注释
语法 (?#这部分是注释)
匹配规则
下面介绍下几种匹配规则:
- 贪婪模式: 尽可能多的匹配. 是正则匹配时的默认模式
- 懒惰模式: 尽可能少的匹配. 通过在量词后添加
?
指定. 如a*?
就是a*
的懒惰版本 - 独占模式: 在匹配过程中不进行回溯. 通过在量词后添加
+
指定. 如a++
就是a+
的独占版本
简单对着几种规则进行说明
贪婪模式
贪婪模式其实就是我们平常最进场使用的模式. 其原则是向后查找尽量长的字符串进行匹配.
比如使用正则b*
来匹配字符串abbbc
, 能够匹配到如下内容:
位置(前闭后开) | 匹配内容 |
---|---|
0-0 | 空 |
1-4 | bbb |
4-4 | 空 |
5-5 | 空 |
匹配过程大致如下:
0-0
: 匹配第一个字符, 发现不是 a, 输出空1-4
: 一直匹配到字母c
发现匹配不上了, 输出bbb
- 剩下的同理
懒惰模式
匹配过程与贪婪模式
相似, 区别是只要有能够匹配到的, 就输出, 会输出符合规则的最短子串.
还用上面相同的例子举例, 使用正则b*?
匹配字符串abbbc
, 能够匹配到如下内容:
位置(前闭后开) | 匹配内容 |
---|---|
0-0 | 空 |
1-1 | 空 |
1-2 | b |
2-2 | 空 |
2-3 | b |
3-3 | 空 |
3-4 | b |
4-4 | 空 |
5-5 | 空 |
与上面的一对比, 区别是不是就很明显了? 这次匹配到的是没单个b
字符, 就连每个b
字符左面的空字符都匹配上了.
匹配过程就不再赘述了. 这里用一个更加明显且常用的场景来说明贪婪模式
与懒惰模式
的区别, 当我们匹配字符串"hi mom" and "hi son"
时:
独占模式
要说明独占模式, 得先简单说一下正则匹配的回溯现象.
比如, 使用正则 ab+bc
匹配字符串 abbbc
的时候, 在贪婪模式下(下方的括号为了标明当前匹配的位置):
正则匹配位置 | 字符串匹配位置 | 说明 |
---|---|---|
(a)b+bc | (a)bbbc | 字符 a 完成匹配, 继续下一个匹配 |
a(b+)bc | a(b)bbc | 此时, 要匹配字符 b 的数量为 >= 1, 因此会继续向后匹配 正则位置不变, 字符串匹配后移(后续相同操作忽略) |
a(b+)bc | abbb(c) | 当匹配到这个位置的时候, 发现已经匹配不上了. 则字符串会向前移动, 以继续完成匹配. |
ab+(b)c | abb(b)c | 此时, 字符串将已经匹配过的字符又吐出来了. 这个过程就被称为回溯 |
… | … | 后续完成匹配, 不再赘述 |
如果正则是ab+bbc
的话, 匹配相同的字符串甚至会发生多次回溯. (懒惰模式也存在回溯现象, 不再赘述)
而回溯现象是及其影响性能的. 而独占模式则是将回溯直接关掉, 匹配性能更好, 但是对正则书写的要求也更高些.
比如匹配abbbc
字符串时, 开启独占模式的匹配规则ab++bc
会匹配不到任何数据.
而ab++c
则是能够匹配到字符串abbbc
的, 因为匹配过程无需回溯,
这里说句题外话, 在有些编程语言中不支持回溯模式, 比如Go
, Python
中使用也需要通过regex
库.
回溯现象真实场景
你以为回溯现象造成的性能微乎其微么? 不, 有这样一篇文章一个由正则表达式引发的血案, 讲述了因为正则回溯而导致的 CPU 爆满, 感兴趣的去原文看一下.
简单来说, 就是正则表达式 ^([a-z]|[a-z])+$
在进行匹配的时候,因为a-z
在前后组合中重复出现了, 导致大量回溯后的重复判断 . 如果所有的字符都能够成功匹配到前一个组合, 问题还不大, 但一旦有一个字符不匹配就会发生回溯, 且不匹配的字符越靠后, 回溯的层级越深. 查看回溯匹配
对于这种情况如何修改呢?
- 将重复匹配去掉, 改为
^[a-z]+$
- 使用独占模式防止回溯.
^([a-z]|[a-z])++$
回溯的介绍, 也可参考此文章