在学习 jQuery 源码时,偶然发现一个有趣的正则:
这个正则的作用很简单,即匹配 html 标签的标签名。 乍看一眼给我的感觉就是,这个管道符不是多余吗?
不过仔细看下,发现事情并没有这么简单。
先介绍一下分组的基本用法:
- 捕获分组:
(partten)
:匹配 pattern 并获取这一匹配。 - 变体:非捕获分组:
(?:partten)
: 匹配 pattern但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。
如果你不想区分大小写的话,可以使用带修饰符的分组:
(?i:partten)
。这个修饰符也叫内标记,有imsxXU 六种。可以用(?-i)
语法删除内标记。
捕获分组和非捕获分组的区别在于,捕获分组可以在内存中存储这一次的匹配的结果,可以用 \num
属性来获取第 num 个分组的内容(js 中可以用$num
来获取分组,作用一样),这一用法,我们称之为反向引用。
例如上图中,我们用 \1
获取 分组 (\w+)
匹配的内容。
如果你觉得用编号的方式获取分组的内容太麻烦,没关系,现代的正则表达式流派还支持命名捕获,即给分组起一个变量名。不过这一特性 JavaScript 并不支持(隔壁 python 支持)。手动滑稽
语法是: (?<group_name>regex)
python 的语法是 (?P<group_name>regex)
这里还有个点需要注意下:
python 的命名反向引用的语法是 (?P=group_name)
,虽然这里是用 ()
裹起来的,但是它并不是一个分组,你不能在名称和括号之间放任何东西。
正如编号捕获分组和命名捕获分组的作用一样,命名反向引用和编号反向引用的作用也一样,不过在使用中,这里还是稍稍有点区别,大家在使用的过程中要稍微注意一下。
最后再来说一下非捕获分组中管道符的用法。
假如我们要匹配 HelloWorld
和 HelloChina
,正常的写法是 HelloWorld | HelloChina
,但是这样写并不简洁,因为这两个字符串有一个相同的子串 Hello
。如果这个子串很长的话,那我们的正则表达式岂不是也会很长?
在非捕获分组中使用管道符就可以解决这一问题。我们提取公共子串,HelloWorld | HelloChina
就可以简写为 Hello(?:World|China)
。
了解了这些基本用法之后,我们在回到最开始的问题,在非捕获分组中,管道符(|
)放到结尾到底有啥用呢?
这个用法可以算是一个小技巧,表示可以匹配 0 次或 1 次 非捕获分组中的内容,例如 Hello(?:World|China|)
可以匹配 HelloWorld、HelloChina,也可以只匹配 Hello。
这个用法相当于量词 ?
即 {0,1}
Hello(?:World|China|)
、 Hello(?:World|China)?
、Hello(?:World|China){0,1}
这三者的作用是等价的。
在实际开发中,推荐第一种用法,因为在正则表达式中,?
的用法非常多:
- 可以接在量词后面,表示非贪婪匹配(也叫懒惰匹配),如
+?
,*?
,{n}?
,{n,}?
,{n,m}?
- 其本身也是量词,表示匹配前面的表达式 0 次或 1 次,和
{0,1}
等价,例如do(es)?
既可以匹配do
,也可以匹配does
- 可以是分组的固定写法:如
(?:)
表示非捕获分组 - 命名分组:
(?'name'partten)
,(?<name>partten)
,(?P<name>partten)
- 也可以是断言的固定写法,如:
- 正向肯定零宽断言:
(?=partten)
- 正向否定零宽断言:
(?!partten)
- 反向肯定零宽断言:
(?<=partten)
- 反向否定零宽断言:
(?<!partten)
- 正向肯定零宽断言:
- 用于表示备注: (?#备注内容,正则匹配时不会解析这段话)
- 其他不常见用法:
- 固化分组: ``
- 内标记。语法是
(?imsxXU)
。如(?i)a
既可以匹配字母a
也可以匹配字母A
- 递归表达式:
(?R)
- 递归第一个子表达式:
(?1)
综上。