正则表达式使用文档

通过网站 https://regex101.com/ 可以测试正则表达式的匹配结果及匹配过程.

本文章抛开各个编程语言实现差异, 仅做正则本身的介绍, 会尽量将正则这玩意说明白, 使得你看完这边文章后对正则基本可以运用自如.

温馨提示, 这篇文章会比较长, 大致浏览即可. 正确的方式是收藏起来, 等到使用正则的时候翻看

语法介绍

在平常进行字符串匹配的时候如何做呢? 比如希望从字符串Hello Word中匹配到第一个单词, 我们就会拿着Hello子串进行匹配.

正则表达式的表示与其相同, 区别是在子串匹配时每个字符都会进行原样匹配, 而正则表达式中会存在一些特殊符号, 这些符号会代表一些特殊的含义, 如a+的意思是匹配任意多个连续的a字符, 是不是十分简单?

如此说来, 要使用正则表达式, 关键点就在于了解其中的特殊字符上.

分类 字符 含义
单字符 . 任意字符(换行符除外)
\d 任意数字
\D 非数字
\w 字母数字下划线
\W 非字母数字下划线
$ 字符串结束位置
^ 字符串开始位置
空白符 \s 任意空白字符
\S 非空白字符(包括空格)
\r 回车
\n 换行
\f 换页
\t 制表符
\v 垂直制表符
量词 前面内容出现 m 次
>=m次
m-n 次
*
+
?
范围选择 | 或. eg: `ab
[...] 单字符多选一. 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. 在表达式后面可以进行引用
  2. 在匹配结果中, 会将匹配的分组同时提取出来

正则引用分组

在正则表达式中, 可以通过 \1 来引用分组. 其中的数字是分组的编号, 从1开始. 从左往右依次递增.

比如正则表达式 (ab)(cd)\2\1 会匹配字符串 abcdcdab 同时会在结果中将2个分组提取出来.

image-20230910204040697

这里注意, 在有些编程语言的实现上, 通过$符号引用分组(比如 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 含义
^ 匹配 不匹配 匹配字符串的开始位置
` 符号 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

匹配过程大致如下:

  1. 0-0: 匹配第一个字符, 发现不是 a, 输出空
  2. 1-4: 一直匹配到字母c发现匹配不上了, 输出bbb
  3. 剩下的同理

懒惰模式

匹配过程与贪婪模式相似, 区别是只要有能够匹配到的, 就输出, 会输出符合规则的最短子串.

还用上面相同的例子举例, 使用正则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"时:

  • ".*" : 贪婪模式下, 匹配到的是字符串"hi mom" and "hi son"
  • ".*?": 懒惰模式下, 则会分别匹配到字符串"hi mom""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在前后组合中重复出现了, 导致大量回溯后的重复判断 . 如果所有的字符都能够成功匹配到前一个组合, 问题还不大, 但一旦有一个字符不匹配就会发生回溯, 且不匹配的字符越靠后, 回溯的层级越深. 查看回溯匹配

对于这种情况如何修改呢?

  1. 将重复匹配去掉, 改为 ^[a-z]+$
  2. 使用独占模式防止回溯. ^([a-z]|[a-z])++$

回溯的介绍, 也可参考此文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章