通过网站 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])++$
回溯的介绍, 也可参考此文章