学习正则表达式 - 量词

2023-10-14 09:48:24 浏览数 (3)


        {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=``。

1 人点赞