目录

Regex

正则表达式的三种功能:校验数据、查找文本、对文本进行切割和替换等操作。

元字符

元字符就是指那些在正则表达式中具有特殊意义的专用字符,正则就是由一系列的元字符组成的。

特殊单字符

字符说明
.任意字符(除换行以外)
\d任意数字
\D任意非数字
\w任意字母,数字,下划线
\W任意非字母,数字,下划线的字符
\s任意空白符
\S任意非空白符

空白符

空白符包括空格,换行符 \n, TAB 制表符 \t 等。不同的操作系统换行符也有区别,例如 Windows 中换行是 \r\n,Linux 和 MaxOS 中是 \n

字符说明
\r回车符
\n换行符
\f换页符
\t制表符
\v垂直制表符
\s任意空白符

量词

量词的元字符用来表示字符出现的次数。

字符说明
*0 到多次,等价于 {0,}
+1 到多次,等价于 {1,}
?0 到 1 次,等价于 {0,1}
{m}m 次
{m,}至少 m 次
{m,n}m 到 n 次

范围

使用量词去匹配手机号 \d{11},但是范围比较大,会匹配到非手机号的数字,例如 11 个 0,所以需要在一定范围里找到符合要求的数字。

字符说明
[...]多选一,括号中的任意单个字符
[a-z]a 到 z 之间的任意单个字符
[^...]取反,不能是括号中的任意单个字符

|,例如 ab|bc 表示 ab 或者 bc

贪婪与非贪婪模式

使用正则 a+ 在 aaabb 中查找,只有一个输出结果 aaa。但是使用 a* 的话会有 4 个匹配结果,分别是 ["aaa", "", "", ""]。 为什么会匹配到空字符串?因为 * 表示 0 到多次,匹配 0 次就是空字符串。aaa 部分应该也有空字符串,为什么没匹配上?

贪婪匹配(Greedy)

在正则中,表示次数的量词默认是贪婪的,在贪婪模式下,会尝试尽可能最大长度去匹配

aaabb 中使用正则 a* 的匹配过程:

匹配次数开始结束说明匹配结果
第 1 次03到第一个字母 b 发现不满足,输出 aaaaaa
第 2 次34匹配剩下的 bb 发现不满足,输出空字符串空字符串
第 3 次44匹配剩下的 b 发现不满足,输出空字符串空字符串
第 4 次55匹配剩下的空字符串,输出空字符串空字符串

a* 在匹配开头的 a 时,会尝试尽量匹配更多的 a,直到第一个字母 b 不满足要求为止,匹配上三个 a,后面每次匹配时都得到了空字符串。

如果想尽可能最短匹配,那就要用到非贪婪匹配模式了。

非贪婪匹配(Lazy)

非贪婪模式只需要在量词后面加上 ?,比如 a*?,在用这个正则去匹配 aaabb,得到的结果就是 ["", "a", "", "a", "", "a", "", "", ""]。 这次匹配到的结果都是单个的 a,就连每个 a 左边的空字符串也匹配上了。

独占模式(Possessive)

不管是贪婪模式,还是非贪婪模式,都需要发生回溯才能完成相应的功能。但是在一些场景下,我们不需要回溯,匹配不上返回失败就好了,因此正则中还有另外一种模式,独占模式,它类似贪婪匹配,但匹配过程不会发生回溯,因此在一些场合下性能会更好。

什么是回溯?

用正则 xy{1,3}z 匹配文本 xyyzy{1,3} 会尽可能长地去匹配,当匹配完 xyy 后,由于 y 要尽可能匹配最长,即三个,但字符串中后面是个 z 就会导致匹配不上,这时候正则就会向前回溯,吐出当前字符 z,接着用正则中的 z 去匹配。

把这个正则改成非贪婪模式 xy{1,3}?zy{1,3}? 尽可能少地匹配。匹配上一个 y 之后,文本中的 xy 后,正则会使用 xy 后面的 y 和正则中的 z 比较,发现正则 z 和 y 不匹配,这时正则就会向前回溯,重新查看 y 匹配两个的情况,匹配上正则中的 xyy,然后再用 z 去文本 中的 z,匹配成功。

独占模式就是在量词后面加上 +

把正则改成独占模式 xy{1,3}+zy{1,3}+ 尽可能长的匹配了两个 y,不回溯导致正则的 z 和前面的 y 匹配不上。

Go 不支持独占模式。

分组与编号

