Patterns
撰写时间:2024-02-28
修订时间:2024-03-06
概述
上面我们学到了在正则表达式中如何设定搜索内容:
像这种将要搜索的字符集is
直接写进pattern中的方法,我们称为静态搜索。正则表达式引擎将完全按照我们所指定的字符来进行搜索。但这种方式缺乏灵活性。
例如,我们准备搜索所有的大写字母
。使用静态搜索是很难实现目标的,除非将26个大写字母都列出来。但我们所不知道的是,正则表达式的pattern已经为我们提供了极大的便利。例如:
pattern表达式[A-Z]
表示匹配从A
到Z
的所有的大写字母。很直观。
还有更多、更丰富、更实用的表达式等待我们去探索。
在支持灵活的pattern表达式后,使用者是很方便了,但可想而知,正则表达式的内部搜索过程将变得无比复杂。而RegExp类的方法却只有两个:test及exec。这也是我们之前为何要花许多时间来学习这些方法的原因。我们将一条大香肠切成众多的小片,烤着吃。不仅味道鲜美,而且还易于消化。
惊叹于这种高效的设计,作为使用者,我们从本节开始,将全面学习pattern的各种表达式。我们将从最实用、最常见的功能开始,循序渐进地学习。
Pattern表达式是最能体现正则表达式强大之处的地方,内容多,但表达式对人类不够友好,因此需慢慢体会、习惯、熟悉。
下面,我们将从4大方面学习pattern:
- 匹配哪些字符
- 限定字符的位置
- 限定字符的数量
- 分组与引用
关于本页面例子的说明
出于讲解的需要,本页面中将会出现许多较小、但功能特定的例子。每个例子都应有3个要求:例子要求准确,且需经过运行测试,最后还能将结果显示出来。这个工作量是相当巨大且很容易出错的,并且,如果以后改了代码,就得重新运行并同步其结果。
鉴于此,我边学边用,利用正则表达式的功能,编写了一段小代码,实现了这样的功能:我只负责编写例子代码,而在HTML页面加载后,这段小代码将自动运行页面中的每个例子程序,并将运行结果动态地添附到该例子代码的下面。
点击查看代码
${outputStr}
`); } } function replacer(matched, offset) { return `${matched}`; }一点说明:上面代码出于直观的目的,为了显示不可打印的控制字符,特将它们均转换为2个可打印的普通字符,并相应地改了匹配模式。具体详见ASCII转义字符。
因此,本页面中所有的例子程序都会自动得到运行,并在该例子代码下面自动生成运行结果。如果运行结果与我们的预期不同,则只能说明一个问题,该例子代码有问题。而如果运行结果没问题,则说明例子代码已经实时运行验证而没问题,可放心使用。
同时,这也是为何要学习正则表达式的一个活生生的解答:学习正则表达式之后,我们的工具箱中又多了一件强大的工具,在需要时,即可将其亮出来。
更重要的是,当学会正则表达式之后,我们的锤子思维又更加丰富了:当我们拥有一把锤子,而又看到面包时,我们就会想到,我能不能用锤子将面包砸开来吃?
逻辑或
pattern表达式|
表示逻辑或的关系。如one|two
表示将匹配字符串one
或字符串two
。
上面的代码w|k
表示,匹配w
或k
这两个字符。
逻辑或的关系可串联多个元素。
下面将逻辑或应用到多字符的场合。
上面|
符号的两边均是多个字符,则see
与sea
成为逻辑或的关系。
see
与sea
只有最后一个字符不同。我们能否写成/see|a/g
?
正则表达式引擎将其理解为共有左右两部分,左边是see
,右边是a
。说明对于|
,正则表达式引擎并非只从|
的左右两边各取1个字符。正则表达式引擎从|
的两边各取所有的字符作为逻辑或运算的成分。
而我们这里的匹配要求是:先是se
两个字符,紧接着是e
或a
字符。
我们可以使用分组符号(...)
来明确要求。
分组
分组的作用
括号(...)
是个好东西,在数学中它可以优先计算,在编程语言中它可以优先执行。
而相类似的,在正则表达式中,它称为分组(grouping),分组内的部分将优先得到解析。
se(e|a)
的意思为:先是se
两个字符,紧接着是e
或a
其中的1个字符。意思非常明确,无任何歧义。
因此,在有可能产生歧义的地方,我们可使用分组符号(...)
来消除歧义。
非捕获的分组
但上面产生了一个副作用。之前我们学过,如果在pattern中使用分组符号,将使result这个数组多产生额外的数组元素。
结果为:
分组可导致额外的数组元素的这个效果,在别的地方可能用得上。但在这里,我们只需要see
或sea
这两个匹配结果。而额外的结果占用了更多的内存空间,加重了系统的负担,降低了程序运行效率。如何避免?
表达式(?:x)
称为非捕获的分组(non-capturing group)。正则表达式将只对x
进行分组,但不会将其匹配结果添加为result新的数组元素。
(?:e|a)
表示,选项只从分组括号内的字符e
或a
中产生,而此分组不进行捕获。
终端显示:
完全符合我们的目标!
范围
[xyz]: 特定范围内的字符集
要匹配特定范围内的字符,可用[xyz]
的表达式。该表达式可匹配到[]
之内的任一字符。
上面的句子是虚拟的伪文,语义上没有任何意义,但在这里可代表随意产生的字母组合,可作随机文本输入源使用。
[abc]
表示,如果出现a
, b
, c
这3个字符中任意一个字符,则予以匹配。我们可以随意将任意字符添加进中括号[]
中。
从上面我们知道,表示范围的代码[abc]
与表示逻辑或的代码a|b|c
的效果都是一样的,都是从a
, b
, c
这3个字符中匹配任意一个字符。当选项元素较多时,使用[abc]
更加便捷、直观。
看下面的例子:
我们的本意是将Mike
或Tom
作为两个独立的个体都列入匹配的范围,因此放在[]
内,且使用()
来明确分组。但如同上面所见,该表达式未产生匹配结果。说明,表示范围的表达式,只对单字符起作用。若要从由多个字符组成的元素中进行选择,可使用逻辑或的表达式。
[^xyz]: 不在特定范围内的字符集
[^xyz]
的表达式,可匹配到不在[xyz]
这个范围内的任意字符。
[A-Z]: 连续递进的范围
如果这些字符是连续且递进的,则可使用[x-z]
的表达式,表示从x
,到z
这个范围的字符均可匹配。因此,最终匹配的字符将是x
, y
, z
。
所以,[A-Z]
匹配所有大写字母:
[a-z]
匹配所有小写字母:
[0-9]
匹配0
到9
所有阿拉伯数字的字符:
注意,连续的字符只能是递进的,不能递减。例如[Z-A]
则是错误的表达式。
[^x-z]: 不在特定范围内的字符集
[^x-z]
的表达式,可匹配到不在[x-z]
这个范围内的任意字符。
我们原想排除匹配a
到v
这些小写字母,但从结果中发现,上面的长文还藏了大写字母、空格及标点符号。
多个范围的合并
可以合并多个范围。下面的表达式可匹配所有的字母及阿拉伯数字。
由于这种组合经常使用,您在下面将会看到,有专门的表达式来实现上面的匹配效果。
范围内的元字符原形毕露
置于中括号[]
内的各个字符,除了^
及-
之外,不管其原来是否元字符,都重新变成单纯的字符的组合。原来的元字符的效用失去作用。
字符(
, )
, .
及?
原是正则表达式中的重量级元字符。但即使是这些元字符,一旦身处[]
中,就会变回普通的字符。因此,上面的结果分别匹配出了左括号(
, 右括号)
, 句号.
及问号?
。
而天选之子^
及-
由于需要在范围内描述否定及连续增进的特征,才可得以保留其原来作为元字符的作用。
转义
所谓转义,是指我们可以通过某种方式,将某字符的原本含义转变为另一含义。在正则表达式中,转义分为两种。
元字符到普通字符的转义
上面我们接触了pattern的一些表达式,它们都使用了专门的字符来表示特定的含义。如|
, ()
, ?
, []
等。当正则表达式看到这些字符,就会按特定的含义来解析这些字符。这些在正则表达式中有特定含义的字符,称为元字符(meta character)。元字符不会被匹配。
如果我们需要匹配元字符,则需要将它们进行转义,只要在它们前面加上转义符\
符号即可。当正则表达式引擎看到字符前面有转义符\
,就不会解析为元字符,而是按普通字符来进行匹配。例如:
?
本来是元字符,由于我们需要匹配它,因此在pattern表达式中,在其前面加上转义符\
,从而构成了表达式\?
。这样,正则表达式引擎将把它视为普通的?
而予以匹配。
当我们使用转义符时,会产生两个问题。
第一个问题是,混杂了元字符及经转义后的普通含义的元字符的表达式,诸如/[\?\(\)]/g
的代码有点难看,大脑一下子转不过来。
这是典型的人脑与电脑的区别。人类可以一眼就认出每个不同的人的相貌,而电脑却要通过人工智能的人脸识别技术才能精准识别出来。但对于/[\?\(\)]/g
这样的代码,人脑确实反映不过来,但电脑却是一眼就能识别。这也是造成许多人对正则表达式望而生畏的主要原因之一:看不懂,不学了!
其实我们应当明白,正则表达式是我们给电脑下指令的一种方式,就是要通过电脑看得懂的方式来下指令,它才会准确执行。当我们有钱了,到国外去买菜,而售货员又不懂中文,要让他将菜卖给我们,我们得用他听得懂的当地语言来与其交流,无可厚非。但此问题并非无解。实际上,构建pattern表达式远比看懂pattern表达式容易得多。在构建pattern表达式时,我们是一步一步进行的,每一步的思路都很明确、清晰。搞定每一步骤后,又长又难看的表达式就出来了。而一旦运行结果与我们预期的完全一致,谁又会去要求自己回头快速地辨识这些表达式呢?逐步地构建表达式并及时验证运行结果,这就足够了。
第二个问题是,我们需时刻谨记哪些字符是元字符。这个好办。实践多了,大脑就会熟悉,就能解决了看不懂的问题。而难以全部记忆的问题也好解决,将这些所遇到的元字符专门列个清单,在需要时随时查阅即可。
后面我们还会学习一些元字符,在全面学习后,我会将这些元字符全部列为清单。
正则表达式为何高效?众多极简的元字符,功不可没。正则表达式的设计,可以说是设计史上的殿堂,自正则表达式设计出来后,几十年来,其他佳作,无出其右。
因此,不要纠结于这些问题,学就对了。
普通字符到元字符的转义
本来是普通字符,但出于应用的目的,正则表达式规定,一些普通字符,可以通过在其前面添加转义符\
,从而将该普通字符转义为正则表达式的元字符。例如:
字符d
是26个英文字母中的一个普通字符,但由于它运气不佳,恰好是英文单词digit
中的首字母,就被正则表达式选用为代表匹配数字(digit)的元字符。当然,选用后,不能失去字符d
原来作为普通英文字符的含义,因此,正则表达式引擎要求在字符d
的前面添加转义符\
而将其转换为元字符:\d
。
相对于第一种的元字符,这一类的元字符不容易出错。一是原来的字符可以照常用来匹配,二是当我们需要使用普通字符转义为元字符来表述这种特定需求时,我们的大脑就会自然而然地想到:我需要将普通的d
字符转义为代表匹配数字的元字符。
下面我们会依序学到许多这样的元字符。只是当我们看到诸如\d
这样的代码时,我们应当清楚,这是由普通字符转义而来的正则表达式元字符。
本节讲述了正则表达式中元字符的转义。此外,还有其他需要转义的场合,如HTML转义,以及ASCII转义字符等。它们均会在很大程度上影响到正则表达式的正确使用。
因此,在学习正则表达式过程中,一旦涉及到各个场合下的字符转义的问题,一定要立即警觉起来。