字符组就是“一组”字符。在正则表达式中,它表示“在同一个位置可能出现的各种字符”。写法:在一对方括号[和]之间,列出所有可能出现的字符。例如:[abc]、[123]、[#.]等
例如:检测字符串中是否存在数字?
/[0123456789]/.test('hello123world');//true字符组中字符的排列顺序并不影响字符组的功能,出现重复字符也不会影响。例如:[0123456789]完全等价于[9876543210]、[998876543210]。
为了代码更容易编写、方便阅读,不推荐在字符组中出现重复字符,而且还应该让字符组中的字符排列更符合认知习惯。为此,正则表达式提供了-范围表示法(range),它更直观,能进一步简化字符组。
所谓-范围表示法,就是用[x-y]的形式,表示x到y整个范围内的字符。[0123456789]就可以表示为[0-9]。
-范围表示法的范围是如何确定的?为什么要写作[0-9],而不写作[9-0]
在字符组中,-表示的范围,一般是根据字符对应的码值(字符在对应编码表中的编码数值)来确定的。码值小的字符在前,码值大的字符在后。在ASCII编码中,字符0的码值是48(十进制),字符9的码值是57(十进制)。所以[0-9]等价于[0123456789]。
在字符组中可以同时并列多个-范围表示法。例如:[0-9a-fA-F]可以匹配数字,大小写形式的a~f,它可以用来验证十六进制字符。
可以使用转义序列\xnum来表示一个字符。
其中\x是固定前缀,表示转义序列的开头,num是字符对应的码值,是一个两位的十六进制数值。例如:字符A的码值是41(十进制则为65),所以也可以用\x41表示。
字符组中有时会出现这种表示法,它可以表示一些难以输入或者难以显示的字符,比如\x7f。也可以用来方便地表示某个范围,例如:
//匹配是否是非中文字符/[\x00-\xff]/.test('路');//false1.2元字符与转义字符组中的横线-并不能匹配横线字符,而是用来表示范围,这类字符叫元字符。
字符组的开方括号[、闭方括号]、横线-算元字符,在匹配中,有着特殊的意义。但有时候并不需要表示特殊的意义,只需要表示普通字符,此时就必须做特殊处理。
字符组中的-,如果它紧邻着字符组中的开方括号[或闭方括号],那么它就是普通字符,其他情况下都是元字符。
//-紧邻开方括号[,表示普通-字符/[-9]/.test('-');//true//-紧邻闭方括号[,表示普通-字符/[9-]/.test('-');//true而对于其他元字符,取消特殊含义的做法都是转义,也就是在正则表达式中的元字符前加上反斜线\字符。
Javascript语言使用RegExp构造器创建正则表达式,转义字符组中的横线-:
newRegExp('[0\\-2]');在上文里说“在正则表达式中的元字符之前加上反斜线\字符,对元字符转义”,而在代码里写的却不是[0\-2],而是[0\\-2]。因为在这段程序里,正则表达式是以字符串的方式提供的,而字符串本身也有关于转义的规定。上面说的“正则表达式”,是经过“字符串转义处理”之后的字符串的值。因为处理字符串时,反斜线和它之后的字符会被认为是转义序列。因此需要\\转义成\。
在Javascript中也可以使用不转义的原生模版字符串,例如:
String.raw`\'"`;//等价于:`\\'"`newRegExp(String.raw`[0\-2]`);//等价于:/[0\-2]/1.3排除型字符组在方括号[...]中列出希望匹配的所有字符,这种字符组叫做普通字符组。它的确非常方便,但也有些问题是普通字符组不能解决的。比如匹配字符串中是否存在非数字字符,不是数字的字符太多了,全部列出几乎不可能,这时就应当使用排除型字符组。
排除型字符组非常类似普通字符串[...],只是在开方括号[之后紧跟一个尖角号^,写作[^...],表示“在当前位置,匹配一个没有列出的字符”。所以[^0-9]就表示“0~9之外的字符”,也就是“非数字字符”。
注意:排除型字符组必须匹配一个字符。
//匹配一个除-、0、9之外的字符/[^-09]/.test('-');//false//匹配一个除0~9之外的字符/[^0-9]/.test('a');//true在排除型字符组中,^是一个元字符,但只有它紧跟在[之后时才是元字符。如果想表示“这个字符组中可以出现^字符”,不要让它紧挨着[即可,否则就要转义。
/[0^9]/.test('^');//true1.4字符组简记法用[0-9]、[a-z]等字符组,可以很方便地表示数字字符和小写字母字符。对于这类常用的字符组,正则表达式提供了更简单的记法,这就是字符组简记法。
常见的字符组简记法有\d、\w、\s。
/\d/.test('2');//true/\w/.test('a');//true/\w/.test('_');//true/\w/.test('2');//true/\s/.test('');//true字符组简记法可以单独出现,也可以使用在字符组中。比如[0-9a-zA-Z]可以写成[\da-zA-Z]。字符组简记法也可以用在排除型字符组中,比如[^0-9]可以写成[^\d]。
相对于\d、\w、\s这三个普通字符组简记法,正则表达式也提供了对应排除型字符组的简记法:\D、\W、\S。字母完全一样,只是改为大写。
//\d和\D/\d/.test('8');//true/\d/.test('a');//false/\D/.test('8');//false/\D/.test('a');//true//\w和\W/\w/.test('c');//true/\w/.test('!');//false/\W/.test('c');//false/\W/.test('!');//true//\s和\S/\s/.test('\t');//true/\s/.test('0');//false/\S/.test('\t');//false/\S/.test('0');//true注意:[\s\S]、[\w\W]、[\s\S]能匹配任意字符。
关于字符组简记法,最后需要补充两点:
验证中国大陆地区的邮政编码(6位数字构成的字符串),比如201203。
只有同时满足以下两个条件,匹配才成功:
/^\d\d\d\d\d\d$/.test('201203');//true注意:以上代码中的^表示字符串的起始位置,$表示字符的结束位置。(后续会讲解)
\d重复了6次,读写都不方便。为此,正则表达式提供了量词。比如上面匹配邮政编码的表达式,就可以简写为\d{6},它使用阿拉伯数字,更简洁直观。
/^\d{6}$/.test('201203');//true正则表达式中一般形式量词,如下所示:
注意:量词结构中的逗号,之后不能有空格。
以上提到的术语:
{n,m}是通用形式的量词,正则表达式还有3个常用量词,分别是+、*、。它们的形态虽然不同于{n,m},功能却是相同的(也可以把它们理解为“量词简记法”)。
正则表达式中常用量词,如下所示:
因为点号能匹配几乎所有的字符,所以实际应用中许多人图省事,随意使用.*或.+,结果却事与愿违,下面以双引号字符串为例来说明。
之前我们使用正则表达式/"[^"]"/来匹配双引号字符串,而“图省事”的做法是/".*"/。通常这么用是没有问题的,但也可能有意外。
'\"quotedstring\"'.match(/".*"/g)[0];//"quotedstring"'"quotedstring"andanother"'.match(/".*"/g)[0];//"quotedstring"andanother"用/".*"/匹配双引号字符串,不但可以匹配正常的双引号字符串"quotedstring",还可以匹配格式错误的字符串"quotedstring"andanother"。这是为什么呢?
在正则表达式/".*"/中,点号.能匹配除换行符外的任意字符,*表示可以匹配的字符串长度没有限制。所以.*在匹配过程结束之前,每遇到一个字符(除换行符外),.*都可以匹配。但是到底是匹配这个字符,还是忽略它,将其交给之后的"来匹配呢?
具体选择取决于所使用的量词。正则表达式中的量词分为几类,之前介绍的量词都可以归到一类,叫做匹配优先量词(或贪婪量词)。匹配优先量词,就是在拿不准是否要匹配的时候,优先尝试匹配,并且记下这个状态,以备将来“反悔”。
来看表达式/".*"/对字符串"quotedstring"的匹配过程。
这时候,字符串之后已经没有字符了,但正则表达式中还有"没有匹配。所以只能查询之前保存备用的状态,看看能不能退回几步,照顾"的匹配。查询到最近保存的状态是:【"也可能是.*不应该匹配的】。于是让.*“反悔”对"的匹配,把"交给",测试发现正好能匹配,所以整个匹配宣告成功。这个“反悔”的过程,专业术语叫做回溯。
如果我们把字符串换成"quotedstring"andanother",.*会首先匹配第一个双引号之后的所有字符,再进行回溯,表达式中的"匹配了字符串结尾的字符",整个匹配宣告完成。
如果要准确匹配双引号字符串,就不能图省事使用/".*"/,而要使用/"\[^"]\*"/。
有些时候,确实需要用到.*(或者[\s\S]*),比如匹配HTML代码中的Javascript示例就是如此。
换个角度,通过改变[\s\S]*的匹配策略来解决问题:在不确定是否要匹配的场合,先尝试不匹配的选择,测试正则表达式中后面的元素。如果失败,再退回来尝试[\s\S]*匹配,如此就没有问题了。
循着这个思路,正则表达式中还提供了忽略优先量词(或懒惰量词)。如果不确定是否要匹配,忽略优先量词会选择“不匹配”的状态,在尝试表达式中之后的元素。如果尝试失败,再回溯,选择之前保存的“不匹配”的状态。
对于[\s\S]*来说,把*改为*就是使用了忽略优先量词,*限定的元素出现次数范围与*完全一样,都表示“可能不出现,也可能出现,出现次数没有上限”。区别在于,在实际匹配过程中,遇到[\s\S]能匹配的字符,先尝试“忽略”,如果后面的元素不能匹配,再尝试“匹配”,这样就保证了结果的正确性。
C语言的注释有两种:
//提取路径/^.*\////提取文件名/[^/]+$/Windows下路径的分隔符是\,比如C:\ProgramFiles\Python2.7.1\python.exe。
//提取路径/^.*\\///提取文件名/[^\\]+$/2.5量词转义在正则表达式中,+、*、等字符作为量词,具有特殊意义。但有些情况下,我们需要的就是这些字符本身,此时就必须使用转义,也就是在它们之前添加反斜线\。
常用量词所使用的字符+、*、。如果希望表示这三个字符本身,直接添加反斜线,变为\+、\*、\即可。
一般形式的量词,比如{n,m},虽然具有特殊含义的字符不止一个,转义时却只需要给第一个{添加反斜线即可。如果希望匹配字符串{n,m},正则表达式必须写成\{n,m}。
忽略优先量词字符串中也包含不止一个特殊含义的字符,在转义时却不像一般形式的量词那样,,只转义第一个字符即可,而需要将两个量词全部转义。例如:如果要匹配字符串*,则正则表达式就必须写成\*\。
正则表达式中各种量词字符串的转义,如下:
点号.也是一个元字符,它可以匹配除换行符外的任意字符。如果要匹配点号本身,必须将它转义为\.。
/^\+$/.test('+');//true/^\*$/.test('*');//true/^\$/.test('');//true/^\{6}$/.test('{6}');//true/^\{6,8}$/.test('{6,8}');//true/^\{0,8}$/.test('{0,8}');//true/^\{0,8}\$/.test('{0,8}');//true/^\{6,8}\$/.test('{6,8}');//true/^\{6}\$/.test('{6}');//true/^\+\$/.test('+');//true/^\*\$/.test('*');//true/^\\$/.test('');//true/^\.$/.test('.');//true第3章括号3.1分组用正则表达式匹配身份证号码,依靠字符组和量词能不能做到呢?
身份证号码是一个长度值为15或18个字符的字符串。
正则表达式\d{2}[\dx]是不行的,因为量词只能限定[\dx]的出现,而正则表达式\d{2}[\dx]同样不行。
使用括号(),把正则表达式改写为/^[1-9]\d{14}(\d{2}[\dx])$/。
量词限定之前元素的出现,这个元素可能是一个字符,也可能是一个字符组,还可能是一个表达式。如果把一个表达式用括号包裹起来,这个元素就是括号里的表达式,括号里的表达式通常称为子表达式。
括号的这种功能叫做分组。如果用量词限定出现次数的元素不是字符或字符组,而是连续的几个字符甚至子表达式,就应该用括号将它们“编为一组”。比如,希望字符串ab重复出现一次以上,就应该写作(ab)+,此时(ab)成为一个整体。
有了分组,就可以准确表示“长度只能是m或n”。
关于括号的分组功能,最后来看E-mail地址的匹配:E-mail地址以@分隔成两段,@之前的是用户名,之后的是主机名。用户名@主机名
用户名的匹配非常简单,其中能出现的字符主要有大写字母[A-Z]、小写字母[a-z]、阿拉伯数字[0-9]、下划线_、点号.,所以总的字符组就是[A-Za-z0-9_.],又可以简化为[\w.]。用户名的最大长度是64个字符,所以匹配用户名的正则表达式就是[\w.]{1,64}。
主机名匹配的情况则要麻烦一些,简单的情况比如somehost.net,复杂的情况则还包括子域名,比如mail.somehost.net,而且子域名可能不止一级,比如mail.sub.somehost.net。查阅规范可知,主机名被点号分隔成为若干段,叫做域名字段,每个域名字段中能出现的字符是字母字符、数字字符和横线字符,长度必须在1~63之间。
最后的域名字段是顶级域名,之前的部分可以看做某种模式的重复:该模式由域名字段和点号组成,域名字段在前,点号在后。匹配域名字段的表达式是[A-Za-z0-9-]{1,63},匹配点号的表达式是\.。使用括号的分组功能,把两个表达式分为一组,用量词*限定表示“不出现,或出现多次”,就得到匹配主机名的表达式([A-Za-z0-9-]{1,63}\.)*[A-Za-z0-9-]{1,63}。(顶级域名也是一个域名字段,所以即便主机名是localhost,也可以由最后那个匹配域名字段的表达式匹配)
将匹配用户名的表达式、@符号、匹配主机名的表达式组合起来,就得到了完整的匹配E-mail地址的表达式:
/^[\w.]{1,64}@([A-Za-z0-9-]{1,63}\.)*[A-Za-z0-9-]{1,63}$/3.2多选结构之前用表达式[1-9]\d{14}(\d{2}[\dx])匹配身份证号,思路是把18位号码多出的3位“合并”到匹配15位号码的表达式中。
能不能直接分情况处理呢?15位身份证号就是[1-9]开头,之后是14位数字。18位身份证号就是[1-9]开头,之后是16位数字,最后是[0-9x]。只要两个表达式中的一个能够匹配,就是合法的身份证号。
答案是可以的,而且仍然使用括号解决问题,只是要用到括号的另一个功能:多选结构。
多选结构的形式是(...|...),在括号内以竖线|分隔开多个子表达式,这些子表达式也叫多选分支。在一个多选结构内,多选分支的数目没有限制。在匹配时,整个多选结构被视为单个元素,只要其中某个子表达式能够匹配,整个多选结构的匹配就能成功。如果所有子表达式都不能匹配,则整个多选结构匹配失败。
身份证号码既然可以区分15位和18位两种情况,就可以将每种情况对应的表达式作为一个分支,“合并”为多选结构。
//正则表达式/^([1-9]\d{14}|[1-9]\d{16}[\dx])$/多选结构在实际中经常用到,匹配IP地址就是如此:IP地址(暂不考虑IPv6)分为4段(4字节),每段都是8位二进制,换算成十进制,取值在0~255之间,中间以点号.分隔。
//精确匹配0~255之间的字符串/^(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/如果要更完善一点,能识别03、030、005这样的数值,可以修改对应的子表达式,为一位数和两位数的情况在前面增加可能出现0的匹配。
/^(0{0,2}\d|0[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/多选结构是正则表达式匹配数值,在某个范围内的通用模式。例如:匹配月(1~12)、日(1~31)、小时(0~23)、分钟(0~59)的正则表达式。
关于多选结构,最后还要补充三点:
第一,多选结构的一般表示法是(option1|option2)(其中option1和option2是两个作为多选分支的正则表达式),在多选结构中一般会同时使用括号()和竖线|。但是如果没有括号(),只出现竖线|,仍然是多选结构。例如:ab|cd既可以匹配ab,也可以匹配cd。
/ab|cd/.test('ab');//true/ab|cd/.test('cd');//true在多选结构中,竖线|用来分隔多选结构,而括号()用来规定整个多选结构的范围。如果没有出现括号,则将整个表达式视为一个多选结构。所以ab|cd等价于(ab|cd)。
推荐明确写出括号,这样更形象,也能避免一些错误。
/^ab|cd$/.test('bcab');//false因为竖线|的优先级很低,所以^ab|cd$等价于(^ab|cd$),而不是^(ab|cd)$。它的真正意思是匹配“字符串开头的ab或者字符串结尾的cd”,而不是匹配“只包含ab或cd的字符串”。
第二,多选结构不等于字符组。多选分支看起来类似字符组,如[abc]能匹配的字符串和a|b|c一样。从理论上说,可以完全用多选结构替换字符组,但这种做法并不推荐。理由在于:首先,[abc]比a|b|c要简洁许多,在多选结构中的每个分支都必须明确写出,不能使用-范围表示法。其次,在大多数情况下,[abc]比a|b|c的效率要高很多。所以能用字符组解决的问题,最好不要用多选结构。
反过来,字符组不一定能对应到多选结构。因为字符组的每个“分支”的长度相同,而且只能是单个字符。而多选结构的每个“分支”的长度没有限制,甚至可以是复杂的表达式,比如(abc|b+c*ab),字符组完全无能为力。
多选结构和字符组的另一点重要区别是:多选结构无法表示排除型字符组。比如[^abc]表示“匹配除a、b、c之外的任意字符”,而(^a|b|c)表示“匹配以a开头的字符串,或匹配字符b,或匹配字符c”。
第三,多选结构的排列是有讲究的。比如这个表达式(jeff|jeffrey),用它匹配字符串jeffrey。结果到底是jeff还是jeffrey呢?这个问题没有标准答案,取决于编程语言。一般多选结果都会优先选择最左侧的分支。正则表达式(jeff|jeffrey)还是(jeffrey|jeff),结果是不一样的。
'jeffrey'.match(/jeff|jeffrey/);//'jeff''jeffrey'.match(/jeffrey|jeff/);//'jeffrey'在平时使用中,如果出现多选结构应当尽量避免多选分支中存在重复匹配。因为这样会大大增加回溯的计算量。也就是说,应当避免这样的情况:针对多选结构(option1|option2),某段文本既可以由option1匹配,也可以由option2匹配。如果出现了这样的多选结构,效率可能会受到极大影响,尤其在受量词限定的多选结构中更是如此。([0-9]|\w)之类的一不留神就会遇到。
括号不仅能把有联系的元素归拢起来并分组,还有其他的作用———使用括号之后,正则表达式会保存每个分组真正匹配的文本,等到匹配完成后,通过编号num“引用”分组在匹配时捕获的内容。
其中,num表示对应括号的编号,括号分组的编号规则是从左向右计数,从1开始。
因为正则表达式的分组“捕获”了文本,并保存下来,所以这种功能叫做捕获分组。
我们经常遇到诸如2010-12-22、2011-01-03这类表示日期的字符串,希望从中提取出年、月、日之类的信息,就可借助捕获分组来实现。在正则表达式中,每个捕获分组都有一个编号,具体情况如下所示:
一般来说,正则表达式匹配完成之后,会得到一个表示“匹配结果”的对象。通过分组编号num,可以得到对应分组匹配的文本。如果匹配成功,会返回一个列表(或数组)。如果匹配失败,会返回空值。
分组的编号从1开始。不过,也有编号为0的存在,对应整个表达式匹配的文本。
'2023-10-13'.match(/(\d{4})-(\d{2})-(\d{2})/)[0];//'2023-10-13'有些正则表达式里可能包含嵌套的括号。但无论括号如何嵌套,分组的编号都是根据括号出现顺序来计数的。开括号是从左向右数起第多少个括号,整个括号分组的编号就是多少。如图所示:
'2023-10-13'.match(/((\d{4})-(\d{2})-(\d{2}))/)[0];//'2023-10-13''2023-10-13'.match(/((\d{4})-(\d{2})-(\d{2}))/)[1];//'2023-10-13''2023-10-13'.match(/((\d{4})-(\d{2})-(\d{2}))/)[2];//'2023''2023-10-13'.match(/((\d{4})-(\d{2})-(\d{2}))/)[3];//'10''2023-10-13'.match(/((\d{4})-(\d{2})-(\d{2}))/)[4];//'13'需要注意:引用分组时,引用的是分组对应括号内,表达式捕获的文本。而非表达式。例如:
'2023-10-13'.match(/(\d{4})-(\d{2})-(\d{2})/)[1];//'2023''2023-10-13'.match(/(\d){4}-(\d{2})-(\d{2})/)[1];//'3'在第一个表达式中,编号为1的分组对应的括号是(\d{4}),其中\d{4}是“匹配4个数字字符”的子表达式。第二个表达式中,编号为1的分组对应的括号是(\d),其中的\d是“匹配1个数字字符”的子表达式。因为之后有量词{4},所以要匹配4次数字字符,而且分组编号都是1。于是每次匹配数字字符,就要重新保存分组编号1匹配的结果。所以在匹配过程中,编号为1的分组匹配文本的值,依次是2、0、2、3,最后的结果是3。
引用分组捕获的文本,不仅仅用于数据提取,也可以用于替换。比如希望将YYYY-MM-DD格式的日期变为MM/DD/YYYY格式,就可以使用正则表达式替换。
在Javascript语言中进行正则表达式替换的方式是str.replace(pattern,replacement),其中str是要进行替换操作的字符串,pattern是用来匹配被替换文本的表达式,replacement是要替换成的文本。例如:
'1a2b3c'.replace(/[a-z]/,'');//'123'在replacement中也可以引用分组。形式是$num,其中的num是对应分组的编号。replacement并不是一个正则表达式,而是一个普通字符串。
“重叠出现”的字母(不考虑大写),取决于第一个[a-z]在运行时的匹配结果,而不能预先设定。也就是说后面的部分必须“知道”前面部分匹配的内容。如果前面的[a-z]匹配的是e,后面就只能匹配e。
引用分组,能引用正则表达式中某个分组内子表达式匹配的文本,但引用都是在匹配完成后进行的,能不能在正则表达式中引用呢?是可以的,这种功能被称为反向引用。
反向引用,它允许在正则表达式内部引用之前捕获分组匹配的文本。其形式也是\num,其中num表示所引用分组的编号。例如:
/^(a-z)\1$/.test('aa');//true需要注意:反向引用,引用的是对应捕获分组匹配的文本,而不是之前的表达式,它本身并不规定文本的特征。例如:
/^([a-z]\1+)$/.test('aa');//true/^([a-z]\1+)$/.test('aaa');//true/^([a-z]\1+)$/.test('aab');//false对分组的引用可能出现在三种场合:
无论是在正则表达式替换时用$num还是在正则表达式内部用\num,都可能遇到二义性的问题:如果出现了\10(或者$10,这里以\num为例),它到底表示第10个捕获分组\10。还是第1个捕获分组\1,之后跟着一个字符0呢?
Javascript中对\num中的num是这样规定的:如果是一位数,则引用对应的捕获分组。如果是两位数且存在对应捕获分组时,引用对应的捕获分组。如果不存在对应的捕获分组,则引用一位编号的捕获分组。
也就是说,如果确实存在编号为10的捕获分组,则\10引用此捕获分组匹配的文本。否则\10表示“第一个捕获分组匹配的文本”和“字符0”。
//存在10分组'0123456789'.replace(/^(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)$/,'$10');//'9'//不存在10分组'012345678'.replace(/^(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)$/,'$10');//'00'有一个问题无法解决:如果存在编号为10的捕获分组,无法用\10表示“编号为1的捕获分组和字符0”,因为此时\10表示的必然是编号为10的捕获分组。
同时捕获分组用数字编号来标识,不够直观,虽然规则是“从左向右按照开括号出现的顺序计数”,但括号多了难免混淆。
为了解决这类问题,一些语言和工具提供了命名分组。命名分组的标识是容易记忆和辨别的名字,而不是数字编号。
这三种用途并不是彼此独立的,而是相互重叠的:单纯的分组可以视为“只包含一个多选分支的多选结构”。整个多选结构也会被视为单个元素,可以由单个量词限定。
最重要的是,无论是否需要引用分组,只要出现了括号,正则表达式在匹配时就会把括号内的子表达式存储起来,提供引用。如果并不需要引用,保存这些信息无疑会影响正则表达式的性能。如果表达式比较复杂,要处理的文本又很多,更可能严重影响性能。
为解决这种问题,正则表达式提供了非捕获分组,非捕获分组类似普通的捕获分组,只是在开括号(后紧跟一个问号和冒号(:...),这样的括号叫做非捕获型括号。它只能限定量词的作用范围,不捕获任何文本。
在引用分组时,分组的编号同样会按开括号出现的顺序从左到右递增,只是必须以捕获分组为准,会略过非捕获分组。例如:
'2023-10-13'.match(/((\d{4})-(\d{2}))-(\d{2})/)[1];//'2023-10''2023-10-13'.match(/(:(\d{4})-(\d{2}))-(\d{2})/)[1];//'2023'非捕获分组不需要保存匹配的文本,整个表达式的效率也因此提高。如果只需要使用括号的分组或者多选结构的功能,而没有用到引用分组,则应当尽量使用非捕获型括号。
之前讲到,如果元字符是单个出现的,直接添加反斜线字符\转义即可。所以*、+、的转义形式分别是\*、\+、\。如果元字符是成对出现的,则有可能只对第一个字符转义,比如{6}和[a-z]的转义分别是\{6}和\[a-z]。
括号的转义与它们都不同,与括号有关的所有三个元字符(、)、|都必须转义。因为括号非常重要,所以无论是开括号还是闭括号,只要出现,正则表达式就会尝试寻找整个括号。如果只转义了开括号而没有转义闭括号,一般会报告“括号不匹配”的错误。另一方面,多选结构中的|也必须转义(多选结构可以不用括号只出现|),所以,也不要忘记对|的转义。例如:
/\(a|b\)/.test('(a|b)');//false/\(a\|b\)/.test('(a|b)');//true3.5.2URLRewrite提到括号的分组和引用功能,就不能不提到URLRewrite。URLRewrite是常见Web服务器中都具备的功能,它用来进行网址的转发。
下面是一个转发的例子:
/^blog\/(\d{4})\/(\d{2})\/$/因为path最后可能出现blog/2006/12/格式,所以使用了量词\/。
以Nginx服务器配置为例:
在Nginx.conf配置文件中找到对应虚拟主机的配置字段,在其中添加下面的规则:
'aaa.bbb.ccc'.match(/(\w+\.)+/);num为0时,表示整个正则表达式匹配的结果。num为1时,表示正则表达式中第一个捕获分组匹配的结果。是否符合你的预期呢?
正则表达式/(\w+\.)+/中存在量词+,所以在整个正则表达式的匹配过程中,括号内的\w+\.会多次匹配,每次匹配,都会更新第1个捕获分组的保存结果。
因此整个表达式匹配结果为aaa.bbb.ccc,第一个捕获分组的匹配结果为ccc。
如果字符串是aaa.bbb,或者aaa.bbb.ccc.ddd。如何能用一个表达式,逐个拆分aaa.、bbb.之类的子串呢?(需要注意的是,子串的个数是变化的,并且不能预先知道。)
要搞清楚这个问题,需要记住:捕获分组的个数是不能动态变化的———单个正则表达式里有多少个捕获分组,一次匹配成功之后,结果中就必然存在多少个对应的元素(捕获分组匹配的文本)。
如果不能预先知道匹配结果中元素的个数,就不能使用捕获分组。如果要匹配数目不定的多段文本,必须通过改变每次匹配的起始位置,多次匹配完成。
具体到以上的问题,使用\w+\.正则表达式,进行重复匹配。
'aaa.bbb.ccc'.match(/\w+\./g);'aaa.bbb.ccc.ddd'.match(/\w+\./g);字符g是正则匹配的模式,表示全局匹配。第一次匹配,从字符串索引为0的位置开始匹配。若是匹配成功,则第二次开始匹配的索引位置等于第一次匹配的索引位置加上匹配文本的长度。依次往下匹配,直到匹配不成功,结束匹配。开始匹配的起始索引再度设置为0。
以'aaa.bbb.ccc'.match(/\w+\./g)例,描述整个匹配过程:
正则表达式中的大多数结构,匹配的文本会出现在最终的匹配结果中。但是也有些结构并不真正匹配文本,而只负责判断在某个位置左/右侧的文本是否符合要求,这种结构被称为断言。
常见的断言有三类:
在文本处理中经常可能进行单词替换,比如把一段文本中的row都替换成line。一般想到的是调用字符串的替换方法,直接替换row。
替换前:Therowwearelookingforisrow10.替换后:Thelinewearelookingforisline10.不过,这样替换也可能会造成意想不到的后果。
替换前:…tomorrowIwillwearinbrownstandinginrow10nexttotherowdyguy…替换后:…tomorlineIwillwearinblinenstandinginline10nexttothelinedyguy…不仅所有单词row都被替换成了line,tomorrow和rowdy两个单词内部的row也被替换成了line,这显然不是我们想要的结果。
要解决这个问题,必须有办法确定单词row,而不是字符串row。
为解决这类问题,正则表达式提供了专用的单词边界,记为\b。
row配合单词边界\b之后的匹配情况,如下所示:
单词边界要求一侧必须出现单词字符,到底什么是单词字符呢?
一般情况下,“单词字符”的解释是\w能匹配的字符。\w只能匹配[0-9a-zA-Z_]。所以\b\w+\b能准确匹配英文单词。
在Web开发中,经常需要对某些单词标记高亮,一般的做法是在单词的前后加上tag。这个功能可以由单词边界配合正则表达式替换完成。例如:
'e-mailM.I.I'.match(/\b[-.\w]+\b/g);//['e-mail','M.I.I']单词边界基本符合以下四种类型:
与单词边界\b对应的还有非单词边界\B,两者的关系类似\s和\S、\w和\W、\d和\D。\B左右两侧都是单词字符。
在同一种语言中,不管\b是如何规定的:
单词边界匹配的是某个位置而不是文本。在正则表达式中,这类匹配位置的元素叫做锚点,它用来“定位”到某个位置。
除了刚才介绍的\b,常用的锚点还有^和$。它们分别匹配字符串的开始位置和结束位置,所以可以用来判断“整个字符串能否由表达式匹配”。
在编辑文本时,敲回车键就输入行终止符,结束当前行,新起一行。不同平台下的行终止符:
为了让换行符“可见”,用[NL]表示。字符串多行模式如下:
一般情况下,^匹配整个字符串的起始位置。
依靠^,就可以用正则表达式^Some准确验证字符串“是否以Some开头”,因为^会把整个表达式的匹配“定位”在字符串的开始位置。
在某些情况下,^也可以匹配字符串内部的“行起始位置”。
如果把匹配模式设定为多行模式,^就既可以匹配整个字符串的起始位置,也可以匹配换行符之后的位置。
如果字符串的末尾出现了行终止符,^也会匹配这个行终止符之后的位置。
//提取每行的第一个单词'firstline\nsecondline\r\n\rlastline'.match(/^\b\w+\b/gm);//['first','second','last']//在每行的开头位置标注$字符'firstline\nsecondline\r\n'.replace(/^/mg,'$');//'$firstline\n$secondline\r$\n$'4.2.2行结束位置一般情况下,$匹配整个字符串的结束位置。
依靠$,就可以用正则表达式line$准确验证字符串“是否以line结尾”,因为$会把整个表达式的匹配“定位”在字符串的结尾位置。
在某些情况下,$也可以匹配字符串内部的“行结束位置”。
如果把匹配模式设定为多行模式,$就既可以匹配整个字符串的结束位置,也可以匹配换行符之前的位置。
如果字符串的末尾出现了行终止符,$既会匹配这个行终止符之前的位置,也会匹配整个字符串的结束位置。
//提取每行的最后一个单词'firstline\nsecondline\r\n\rlastline'.match(/\b\w+\b$/gm);//['line','line','line']//在每行的结束位置标注$字符'firstline\nsecondline\r\n'.replace(/$/mg,'$');//'firstline$\nsecondline$\r$\n$'4.2.3^和$应用非多行模式匹配:
多行模式匹配:
借助^和$完成数据验证
/^\d{6}$/.test('012345');//true/^\d{6}$/.test('a012345b');//false最常用到数据验证的场合就是对用户提交的数据进行验证。
在^和$位置插入字符串内容
借助^和$去除行首尾多余空白字符(为方便识别空白,在行的首尾分别用「和」标识)
这里必须使用多行模式,否则就只能删除整个字符串首尾的空白字符。另一方面,此处使用了量词+而不是*,因为/^\s*/可以不匹配任何字符,这样的“删除”没有意义。将/^\s+/匹配的文本替换为空字符串,就执行了删除操作。(正则表达式应用中没有单独的“删除”操作,删除操作都是通过将文本替换为空字符串实现的)
现在来删行尾的空格,使用表达式/\s+$/,同样要使用多行模式。
能不能用多选结构/(^\s+|\s+$)/并列两个表达式,一步完成呢?是不能的。
多选结构合并多个表达式时,一定要小心未曾预期的后果。有时候,分几步进行反而能省去许多麻烦。
单词边界匹配的是这样的位置:一侧是单词字符,另一侧不是单词字符(可能是非单词字符,也可能没有任何字符)。从另一个角度来看,它能进行这样的判断:在某个位置向左/向右看,必须出现或不是某类字符。
也可以从另外一个角度描述:
针对这种要求,正则表达式专门提供了环视用来“停在原地,四处张望”。环视类似单词边界,在它傍边的文本需要满足某种条件,而且本身不匹配任何字符。
在当前位置:
对于字符串12345,以\d{3}为表达式的四种环视能匹配的位置分别是:右侧必须出现三个数字字符,右侧不能出现三个数字字符,左侧必须出现三个数字字符,左侧不能出现三个数字字符。如下所示:
有时候确实需要用到“原地”的判断,因为要寻找的确实只是位置,而不是真正匹配任何字符,比如格式化数字字符串的格式。
英文中的数字更习惯用逗号分隔以方便阅读,比如12345应该写作12,345。如果用正则表达式来完成任务,就是“把逗号添加到这样的位置:右侧的数字字符串的长度是3的倍数”。看起来只需要使用肯定顺序环视就足够了,用正则表达式找到这样的位置(=(\d{3})+),将它“替换”为逗号。
'12345'.replace(/(=(\d{3})+)/g,',');//,1,2,345结果却不是想象的那样。因为“右侧数字字符串”严格说应该是“当前位置右侧,所有数字字符构成的字符串”。但是(=(\d{3})+)并不能表达这个意思,比如第一个字符1之前的位置,右侧数字字符串长度为5,但其中存在长度为3的子串,所以这个位置可以匹配。同样2、3之前的位置都是如此。
解决这个问题必须配合否定顺序环视,让(\d{3})+能匹配右侧的整个数字字符串,而不能只匹配其中的一个子串。也就是说,要一直匹配到“右侧不再有数字字符的位置”为止。所以,必须将表达式改写为(=(\d{3})+(!\d))。
'12345'.replace(/(=(\d{3})+(!\d))/g,',');//12,345'123456'.replace(/(=(\d{3})+(!\d))/g,',');//,123,456如果字符串的长度正好是3的倍数,还是有问题。
为了整理格式,需要删除这些空白字符。正则表达式匹配空白字符很容易,直接用\s+即可。但如果直接删除\s+能匹配的所有文本。
这个问题其实涉及肯定环视和否定环视的一大根本不同:肯定环视要判断成功,字符串中必须有字符由环视结构中的表达式匹配。而否定环视要判断成功,却有两种情况:字符串中出现了字符,但这些字符不能由环视结构中的表达式匹配。或者字符串中不再有任何字符,也就是说,这个位置是字符串的起始位置或结束位置。
在电子邮件地址中,更准确地进行主机名验证。
根据规范,主机名以点号分隔为多个域名字段,每个域名字段可以包含大小写字母、数字、横线,但是横线不能出现在开头位置。关于长度,每个域名字段的长度最多为63个字符,整个主机名的长度最多为255个字符。通常用的表达式是([-a-zA-Z0-9]{1,63}.)*[-a-zA-Z0-9]{1,63},这个表达式有两个问题:第一,它允许域名字段的第一个字符是横线-。第二,它没有限定整个主机名的长度最长为255个字符。为准确匹配主机名,就必须解决这两个问题。
为保证域名字段第一个字符不能是横线,在表达式开始加上否定顺序环视,即(!-)[-a-zA-Z0-9]{1,63}
为保证整个主机名字符串长度小于255个字符,主机名中全部可能出现的字符都用[-a-zA-Z0-9.]表示,所以对应的肯定顺序环视就是(=[-a-zA-Z0-9.]{0,255})。但是并不能直接把它添加到匹配主机名的整个表达式的开头,因为这个表达式只要求匹配一个长度在255个字符以内的字符串,并不能保证“之后的整个字符串长度在255个字符以内”。如果是单独给出一个字符串,验证它是否是合法的主机名,那么可以在这个环视中的表达式末尾添加$。如果是要从一长段文本中提取出某个主机名,那么主机名之后还有其他字符,只是这些字符不能是[-a-zA-Z0-9.](可能是空白字符,也可能在字符串的末尾,之后没有任何字符),使用否定顺序环视(![-a-zA-Z0-9.])就可以兼顾这两种情况。
还有些时候,可以在匹配的同时以环视施加限制,达到“双管齐下”的效果。比如匹配所有的辅音字母,使用环视写作(![aeiou])[a-z],[a-z]真正匹配的是一个小写字母,但环视(![aeiou])同时要求这个字母不能由[aeiou]匹配,最终效果就是“从26个字母中减去5个辅音字母”。
环视结构也要用到括号,这种括号是否会影响到分组编号呢?
分组的编号只与捕获型括号有关,而不受其他任何类型括号的影响。环视结构虽然必须用到括号字符,但这里的括号只是结构需要,并不影响捕获分组。
单纯的环视结构并不影响引用分组
括号有多种用途,比如表示多选结构。即便括号只表示多选结构,如果没有显式指定为非捕获型括号(:...),也会被视为捕获型括号。
环视结构中出现了捕获型括号,会影响分组
环视结构中指定使用非捕获型括号
常用的语言大都支持环视,但语言不同,支持的程度也不同。一般来说,所有语言都支持两种顺序环视,而且没有限制。也就是说,无论你使用肯定顺序环视,还是否定顺序环视,都可以在其中使用各种复杂的表达式。
Javascript对环视的支持比较复杂。“经典”(ES3~ES6)的Javascript只支持顺序环视,不支持逆序环视。直到ES2017,Javascript中的正则表达式也可以使用逆序环视了。
语言不同,对逆序环视的限制也不相同。逆序环视之所以麻烦,是因为其机制与正常的匹配机制完全不同:它从当前位置开始,由右向左“倒过来”查找可能得匹配。实际的操作过程更像每次从右向左截取一段文本,再判断它能不能由表达式匹配,不行再尝试......这样的过程可能要重复尝试很多次。如果表达式能匹配的文本长度确定,处理的代价很小,否则代价可能很大。所以,比较好的做法是尽量避免在逆序环视中使用复杂的表达式。
环视匹配的并不是字符,而是位置。在正则表达式匹配时,环视结构匹配成功,并不会更改“当前位置”。所以多个环视可以组合在一起,实现在同一个位置的多重判断。
环视中包含环视
最常见的组合是环视中包含环视,比如之前在匹配主机名时,我们限定主机名的长度不能超过255个字符,使用表达式(=[-a-zA-Z0-9.]{0,255}(![-a-zA-Z0-9.]))。其中(![-a-zA-Z0-9.])是包含在外层的环视中的,它要求在这个位置(也就是主机名字符串之后)不能再出现属于主机名字符串的字符,也就是保证之前的表达式匹配整个主机名字符串,而不是“可能的主机名字符串的一部分”。综合起来(=[-a-zA-Z0-9.]{0,255}(![-a-zA-Z0-9.]))保证的是“整个主机名字符串的长度在255个字符以内”。
并列多个环视
并列多个环视,它要求在当前位置,所有环视的判断都必须成功。比如要找到这样的位置:它之后是一个数字字符串,但不能是999开头的数字。这时候,就必须并列两个环视。表示数字字符串的表达式是\d+,对应的环视结构是(=\d+)。表示“不是999开头”的表达式的环视结构是(!999)。现在要做的是把两个环视并列起来,得到(=\d+)(!999)。
因为环视结构不会更改当前位置,所以先后顺序无所谓,无论是(=\d+)(!999)还是(!999)(=\d+),效果是相同的,都要求同时满足下面两个条件:在当前位置,之后必须出现数字字符串。在当前位置,之后不能出现999。最终的结果都是对两个环视做“与运算”,也就说,两个条件必须同时满足才算匹配成功,否则宣告当前位置匹配失败。
最后一种组合是将若干个环视作为多选分支排列在多选结构中。比如要找到这样的位置:它之后要么不是数字字符,要么是一个数字字符和一个非数字字符。“不是数字字符”对应的环视是(!\d)。而“一个数字字符和一个非数字字符”对应的环视是(=\d\D)。所以总的环视就是((!\d))|(=\d\D)。使用多选结构时,列出的多个环视只要有一个成立,整个判断就成功。不使用多选结构时,所有列出的环视都必须成立,整个判断才成功。
断言不匹配任何字符,只匹配位置。而反向引用只引用之前捕获分组匹配的文本,之前捕获分组中锚点表示的位置信息,在反向引用时并不会保留下来。
反向引用时不会保留断言的判断
/(\bcat\b).*\1/.test('catcate');//true/(\bcat\b)\s+\b\1\b/.test('catcate');//false/(\bcat\b)\s+\b\1\b/.test('catcat');//true用反向引用匹配重复单词,应该使用(\b\w+\b)\s+\b\1\b。
需要注意:反向引用,仅引用之前捕获分组匹配的文本内容,而捕获分组中的断言都会被忽略。
所谓匹配模式,指的是匹配时遵循的规则。
设置特定的模式,可能会改变对正则表达式的识别,也可能会改变正则表达式中字符的匹配规定。
常用的匹配模式一共有三种:
匹配一段文本中所有的the,不区分大小写?
可以使用表达式/[tT][hH][eE]/进行匹配。但这样写不够直观,也难以阅读。若是匹配的英文单词足够长,那写起来够麻烦的。
为解决这类问题,正则表达式提供了不区分大小写的匹配模式。指定此模式后,在正则表达式中可以直接写the,就可以匹配the、The、THE等各种大小写形式的the。
模式的指定方式,通常有两种:
模式修饰符即模式名称对应的单个字符,使用时将其填入特定结构(modifier)中(其中modifier为模式修饰符)。几乎所有的语言支持(modifier),但Javascript不支持模式修饰符(modifier)。
另一种指定模式的方式是使用预定义的常量作为参数,传入正则函数。在Python中不区分大小写的预定义常量是re模块的静态成员re.IGNORECASE。但Javascript和PHP不支持,它们的做法是在正则表达式末尾的分隔符之后加上模式对应的字母(比如不区分大小写模式对应的字母是i,则添加字母i)。
两种指定匹配模式的形式,模式修饰符较为通用,因为在各种语言中写法基本相同,而预定义常量在不同语言中写法不同。无论以哪种方式,只要指定了不区分大小写模式,正则表达式在匹配时,就不会区分同一个字母的大小写形式。
/the/i.test('The');//true5.2单行模式元字符点号.能匹配除换行符外的任何字符,等价于:[^\r\n]。但有时候确实需要匹配“任意字符”,比如处理HTML源代码,经常会遇到跨多行的脚本代码。
不过对大多数人来说,点号更自然,也更简洁。所以正则表达式提供了单行模式。在这种模式下,所有文本似乎只在一行里,换行符也是这一行中的“普通字符”,所以可以由点号.匹配。
单行模式对应的模式修饰符是s。
单行模式在不同的语言中称呼也不一样,有叫点号通配(dotAll)的,但约定俗成的称呼是“单行模式”。
在Javascript中指定单行模式的匹配:
/./s.test('\n');//true5.3多行模式“多行模式”与“单行模式”没有任何联系,除了听起来是对应的。
单行模式影响的是点号的匹配规则:在默认模式下,点号.可以匹配除换行符之外的任意字符。但在单行模式下,点号.可以匹配包括换行符在内的任意字符。
多行模式影响的是^和$的匹配规则:在默认模式下,^和$匹配的是整个字符串的起始位置和结束位置。但在多行模式下,它们也能匹配字符串内部换行符之前和之后的位置。
假设,需要找到下面文本中所有数字字符开头的行:
要解决这个问题,需要定位到每行的起始位置,尝试匹配一个数字字符。如果成功,则匹配之后的整行文本。
多行模式的模式修饰符是m。
理解转义的基础是,明白字符串与正则表达式的关系。
通常说的string中,string称为字符串文字,它是某个字符串的值在源代码中的表现形式。比如字符串文字\n,它包含``和n两个字符,意义(或者说它的值)是一个换行符(为方便观察,表示为[NL])。在生成字符串时,应当进行“字符串转义”,才能准确识别字符串文本中\n的意思。
//字符串值在源代码中表现形式'hello\nworld'转义过程:字符串文本(源码)==>“字符串转义”==>字符串(显示效果)
常见字符串转义
在源代码中看到的“正则表达式”regex,其中的regex称为正则表达式文本(以下简称正则文字),是正则表达式的表现形式。比如正则表达式\d,其正则文字包含``和d两个字符,它的意义(或者说值)是匹配数字字符的字符组简记法。在生成正则表达式时需要进行“正则转义”,才能将正则文字中的\d识别为字符组简记法。
不少正则表达式都是以字符串形式提供的,所以必须经过从字符串文字到正则表达式的转换。根据上面的介绍,字符串文字必须首先经过“字符串转义”,才能得到真正的字符串。接下来,这个字符串作为正则文字,经过“正则转义”,最终得到正则表达式。字符串转义与正则转义,如下所示:
常见几个字符的字符串转义和正则转义
正则文本是\n和\t,也就是字符串的值。但是从字符串文字必须经过字符串转义才能得到正则文字的\n和\t,根据字符串转义的规则,反斜线字符``必须写成\。所以字符串文字必须写成\n和\t。
如果要表示正则表达式中的,必须使用正则文字`\`,这样在正则转义时才能正确识别。同时,正则文字中的每个都必须用字符串文字\表示,所以正则表达式``对应的字符串文字就是\\。
newRegExp('\\').test('\');//true有时候情况更复杂:正则表达式中的换行符或者制表符,在字符串文字中必须写成\n或\t。但是,用\n或\t也没有问题。原因在于,在处理字符串转义时,它们已经被解释为换行符或制表符,所以传递给正则表达式的字符串中就包含了换行符或者制表符。
newRegExp('\n').test('\n');//truenewRegExp('\n').test('\n');//truenewRegExp('\t').test('\t');//truenewRegExp('\t').test('\t');//true要特别注意的是\b。在一般字符串中,\b是预定义的转义序列,表示退格符(backspace,为方便观察,表示为[BS])。但是在正则表达式中,它表示单词边界(记为\b)。如果在字符串文字中写\b,字符串转义为退格符,作为正则文本转义为正则表达式,正则表达式真正得到的就是退格符,而不是单词边界。所以,如果用到了单词边界,在字符串文字中一定要写成\b。所以,最保险的办法是:正则表达式中的每个``,在字符串文字中都要写成\。
\b的转义
newRegExp('\ba\b').test('a');//falsenewRegExp('\ba\b').test('a');//true这样看来,使用字符串形式的正则表达式,转义的处理确定比较复杂。最好是能省去这些麻烦——正则表达式是怎样,正则文字中就是怎样写。要做到这一点,有两种办法:
第一,使用原生字符串,也就是完全忽略字符串转义的特殊字符串。例如,Javascript中提供的原生模版字符串:
newRegExp(String.raw`\n`).test('\n');//truenewRegExp(String.raw`\`).test('\');//true第二,直接使用正则文字。例如,Javascript正则字面量:(推荐做法)
/\n\/.test('\n\');//true如果必须使用字符串文字,请尽量坚持这条原则:正则表达式中的每个反斜线``,在字符串文字中都必须写成\,只有\n、\t中的反斜线例外(但是\n和\t也不难理解)。
元字符是有特殊含义的字符,如果要匹配“元字符”自身则必须转义,也就是在元字符之前添加反斜线``。比如元字符点号.,可以匹配除换行符外的任何字符,如果要准确匹配字符串中的点号.,正则表达式中就必须写成.。
也有些时候,匹配元字符自身并非一定要转义。
字符组只要转义开方括号
字符组内部的闭方括号]在任何情况下都要转义**,否则类似[]]的正则表达式会出现二义性,造成识别错误。所以能匹配字符a、字符b、字符]的字符组,应当写为[ab]]。同样,括号内部的任何闭括号)都要转义,比如包含ab和b)的多选结构的正则表达式就应当写为(ab|b))。
字符组有自己的元字符规定,也有相应的转义规定:在字符组内部,只有三个字符需要转义。
/[0]9]/.test(']');//true/[0-9]/.test('-');//false/[-09]/.test('-');//true/[^09]/.test('0');//false/[0^9]/.test('0');//true/[0^9]/.test('^');//true6.2表达式中的优先级正则表达式的元素之间的组合关系只有4种。
注:“普通拼接”可能是最常见的组合关系,正则表达式abc就是a、b和c的普通拼接,a(bc)则可以看作a和(bc)的拼接。
正则表达式的优先级
正则表达式中的优先级举例
表达式ab*(cd|e+)|fg优先级:
专注于原创短更,便于碎片化涉猎知识。希望我走过的路,留下的痕迹,能对你有所启发和帮助。