() 在正则中可以用于分组,被括号括起来的部分“子表达式”会被保存成一个子组。分组和编号的规则,用一句话来说就是,第几个括号就是第几个分组

假设时间格式是 2020-05-10 20:23:05,使用正则 ((\d{4})-(\d{2})-(\d{2})) ((\d{2}):(\d{2}):(\d{2})) 来匹配。

  2020  -   05  -   10       20   :   23  :  05
((\d{4})-(\d{2})-(\d{2})) ((\d{2}):(\d{2}):(\d{2}))
12       3       4        56       7       8        // 分组编号对应着左括号的位置  

分组引用

知道了分组的编号可以通过 \<number> 的方式来引用,如 \2。在 JavaScript 中是通过 $<number> 来引用,如 $2。Go 不支持。

不保存子组

默认情况下,在括号里面的会保存成子组,如果不需要保存子组可以在括号里面使用 ?:,如 \d{15}(?:\d{3})?

不保存子组可以理解成,括号只用于归组,把某个部分当成“单个元素”,不分配编号,后面不会再进行这部分的引用。可以提高正则的性能。

4 种匹配模式

不区分大小写

不区分大小写的模式修饰符是 (?i),使用时放在整个正则前面时,就表示整个正则表达式都是不区分大小写的,如 (?i)cat,等价于 [Cc][Aa][Tt]

点号通配模式

. 可以匹配上任何符号,除了换行。要匹配真正的“任意”符号的时候,可以使用 [\s\S][\d\D][\w\W] 等。也可以使用点号通配模式。有很多地方把它称作单行匹配模式。但这么说容易造成误解,毕竟它与多行匹配模式没有关系。

点号通配模式修饰符是 (?s),如 (?s).+

JavaScript 不支持,可以使用 [\s\S] 等方式替代。

多行匹配模式

通常情况下,^ 匹配整个字符串的开头,$ 匹配整个字符串的结尾。多行匹配模式改变的就是 ^$ 的匹配行为。

例如 ^the|cat$ 匹配下面的文本:

the little cat
the small cat

只能匹配到第一个 the 和最后一个 cat

多行模式的作用在于,使 ^$ 能匹配上每行的开头或结尾,我们可以使用模式修饰符号 (?m) 来指定,如 (?m)^the|cat$

正则中还有 \A\z(Python 中是 \Z) 这两个元字符容易混淆,\A 仅匹配整个字符串的开始,\z 仅匹配整个字符串的结束,在多行匹配模式下,它们 的匹配行为不会改变,如果只想匹配整个字符串,而不是匹配每一行,用这个更严谨一些。

注释模式

