前文链接:正则表达式系列 一
本文介绍的是正则的各种性质,有阅读难度,建议先阅读前文
括号
这里讲的括号特指小括号。
分组作用
最直白的含义,即将一部分元素括起来视为一个整体。
#python
re.search(r"^ab+$","abab") != None #False,即无法匹配
re.search(r"^(ab)+$","abab") != None #True,可以匹配
ab+的含义是匹配一个字符a开头,后面跟1个或多个字符b的字符串。所以不能匹配(注意用^和$限制匹配范围)
(ab)+的含义是匹配一个或多个字符串“ab”,所以能匹配
多选分支
这个能力需要借助元字符“|”,表示“或”的含义
#python
regex = r"^([a-zA-Z]+|[0-9]+)$"
re.search(regex, "19987234") != None #True
re.search(regex, "cajsdioc") != None #True
re.search(regex, "89dsa") != None #False
regex
中含有两个字符组,[a-zA-Z]+
匹配一个纯字母的字符串,[0-9]+
匹配一个纯数字的字符串。二者用或相连。所以既能匹配纯数字串,也能匹配纯字母串,但无法匹配混杂字母和数字的字符串
多选分支还有一些注意事项
-
多选分支也可以没有括号。因为元字符“|”的运算优先级非常低。
ab|cd和(ab|cd)
的效果完全相同 -
能用字符组实现的功能,不要用多选分支,因为相同情况下多选分支效率差得多
-
多选分支的排列顺序,大部分语言是优先匹配靠左的分支条件。所以如果写出的两个分支能匹配同一个字符串,最后匹配结果是靠左的分支。
#python re.search(r"(ljj|ljjliujunjie)", "ljjliujunjie123").group() #输出 ljj re.search(r"(ljjliujunjie|ljj)", "ljjliujunjie123").group() #输出 ljjliujunjie
所以如果没有必要,不要写出会重复匹配的分支。但如果必须要重复,那就把最难触发的分支靠前写。
引用分组
举个例子先,如果你希望从2022-10-11这种日期中截取年月日,那很自然的写出正则regex1 = r"\d{4}-\d{2}-\d{2}"
,但这个虽然能匹配,但无法截取年月日。所以正确的做法是下面这种
#python
regex2 = r"(\d{4})-(\d{2})-(\d{2})"
res = re.search(regex2, "2022-10-11")
res.group(0) #2022-10-11
res.group(1) #2022
res.group(2) #10
res.group(3) #11
默认情况下,括号中的匹配内容会被保存下来,从1开始编号形成一个列表返回。至于为啥从1开始编号,是因为如果没有加任何括号,整体的返回结果会被默认为0。如果括号存在嵌套,分组的编号是以开括号出现的顺序来计数。
还有一个需要注意的特点,所谓引用分组,引用的是对应括号的表达式最终匹配到的文本,或者叫捕获到的文本。比如下面这个错误
#python
res.search(r"(\d){4}-(\d{2})-(\d{2})", "2022-10-11").group(1)
#返回 2
这里把第一个括号的限定范围缩小了,(\d){4}
这个整体的表示意思仍然是匹配一个长度为4的连续数字串,但括号内的表达式是 \d
,它的意思是匹配一个长度为1的数字串。所以匹配时,先匹配到2022的第一个字符2,此时1号括号的捕获结果是2,再匹配到2022的第二个字符0,此时1号括号的捕获结果就被更新为0。依次进行,最终1号括号的捕获结果是2022的最后一个字符2。
引用分组不仅可以用于获取特定匹配结果,还可以替换指定内容,只需要更改下调用的正则api
即可,建议查询对应语言文档
引用分组还有些特殊用法
反向引用
如果想判断一个单词是否存在连续出现的重复字,比如pool,book,hello等等,我们不能用[a-z][a-z]
这种方式来匹配,因为两个字符组是各自匹配互不干扰的,不能保证匹配结果一定相同。
反向引用则提供一种能力,允许正则表达式的内部成员,获取到靠前部分的表达式的匹配结果。
所以只需要用([a-z])\1
,后面的\1
表示引用编号为1的括号的匹配内容。同理,如果多个括号,就依次\2,\3等等
不过切记,引用的是文本而不是表达式,不要试图用反向引用实现“重复某个正则表达式”。比如
#python
#下面两个正则表达式都试图匹配 3.1.2.0 这种字符串
regex1 = r"([0-9]\.){3}[0-9]" #正确
regex2 = r"([0-9])\.\1\.\1\.\1" #错误
第二个之所以错误,是因为\1引用的是([0-9])的匹配结果,比如匹配3.1.2.0时,第一个小括号的匹配结果是3,那么regex2
此时就等价于"3\.3\.3\.3"
,所以只能匹配3.3.3.3。
命名分组
引用分组的引用默认是数字编号,如果括号较多便很不直观,所以可以用下面语法给括号命名
#python
regex1 = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
regex2 = r"(\d{4})-(\d{2})-(\d{2})"
re.search(regex1, "2022-10-11").group("year") #2022
re.search(regex2, "2022-10-11").group(1) #2022
#即便命名了,也可以用数字编号
re.search(regex1, "2022-10-11").group(1) #2022
上述两种正则表达式是完全等价的。另外注意,上述代码的语法是python规定的,其余语言不尽相同
非捕获分组
括号的引用能力是默认开启的,虽然很方便,但是性能很低。如果你仅仅需要一个括号的分组能力,可以使用如下语法关闭这个括号的引用能力,这种语法叫作非捕获分组。引用编号时会跳过非捕获分组
#python
re.search(r"(?:\d{4})-(\d{2})-{\d{2}}", "2022-10-11").group(1)
#10
虽然这个语法看起来怪,但其实大部分情况我们不需要引用能力,只是简单分组,所以尽量使用非捕获分组来替代默认
断言
正则表达式中有些结构,虽然也能匹配到内容,但这种匹配只是用于分支判断,匹配的内容也不会出现在结果中,这样的结构被称为断言。
单词边界
假设需要下面这段文本中的单词“ljj”
,很自然会写出正则 regex = r"ljj"
。但很不幸会发现有两处匹配,"ljjliujunjie123@some.com"
的前缀ljj
也被视为了一个单词
text = "ljj has a email which is ljjliujunjie123@some.com"
说明单词ljj 并不等于 字符串ljj
。所以正则提供了特殊元字符 \b
,表示当前位置,一侧是单词字符,一侧不是单词字符。这样的位置自然就是一个单词的边界。所以改写一下,regex = r"\bljj\b"
就能正确匹配了
有一点需要注意,单词边界\b要求中,“一侧不是单词字符”,含义是既可以出现非单词字符,也可以没有任何字符
行起始or结束位置
类似单词边界的匹配,我们匹配位置时,匹配到的是两个字符之间的地方,这种匹配又被叫作锚点。
正则中关于行的起始与结束提供了如下锚点元字符
^ 匹配整个字符串/文本的起始位置,和行终止符之后的位置
$ 匹配行终止符之前的位置(但在JS语言中,$匹配的是行终止符之后的那个位置)
\A 不论在任何模式下,永远匹配整个字符串/文本的起始位置
\z 不论在任何模式下,永远匹配整个字符串/文本的结束位置(即行终止符之后的位置)
其中前两个是所有语言都支持的,也是最常用的,后两个在不同语言下语言不尽相同,使用前查文档
环视
顾名思义:站在某个位置,环顾四周的情况。正则的环视就是站在当前位置,考察其前面的字符串或后面的字符串是否满足特定要求。其语法分为如下四种
名字 | 语法 | 方向 | 含义 |
---|---|---|---|
肯定顺序环视 | (?=regex) |
向右匹配 | 匹配到当前位置时,其右侧的子表达式必须满足regex
|
否定顺序环视 | (?!regex) |
向右匹配 | 匹配到当前位置时,其右侧的子表达式必须不能满足regex
|
肯定逆序环视 | (?<=regex) |
向左匹配 | 匹配到当前位置时,其左侧的子表达式必须满足regex
|
否定逆序环视 | (?<~!regex) |
向左匹配 | 匹配到当前位置时,其左侧的子表达式必须不能满足regex
|
举个例子,如果我想从下面的字符串中匹配这样的子字符串,其左侧必须是#,其右侧不能是?,其本身必须是由小写字母构成的字符串。
#python
text = "ljj want #to be? a# ?#better? coder"
regex = r"(?<=#)[a-z]+(?!\?)"
re.search(regex, text)
#匹配结果是 to 和 bette
环视的注意事项:
- 环视并不真正匹配字符,它只是在新增一些限制,所以适合用于修改复杂的正则表达式
- 环视语法本身用到了括号,但这个括号是不会被记入引用括号的编号。但如果环视的子表达式中出现了捕获型括号,该捕获型括号会被算入引用编号,但同时注意,由于环视的特殊性,一旦从环视结构中跳出,其中所有的捕获型括号的匹配内容都会被清空,所以外部无法获取环视内部捕获型括号的匹配内容
- 顺序环视在大部分语言中都支持,逆序环视的支持程度差很多,使用需谨慎
- 环视可以进行嵌套,因为环视结构中的子正则表达式是一个完整的正则,理论上可以随便写
- 环视可以并列,结果取它们的交集。比如
(?=[0-9]+)(?!666)
表示当前位置右侧需要是数字串,但不能是666
模式
正则的常用匹配模式有以下四种,不同模式会影响特定元字符的含义
选择模式有两种方法,一是模式修饰符,直接写在正则表达式中,二是预定义常量,是不同语言提供的一些参数,传入这些参数即可自动构建包含对应模式修饰符的正则表达式。
模式修饰符的语法是(?modifier)
,写在一个正则的最开头
无大小写模式
modifier = i,不区分字母的大小写
#python
re.search(r"(?i)hello", "HELLO") != None #True
单行模式
modifier = s,表示将文本中的换行符视为普通字符,可以用元字符点号.来匹配(默认情况.不能匹配换行符)
单行模式在部分语言中叫作点号通配,这个叫法听起来更好,因为单行模式和下面的多行模式其实毫无关系
另外,JavaScript
不支持单行模式
多行模式
modifier = m,默认模式下,^和$的匹配方式如这里所示,但在多行模式下,它们可以匹配一个文本内部的某一行字符串的开始和结束位置
注释模式
modifier = x,这是为了复杂正则设计的,例如
#python
regex = r"""
(?x) #启用了注释模式
[0-9] #匹配数字
[a-z] #匹配字母
"""
关于模式有一些补充
- 真实使用时,更推荐用对应语言的预定义常量,可以通过这个网站查询https://www.runoob.com/。原因是不同语言对这些模式的支持程度不一样,甚至含义也不太一样
- 模式是可以同时使用多个的,比如
(?ismx)
就表示同时启用这四种模式,它们事实上是不冲突的 - 模式修饰符的范围可以自定义的,如果写在开头,就是对整个正则生效,如果写在中间,就是对它后面的子正则生效。如果用括号括起来,可以限定指对某个子串生效,如
"h((?i)e)llo"
表示只对e这个字符不区分大小写