{m,n} 是通用形式的量词,正则表达式还有三个常用量词,分别是 、?、*。它们的形态虽然不同于 {m,n},功能却是相同的,因此也可以把它们理解为“量词简记法”。具体说明见下表。
常用量词 | {m,n}等价形式 | 说明 |
---|---|---|
* | {0,} | 出现零次、一次或多次 |
| {1,} | 出现至少一次 |
? | {0,1} | 出现至多一次 |
一、贪心、懒惰和占有
量词自身是贪心的。贪心量词会首先匹配整个字符串。尝试匹配时,它会选定尽可能多的内容,也就是整个输入。量词首次尝试匹配整个字符串,如果失败则回退一个字符后再次尝试。这个过程叫做回溯(backtracking)。它会每次回退一个字符,直到找到匹配的内容或者没有字符可尝试为止。此外,它还记录所有的行为,因此相较另两种方式它对资源的消耗最大。
懒惰(有时也说勉强)量词则使用另一种策略。它从目标的起始位置开始尝试寻找匹配,每次检查字符串的一个字符,寻找它要匹配的内容。最后,它会尝试匹配整个字符串。要使一个量词成为懒惰的,必须在普通量词后添加一个问号 ?。
占有量词会覆盖整个目标然后尝试寻找匹配内容,但它只尝试一次,不会回溯。占有量词就是在普通量词之后添加一个加号 。
二、用 *、 和 ? 进行匹配
这些量词默认是贪心的,这意味它们在第一次尝试时会尽可能多地匹配字符。
代码语言:javascript复制drop table if exists t1;
create table t1 (a varchar(10));
insert into t1 values
('1'),('22'),('333'),('4444'),('55555'),('666666'),
('7777777'),('88888888'),('999999999'),('0000000000');
.* 匹配任何字符零次或多次,因此会以贪心的方式匹配所有行。
代码语言:javascript复制mysql> select regexp_substr(a,'.*') from t1;
-----------------------
| regexp_substr(a,'.*') |
-----------------------
| 1 |
| 22 |
| 333 |
| 4444 |
| 55555 |
| 666666 |
| 7777777 |
| 88888888 |
| 999999999 |
| 0000000000 |
-----------------------
10 rows in set (0.01 sec)
9* 匹配 0个到多个9,因此会以贪心的方式匹配所有行。数字9的行完全匹配,其它行匹配空串。
代码语言:javascript复制mysql> select distinct regexp_substr(a,'9*') from t1;
-----------------------
| regexp_substr(a,'9*') |
-----------------------
| |
| 999999999 |
-----------------------
2 rows in set (0.01 sec)
9.* 匹配 9 后面跟着任何字符零次或多次,因此包含数字9的行,其它行不匹配,因此返回NULL,注意不是空串!
代码语言:javascript复制mysql> select distinct regexp_substr(a,'9.*') from t1;
------------------------
| regexp_substr(a,'9.*') |
------------------------
| NULL |
| 999999999 |
------------------------
2 rows in set (0.00 sec)
9 匹配 1个到多个9,结果包含数字9的行。
代码语言:javascript复制mysql> select distinct regexp_substr(a,'9 ') from t1;
-----------------------
| regexp_substr(a,'9 ') |
-----------------------
| NULL |
| 999999999 |
-----------------------
2 rows in set (0.00 sec)
9? 匹配 0个或1个9。数字9的行只匹配第一个字符9,其它行匹配空串。
代码语言:javascript复制mysql> select distinct regexp_substr(a,'9?') from t1;
-----------------------
| regexp_substr(a,'9?') |
-----------------------
| |
| 9 |
-----------------------
2 rows in set (0.00 sec)
99? 匹配 9后面跟0个或1个9。数字9的行匹配前两个字符9,其它行不匹配。
代码语言:javascript复制mysql> select distinct regexp_substr(a,'99?') from t1;
------------------------
| regexp_substr(a,'99?') |
------------------------
| NULL |
| 99 |
------------------------
2 rows in set (0.00 sec)
三、匹配特定次数
使用花括号可以限制某个模式在某个范围内匹配的次数,未经修饰的量词就是贪心量词。例如 7{1} 会匹配第一次出现的7。
代码语言:javascript复制mysql> select regexp_substr(a,'7{1}') from t1 where a regexp '7{1}';
-------------------------
| regexp_substr(a,'7{1}') |
-------------------------
| 7 |
-------------------------
1 row in set (0.01 sec)
要匹配一个或多个数字7,只要加一个逗号即可。
代码语言:javascript复制mysql> select regexp_substr(a,'7{1,}') from t1 where a regexp '7{1,}';
--------------------------
| regexp_substr(a,'7{1,}') |
--------------------------
| 7777777 |
--------------------------
1 row in set (0.00 sec)
7 和 7{1,} 本质上是一样的;7* 和 7{0,}、7? 和 7{0,1} 也是一样的。还可以匹配 m到n 次,比如 7{3,5} 会匹配三个、四个以及五个7。
代码语言:javascript复制mysql> select regexp_substr(a,'7{3,5}') from t1 where a regexp '7{3,5}';
---------------------------
| regexp_substr(a,'7{3,5}') |
---------------------------
| 77777 |
---------------------------
1 row in set (0.00 sec)
可以看出,花括号(或者说范围语法)是最灵活和精确的量词。
四、懒惰量词
理解懒惰特性最好的方式就是看看实际应用,尝试用问号 ? 匹配零个或者一个5。
代码语言:javascript复制mysql> select distinct regexp_substr(a,'5?') from t1 where a regexp '5?';
-----------------------
| regexp_substr(a,'5?') |
-----------------------
| |
| 5 |
-----------------------
2 rows in set (0.01 sec)
请再加一个 ? 来使量词变为懒惰的。
代码语言:javascript复制mysql> select distinct regexp_substr(a,'5??') from t1 where a regexp '5??';
------------------------
| regexp_substr(a,'5??') |
------------------------
| |
------------------------
1 row in set (0.00 sec)
现在它看起来只匹配空串,其原因是该模式已经是懒惰的了。也就是说,它不会强制匹配第一个5。懒惰的基本特性就是匹配尽可能少的字符。试一下匹配零次或多次的量词。
代码语言:javascript复制mysql> select distinct regexp_substr(a,'5*?') from t1 where a regexp '5*?';
------------------------
| regexp_substr(a,'5*?') |
------------------------
| |
------------------------
1 row in set (0.01 sec)
它也只匹配空串,因为它可以选择匹配最少的次数——零次。再试一下匹配一次或多次。
代码语言:javascript复制mysql> select distinct regexp_substr(a,'5 ?') from t1 where a regexp '5 ?';
------------------------
| regexp_substr(a,'5 ?') |
------------------------
| 5 |
------------------------
1 row in set (0.00 sec)
懒惰特性使其只匹配了一个5。它只需要做到这个程度就可以了。使用 m和n 方式匹配时就更为有趣了。
代码语言:javascript复制mysql> select distinct regexp_substr(a,'5{2,5}?') from t1 where a regexp '5{2,5}?';
----------------------------
| regexp_substr(a,'5{2,5}?') |
----------------------------
| 55 |
----------------------------
1 row in set (0.00 sec)
只匹配了两个5,而不像贪心量词那样匹配五个。
下表列出了懒惰量词。什么时候懒惰式匹配最实用?如果想匹配最少而不是最多数目的字符,就可以使用懒惰量词。
语法 | 说明 |
---|---|
?? | 懒惰匹配零次或一次 |
? | 懒惰匹配一次或多次 |
*? | 懒惰匹配零次或多次 |
{n}? | 懒惰匹配n次 |
{n,}? | 懒惰匹配n次或多次 |
{m,n}? | 懒惰匹配m至n次 |
五、占有量词
占有式匹配很像贪心式匹配,它会选定尽可能多的内容。但与贪心式匹配不同的是它不进行回溯。它不会放弃所找到的内容,这也是把它称为占有式(possessive)的原因。占有量词的优点是速度快,因为无需回溯。当然,匹配失败的话也很快。
为了理解这一点,我们先尝试匹配以零开头的多个零。
代码语言:javascript复制mysql> select distinct regexp_substr(a,'0.* ') from t1 where a regexp '0.* ';
-------------------------
| regexp_substr(a,'0.* ') |
-------------------------
| 0000000000 |
-------------------------
1 row in set (0.00 sec)
匹配了所有的零。占有式的匹配看起来和贪心式的匹配是一样的,但没有回溯。可以证明一下,输入带有结尾零的表达式。
代码语言:javascript复制mysql> select distinct regexp_substr(a,'.* 0') from t1 where a regexp '.* 0';
Empty set (0.01 sec)
没有匹配——原因就是没有回溯。它一下就选定了所有的输入,不再回过来查看。它一下子没在结尾找到零,也不知道该从哪里找起。如果将加号去掉,它会找到所有的0,因为它变回贪心式匹配了。
代码语言:javascript复制mysql> select distinct regexp_substr(a,'.*0') from t1 where a regexp '.*0';
------------------------
| regexp_substr(a,'.*0') |
------------------------
| 0000000000 |
------------------------
1 row in set (0.00 sec)
当知道文本中的内容就知道在哪里可以找到匹配时,应该会使用占有量词。它不在乎是否会选定所有内容。占有式匹配有助于提高匹配的性能。下表列出了占有量词。
语法 | 说明 |
---|---|
? | 占有匹配零次或一次 |
| 占有匹配一次或多次 |
* | 占有匹配零次或多次 |
{n} | 占有匹配n次 |
{n,} | 占有匹配n次或多次 |
{m,n} | 占有匹配m至n次 |
六、示例——括号字符串计数
有一张表 t1 存储用户评论内容,如下所示(只列出相关列):
现在想得出每种评论字数的个数,每个字符包括标点、空格、表情符号都算一个字,但每对中括号连同其中的内容只算一个字。对于上面的数据行,结果为:
第一感觉这是使用正则表达式的场景。只要将每对中括号连同其中的内容替换为单个字符,再用char_length函数求长度即可。查询语句如下:
代码语言:javascript复制select char_length(regexp_replace(Content,'\[.*?\]', 'A')) r,count(*)
from t1 group by char_length(regexp_replace(Content,'\[.*?\]', 'A'))
order by r;
\[ 和 \] 用于将中括号转义为普通字符。非 dotall 模式下的正则表达式中,“.”表示匹配除换行符 n 之外的任何单字符,“*”表示零次或多次。所以 “.*” 连在一起就表示任意字符出现零次或多次。没有“?”表示贪婪模式。比如 a.*b,它将会匹配最长的以 a 开始,以 b 结束的字符串。如果用它来搜索 aabab 的话,它会匹配整个字符串 aabab。又比如模式src=`.*`, 它将会匹配以 src=` 开始,以`结束的最长的字符串。用它来搜索 <img src=``test.jpg` width=`60px` height=`80px`/> 时,将会返回 src=``test.jpg` width=`60px` height=`80px`。
“?”跟在“*”后边用时,表示懒惰模式,就是匹配尽可能少的字符。这就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。a.*?b 匹配最短的以 a 开始,以 b 结束的字符串。如果把它应用于 aabab 的话,它会匹配 aab(第一到第三个字符)和 ab(第四到第五个字符)。又比如模式 src=`.*?`,它将会匹配 src=` 开始,以 ` 结束的尽可能短的字符串,且开始和结束中间可以没有字符,因为 * 表示零到多个。用它来搜索 <img src=``test.jpg` width=`60px` height=`80px`/> 时,将会返回 src=``。