正则中支持添加注释,修饰符 (?#),如 (\w+)(?#word) \1(?#word repeat again)

断言

断言是指对匹配到的文本位置有要求。 例如 \d{11} 能匹配上 11 位数字,但这 11 位数字可能是 18 位身份证号中的一部分。查找 tom 这个单词,但其它的单词,比如 tomorrow 中也包含了tom。

正则中提供了一些结构,只用于匹配位置,而不是文本内容本身,这种结构就是断言。

单词边界

例如 tom asked me if I would go fishing with him tomorrow.,要把 tom 换成 jim,文本中除了 tom,tomorrow 也是以 tom 开头的。

单词的组成一般可以用元字符 \w+ 来表示,只要找出单词的边界,也就是当出现了 \w 表示的范围以外的字符,比如引号、空格、标点、换行等这些符号,我们就可以在正则中使用 \b (Boundary)来表示单词的边界。

正则应该是 \btom\b

行的开始或结束

如果要求匹配的内容要出现在一行文本开头或结尾,就可以使用 ^$ 来进行位置界定。在多行模式下,^$ 符号可以匹配每一行的开头或结尾。更严谨的做法是,使用 \A\z 来匹配整个文本的开头或结尾。

环视

邮政编码的规则是由 6 位数字组成。现在要求提取文本中的邮政编码。根据规则,很容易就可以写出邮编的组成 \d{6}。但是 7 位数的前 6 位也能匹配上,12 位数可以匹配上两次。

也就是说,除了文本本身组成符合这 6 位数的规则外,这 6 位数左边或右边都不能是数字。正则是通过环视来解决这个问题的。

正则名称含义示例
(?<=Y)肯定逆序环视左边是 Y(?<=\d)th th 的左边是数字,可以匹配文本 9th
(?<!Y)否定逆序环视左边不是 Y(?<!\d)th th 的左边不是数字,可以匹配文本 health
(?=Y)肯定顺序环视右边是 Yth(?=\d) th 的右边是数字,可以匹配文本 th9
(?!Y)否定顺序环视右边不是 Yth(?!\d) th 的右边不是数字,可以匹配文本 the

左尖括号代表看左边,没有尖括号是看右边,感叹号是非的意思

针对邮编的正则可以改成 (?<!\d)\d{6}(?!\d)

表示单词边界的 \b 也可以用环视的方式来写。例如 the little cat is in the hat,单词可以用 \w+ 来表示,单词的边界其实就是那些不能组成单词的字符,即左边和右边都不能是组成单词的字符。

(?<!\w) 表示左边不能是单词组成字符,(?!\w) 右边不能是单词组成字符,即 \b\w+\b 也可以写成 (?<!\w)\w+(?!\w)。可以写成 (?<=\W)\w+(?=\W)

转义

转义在工作中是比较常见的,如 str = "How do you spell the word \"regex\"?",但是正则中什么时候需要转义,什么时候不用转义?

转义字符

转义字符自身和后面的字符看成一个整体,用来表示某种含义。最常见的例子是,C 语言中用反斜线字符 \ 作为转义字符,来表示那些不可打印的 ASCII 控制符。另外,在 URI 协议中,请求串中的一些符号有特殊含义,也需要转义,转义字符用的是百分号 %

之所以称为转义字符,是因为它后面的字符,不是原来的意思了。例如文件名中有 * 号,我们就需要转义:

rm access_log*    # 删除当前目录下 access_log 开头的文件
rm access_log\*   # 删除当前目录下名字叫 access_log* 的文件

字符串转义和正则转义

正则中也是使用 \ 进行转义的。

一般来说,正则中 \d 代表的是单个数字,但如果我们想表示成 反斜杠和字母 d,这时候就需要进行转义,写成 \\d

在程序中表示普通字符串的时候,我们如果要表示反斜杠,通常需要写成两个反斜杠,因为只写一个会被理解成“转义符号”,而不是反斜杠本身。

在程序使用过程中,从输入的字符串到正则表达式,其实有两步转换过程,分别是字符串转义正则转义

在正则中正确表示“反斜杠”具体的过程是这样子:我们输入的字符串,四个\\\\,经过第一步字符串转义,它代表的含义是两个 \\;这两个反斜杠再经过第二步正则转义,它就可以代表单个 \ 了。

元字符的转义

如果现在要查找比如*+? 本身,而不是元字符的功能,这时候就需要对其进行转义,直接在前面加上反斜杠就可以了。

正则中方括号 [] 和 花括号 {} 只需转义开括号,但圆括号 () 两个都要转义。

>>> import re
>>> re.findall('\(\)\[]\{}', '()[]{}')
['()[]{}']
>>> re.findall('\(\)\[\]\{\}', '()[]{}')  # 方括号和花括号都转义也可以
['()[]{}']

在正则中,圆括号通常用于分组,或者将某个部分看成一个整体,如果只转义开括号或闭括号,正则会认为少了另外一半,所以会报错。

字符组中的转义

字符组中需要转义的有三种情况:

  1. 脱字符在中括号中,且在第一个位置需要转义:
>>> import re
>>> re.findall(r'[^ab]', '^ab')  # 转义前代表"非"
['^']
>>> re.findall(r'[\^ab]', '^ab')  # 转义后代表普通字符
['^', 'a', 'b']
  1. 中划线在中括号中,且不在首尾位置:
 >>> import re
 >>> re.findall(r'[a-c]', 'abc-')  # 中划线在中间,代表"范围"
 ['a', 'b', 'c']
 >>> re.findall(r'[a\-c]', 'abc-')  # 中划线在中间,转义后的
 ['a', 'c', '-']
 >>> re.findall(r'[-ac]', 'abc-')  # 在开头,不需要转义
 ['a', 'c', '-']
 >>> re.findall(r'[ac-]', 'abc-')  # 在结尾,不需要转义
 ['a', 'c', '-']
  1. 右括号在中括号中,且不在首位:
 >>> import re
 >>> re.findall(r'[]ab]', ']ab')  # 右括号不转义,在首位
 [']', 'a', 'b']
 >>> re.findall(r'[a]b]', ']ab')  # 右括号不转义,不在首位
 []  # 匹配不上,因为含义是 a后面跟上b]
 >>> re.findall(r'[a\]b]', ']ab')  # 转义后代表普通字符
 [']', 'a', 'b']

参考链接