正则-非捕获分组中,管道符(|)放结尾有啥用?

2,094 阅读3分钟

在学习 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),虽然这里是用 ()裹起来的,但是它并不是一个分组,你不能在名称和括号之间放任何东西。

正如编号捕获分组和命名捕获分组的作用一样,命名反向引用和编号反向引用的作用也一样,不过在使用中,这里还是稍稍有点区别,大家在使用的过程中要稍微注意一下。

最后再来说一下非捕获分组中管道符的用法。

假如我们要匹配 HelloWorldHelloChina,正常的写法是 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)

综上。