7.1 什么是子表达式
我们在第 5 章学习了如何匹配一个字符的连续多次重复。正如我们讨论的那样, \d+
将匹配一个或多个数字字符,而 https?:// 将匹配 http:// 或 https://。
什么是子表达式?
在这两个例子里(事实上,是在我们此前见过的所有例子里),用来表明重复次数的元字符(如 ?
或 \*
或 {2}
,等等)只作用于紧挨着它的前一个字符或元字符。
我们来看一个例子。有些短语(例如 Windows 11)虽然由多个单词构成,但其实是一个整体。有许多 HTML 程序员喜欢让这类短语在浏览器里显示在同一行上。为了确保这一点,他们会在编写 HTML 文档时在这些短语的单词之间使用非换行型空格( , nbsp 是“non-breaking space”的缩写,其含义是“不是换行符的空格”)而不是普通的空格。下面就是一个这样的例子:
{2,}
Hello, my name is fang he, Welcome to my 《tutorial of regex》
" " 是 HTML 语言中的非换行空格字符。在这里使用模式 {2,}
的本意是希望它能把 " " 连续两次或更多次的重复出现找出来,但它没能给出我们所预期的结果。为什么会这样?因为 {2,}
只作用于紧挨着它的前一个字符——那是一个分号。如此一来,这个模式只能匹配像 " ;;;" 这样的文本,但无法匹配 " ",这就引出了子表达式的概念。
子表达式
子表达式是一个更大的表达式的一部分;把一个表达式划分为一系列表达式的目的是为了把那些子表达式当作一个独立元素来使用。子表达式必须用 (
和 )
括起来。
(
和 )
是元字符。如果需要匹配 "(" 和 ")",就必须使用 \
转义。
我们再来看刚才的例子:
( ){2,}
Hello, my name is fang he, Welcome to my 《tutorial of regex》
( )
是一个子表达式,它将被视为一个独立元素,而紧跟在它后面的 {2,}
将作用于这个子表达式(不仅仅是分号)。这个模式解决了我们的问题。
还记得我们在 5.2 匹配的重复次数 中的例子吗?在这个例子里面我们匹配了 ipv4 地址,当时我们用作例子的模式并不最优的,我们还可以使用子表达式缩短它,看下面这个模式:
\d{1,3}(\.\d{1,3}){3}
路由器: 192.168.1.1 子网掩码:192.168.255.255 手机:192.168.1.10 电脑:192.168.1.120
这个模式的作用和 5.2 匹配的重复次数 中的模式作用完全相同,但我们使用了子表达式,让整个模式缩短了一些。在新模式中,我们先使用 \d{1,3}
匹配 1 到 3 数数字。 \.\d{1,3}
做为一个子表达式被重复了 3 次。
为了提高可读性,有不少用户喜欢给表达式的每一个子表达式都加上括号。比如,把上面那个例子里的模式写成 (\d{1,3})(\.\d{1,3}){3}
。这种做法在语法上完全成立,对表达式的实际行为也没有任何不良影响(但视乎具体的正则表达式实现,这对匹配操作的速度可能会有点儿影响)。
或操作符
子表达式是一个非常重要的概念,所以我们认为有必要再给大家看一个例子,它不涉及重复次数问题。在下面的例子里,我们的任务是把一条用户记录里的年份数字完整地匹配出来:
19|20\d{2}
id: 1000
name: hefang
sex: 1
birthDate: 1994-08-31
role: root
这个例子需要我们构造一个模式去查找一个 4 位数的年份数字。为了排除没有实际意义的结果,我们把前两位数字限定为 19 和 20。这个模式里的 |
字符是正则表时的或操作符 19|20
匹配数字序列 "19" 或 "20"。既然如此,模式 19|20\d{2}
应该匹配以 "19" 或 "20" 开头的 4 位数字("19" 或 "20" 的后面再跟着两位数字)。可是,这个模式的匹配结果与我们的预期并不相符,它只匹配到了 19,随后两位数字没有被匹配到。为什么会这样?因为 |
操作符是把位于它左边和右边的两个部分都作为一个整体来看待的,它会把模式 19|20\d{2}
解释为 19
或 20\d{2}
(也就是把 \d{2}
解释为以 20 开头的那个表达式的一部分)。换句话说,它将匹配数字序列 19 或以 20 开头的任意 4 位数字。最终的结果你们已经看到了,它只匹配到了 19。
这个例子的正确答案是把 19|20
归为一个子表达式,如下所示:
(19|20)\d{2}
id: 1000
name: hefang
sex: 1
birthDate: 1994-08-31
role: root
我们把所有的选项都归纳到了一个子表达式里,这将向 |
表明我们打算匹配的是这个子表达式里的选项之一。 (19|20)\d{2}
正确地匹配到了 1967;当然,以 19 或 20 开头的任何一个 4 位数字都将与这个模式相匹配。今后(比如,从现在算起 100 年内),如果需要修改这段代码以包括以 21 开头的年份,只要把这个模式改成 (19|20|21)\d{2}
就可以了。
本章讨论的只是子表达式的用途之一。子表达式还有另外一个非常重要的用途,我们将在第 8 章里对之进行讨论。