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 次 | 0 | 3 | 到第一个字母 b 发现不满足,输出 aaa | aaa |
第 2 次 | 3 | 4 | 匹配剩下的 bb 发现不满足,输出空字符串 | 空字符串 |
第 3 次 | 4 | 4 | 匹配剩下的 b 发现不满足,输出空字符串 | 空字符串 |
第 4 次 | 5 | 5 | 匹配剩下的空字符串,输出空字符串 | 空字符串 |
a*
在匹配开头的 a 时,会尝试尽量匹配更多的 a,直到第一个字母 b 不满足要求为止,匹配上三个 a,后面每次匹配时都得到了空字符串。
如果想尽可能最短匹配,那就要用到非贪婪匹配模式了。
非贪婪匹配(Lazy)
非贪婪模式只需要在量词后面加上 ?
,比如 a*?
,在用这个正则去匹配 aaabb,得到的结果就是 ["", "a", "", "a", "", "a", "", "", ""]
。
这次匹配到的结果都是单个的 a,就连每个 a 左边的空字符串也匹配上了。
独占模式(Possessive)
不管是贪婪模式,还是非贪婪模式,都需要发生回溯才能完成相应的功能。但是在一些场景下,我们不需要回溯,匹配不上返回失败就好了,因此正则中还有另外一种模式,独占模式,它类似贪婪匹配,但匹配过程不会发生回溯,因此在一些场合下性能会更好。
什么是回溯?
用正则 xy{1,3}z
匹配文本 xyyz
。y{1,3}
会尽可能长地去匹配,当匹配完 xyy 后,由于 y 要尽可能匹配最长,即三个,但字符串中后面是个 z 就会导致匹配不上,这时候正则就会向前回溯,吐出当前字符 z,接着用正则中的 z 去匹配。
把这个正则改成非贪婪模式 xy{1,3}?z
,y{1,3}?
尽可能少地匹配。匹配上一个 y 之后,文本中的 xy 后,正则会使用 xy 后面的 y 和正则中的 z 比较,发现正则 z 和 y 不匹配,这时正则就会向前回溯,重新查看 y 匹配两个的情况,匹配上正则中的 xyy,然后再用 z 去文本 中的 z,匹配成功。
独占模式就是在量词后面加上 +
。
把正则改成独占模式 xy{1,3}+z
,y{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) | 肯定顺序环视 | 右边是 Y | th(?=\d) th 的右边是数字,可以匹配文本 th9 |
(?!Y) | 否定顺序环视 | 右边不是 Y | th(?!\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('\(\)\[\]\{\}', '()[]{}') # 方括号和花括号都转义也可以
['()[]{}']
在正则中,圆括号通常用于分组,或者将某个部分看成一个整体,如果只转义开括号或闭括号,正则会认为少了另外一半,所以会报错。
字符组中的转义
字符组中需要转义的有三种情况:
- 脱字符在中括号中,且在第一个位置需要转义:
>>> import re
>>> re.findall(r'[^ab]', '^ab') # 转义前代表"非"
['^']
>>> re.findall(r'[\^ab]', '^ab') # 转义后代表普通字符
['^', 'a', 'b']
- 中划线在中括号中,且不在首尾位置:
>>> 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', '-']
- 右括号在中括号中,且不在首位:
>>> 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']