在前面的几个章节中,我们简单的学习了一些基本的正则表达式的一些元素,今天,我们来讨论一下Java 正则表达式重要的一个概念–Quantifiers(量词).
啥为量词?从字面意义上可能和数量有关,其实Java 正则里面的量词被用来需要指定某种模式需要重复出现一定次数的情况,比如:我们想匹配100个连续的a
, 当然了,你可以写100个a
的正则表达式来达到同样的功能,但是,这种也太辛苦了,我们程序员是很懒的。所以学习Quantifier相关知识,可以少搬一点砖<_
在Java的Pattern类的java doc中其描述了三种类型的,下面列举三种的内容:
Greedy | Reluctant | Possessive | Meaning | 翻译 |
---|---|---|---|---|
X? | X?? | X?+ | X, once or not at all | X字符出现一次或者没有匹配 |
X* | X*? | X*+ | X, zero or more times | X字符出现0次或者多次 |
X+ | X+? | X++ | X, one or more times | X字符至少重复出现1次 |
X{n} | X{n}? | X{n}+ | X, exactly n times | X字符重复n次 |
X{n,} | X{n,}? | X{n,}+ | X, at least n times | X字符至少重复n次 |
X{n,m} | X{n,m}? | X{n,m}+ | X, at least n but not more than m times | X字符至少出现n次,但不会超过m次 |
上面几种类型对应的中文描述是: Greedy–贪婪型(最大匹配) Reluctant–勉强型(最小匹配), Possessive—占有型(全部匹配),在后续内容我将继续使用英文。
咋眼一看,X?
, X??
和X?+
做着同样的事情,它们都是用来匹配字符X出现一次或者没有出现
, 其实这个问题开始学习确实烦的要死,后面的部分说明它们之间的细微差别。
这里让大家了解一下*
,?
和+
这几个Metacharater的意思,下面几个实验中,带匹配的空字符串,即”“.
通过上面的几个例子,我们简单列举它们具体的意义:
1. *
:字符出现的次数>=0;
2. ?
: 字符出现的次数要么是1,要么是0次;
3. +
:字符出现的次数>=1;
这个概念有点奇怪,长度为0的匹配是什么鬼?请大家之前说的匹配start index和 end index.可以通过下面的图了解一下:
上面是某次匹配的结果,匹配的子串是原字符串索引1到3为止,不包括3,所以这里面的end index指的是下一次继续寻找匹配子串的起始索引。
那这个和我们现在要说的零长度匹配有什么关系吗? 认真一点的可能会看到上面的头两个示例中,出现了start index = 0 和end index也为0的情况,这个怎么解释呢?
首先我们要了解一下a?
和a*
这两个表达式都允许字符a
出现0次的情况,考虑到我们输入的是空字符串,字符串的长度为0, 这时候他们去匹配0长度的时候,没有出现字符a
,说明也满足上面两个正则表达式的条件,这种类型的匹配我们称之为“零长度匹配”,它们特征也很明显,就是start index和 end index相等。
这里大概可能有疑问了,空字符串不是长度为0吗?你这里匹配index=0,按道理应该说明字符串是长度为一的字符串啊!!!
是时候表演真正的技术了,这里注意一下end index的意义,上面我有提到它表示下一次匹配的开始,这个时候匹配过程就是去尝试匹配 end index,这时候我们start index就应该调整到end index, 但是匹配的结果让人很心酸—start index没有数据,这时候匹配引擎回溯,当然这时候回溯后的位置还是start index,此时,构造出的是空白,然而这种空白符合a?
和a*
的意,这就是为啥end index = start index, 此时匹配过程结束,原因在于start index已经>= 字符串的长度了,说明已经没有的匹配了。
下面我们在通过几个示例来感受这波骚操作:
下面我将输入字符串长度变大,大家可以看到各个元字符的差异性了:
下面我们再尝试一些非纯a
字符的情况:
解释: abcaad,
第一次匹配a, 成功,此时打印Found [a] starting at 0 and ending at 1;第二次匹配b, 失败,但是匹配引擎回溯,此时start index = end index,这样就构成了空串,正则
a?
匹配这种情况,所以打印: Found [a] starting at 1 and ending at 1;第三次匹配c,此时start index为2, 为啥是2不是1呢?原因在于 index = 1的位置我们已经匹配过,不要因为匹配引擎回溯,而以为start index = 1, 这里start index应该表示上次已经匹配过的字符索引位置的下一位,即没有匹配过的位,现在匹配d字符,同匹配b一样,打印:*Found [a] starting at 2 and ending at 2;
第四次匹配字符a, 匹配成功,则输出: *Found [a] starting at 3 and ending at 4;
第五次匹配字符a, 匹配成功,则输出: *Found [a] starting at 4 and ending at 5;
第六次匹配字符d,匹配失败,则输出: Found [a] starting at 5 and ending at 5;
第七次匹配字符d,匹配失败,则输出: Found [a] starting at 6 and ending at 6;
下面再给出两个例子:
某些情况下,我们希望字符重复出现的次数可控,这是我们可以用大括号表达自己期望的次数,{min, max}.
这个存在三种变形:
Format | Meaning |
---|---|
{num} | 重复num次 |
{min,} | 至少重复num次 |
{min, max} | 重复次数在min<= 重复次数 <= max |
下面给出几个示例:
上面的学习中,我们仅仅将量词放在单个字符情况,下面我们来讨论量词用在Capturing Groups 和 Character classes的情况.
量词用在Capturing Groups和Character Classes时,我们将它们当做一个整体来看待,下面通过例子来了解:
上面的例子在匹配(dog){3}
时,将括号里面的dog看做一个整体来,即dog字符串得重复出现三次,也就是dogdogdog
,
然而dog{3}
我们这里的{3}修饰的g
字符,它匹配的字符是doggg
这种情况.
下面我们来了解一下Character Classes的情况:
对于[abc]{3}
表示重复出现的三个字符,可以是字符a
, b
或c
中任意一个,所以匹配到可能的子串为: Character Classes Length * repeat Num
Greedy: 从字面意义来看它是贪婪,想一口气吃一个大胖子,它在匹配的时候,总是先整个字符串匹配,匹配不了,它在回退,先看个例子:
上面的例子中.*foo
先将整个字符串进行匹配,结果发现模式匹配,就返回整个字符串作为结果.
reluctant: 表示最小匹配,它总是小口小口的吃,可以通过例子比较一下区别:
Possessive:表示最大匹配,而且它就匹配一下,匹配不成功,匹配引擎不会回溯的,可以通过如下例子了解:
这个为啥不匹配呢?原因在于对于.*+foo
而言,.*+
匹配完了整个字符,当它继续匹配foo的时候发现找不到了。因为此时没有可供匹配的字符的,故返回”Not Found”。
大概可能就要问了这个Possessive有什么使用场景呢?这个通常用在严格模式上面的匹配,比如邮箱匹配之类的场景。
完!
上一节: 【Java正则表达式系列】5. 预定义Character classes
下一节: 【Java正则表达式系列】7 Capturing Groups(匹配组)