正则表达式必知必会 - 位置匹配

2023-10-14 09:51:49 浏览数 (2)

一、边界

        位置匹配用于指定应该在文本中什么地方进行匹配操作,先来看一个例子。

代码语言:javascript复制
mysql> set @s:='The cat scattered his food all over the room.';
Query OK, 0 rows affected (0.00 sec)

mysql> set @r:='cat';
Query OK, 0 rows affected (0.00 sec)

mysql> select regexp_extract(@s, @r, '') s, regexp_extract_index(@s, @r, 0, '') i;
 --------- ------ 
| s       | i    |
 --------- ------ 
| cat,cat | 5,10 |
 --------- ------ 
1 row in set (0.00 sec)

        模式 cat 可以匹配文本里所有的 cat,即便是单词 scattered 里的那个 cat 也不例外。但这很可能并不是想要的结果。如果这样搜索所有的cat,并将其替换为dog,那么得到的只会是毫无实际意义的一句话。这就要用到边界了,也就是一些用于指定模式前后位置(或边界)的特殊元字符。

二、单词边界

        第一种边界,也是最常用到的,是由 b 指定的单词边界。b 是英文 boundary 的首字母,顾名思义,b 用来匹配一个单词的开头或结尾。

代码语言:javascript复制
mysql> set @r:='\bcat\b';
Query OK, 0 rows affected (0.00 sec)

mysql> select regexp_extract(@s, @r, '') s, regexp_extract_index(@s, @r, 0, '') i;
 ------ ------ 
| s    | i    |
 ------ ------ 
| cat  | 5    |
 ------ ------ 
1 row in set (0.00 sec)

        单词 cat 的前后都有一个空格,所以匹配模式 bcatb,空格是用来分隔单词的字符之一。该模式并不匹配单词 scattered 中的字符序列 cat,因为它的前一个字符是s、后一个字符是t,这两个字符都不能与 b 相匹配。

        b 到底匹配什么东西呢?正则表达式引擎不懂任何人类语言,所以也不知道什么是单词边界。简单地说,b 匹配的是字符之间的一个位置:一边是单词(能够被 w 匹配的字母数字字符和下划线),另一边是其他内容(能够被 W 匹配的字符)。重要的是要认识到,如果想匹配一个完整的单词,就必须在要匹配的文本的前后都加上 b。

代码语言:javascript复制
mysql> set @s:='The captain wore his cap and cape proudly as
    '> he sat listening to the recap of how his
    '> crew saved the men from a capsized vessel.';
Query OK, 0 rows affected (0.00 sec)

mysql> set @r:='\bcap';
Query OK, 0 rows affected (0.00 sec)

mysql> select regexp_extract(@s, @r, '') s, regexp_extract_index(@s, @r, 0, '') i;
 ----------------- ------------- 
| s               | i           |
 ----------------- ------------- 
| cap,cap,cap,cap | 5,22,30,113 |
 ----------------- ------------- 
1 row in set (0.00 sec)

        模式 bcap 匹配任何以字符序列 cap 开头的单词。这里总共找到了 4 个匹配,其中有 3 个都不是独立的单词 cap。下面这个例子里的文本还是刚才那段文字,但在这次的正则表达式里只有一个 b 后缀。

代码语言:javascript复制
mysql> set @r:='cap\b';
Query OK, 0 rows affected (0.00 sec)

mysql> select regexp_extract(@s, @r, '') s, regexp_extract_index(@s, @r, 0, '') i;
 --------- ------- 
| s       | i     |
 --------- ------- 
| cap,cap | 22,72 |
 --------- ------- 
1 row in set (0.00 sec)

        模式 capb 匹配以字符序列 cap 结束的任意单词。这里总共找到了 2 个匹配,其中有一个不是独立的单词 cap。如果只想匹配单词 cap 本身,那么正确的模式应该是 bcapb。

        b 匹配的是一个位置,而不是任何实际的字符。用 bcatb 匹配到的字符串的长度是 3 个字符(c、a、t),不是 5 个字符。如果不想匹配单词边界,那么可以使用 B。下面的例子使用 B 来查找前后都有多余空格的连字符。

代码语言:javascript复制
mysql> set @s:='Please enter the nine-digit id as it
    '> appears on your color - coded pass-key.';
Query OK, 0 rows affected (0.00 sec)

mysql> set @r:='\B-\B';
Query OK, 0 rows affected (0.00 sec)

mysql> select regexp_extract(@s, @r, '') s, regexp_extract_index(@s, @r, 0, '') i;
 ------ ------ 
| s    | i    |
 ------ ------ 
| -    | 60   |
 ------ ------ 
1 row in set (0.00 sec)

        B-B 将匹配一个前后都不是单词边界的连字符。nine-digit 和 pass-key 中的连字符不能与之匹配,但 color - coded 中的连字符可以与之匹配,因为空格和连字符都不属于w。同一个元字符的大写形式与它的小写形式在功能上往往刚好相反。

三、字符串边界

        单词边界可以用来对单词位置进行匹配,如单词的开头、单词的结尾、整个单词等。字符串边界有着类似的用途,只不过用于在字符串首尾进行模式匹配。字符串边界元字符有两个:^ 代表字符串开头,$ 代表字符串结尾。

        有些元字符拥有多种用途,^ 就是其中之一。只有当它出现在字符集合里,即位于 [ 和 ] 之间,且紧跟在左方括号的后面时,它才表示排除该字符集合。如果出现在字符集合之外并位于模式的开头,^ 将匹配字符串的起始位置。为了演示字符串边界的用法,下面准备了一个例子。有效的 XML 文档都必须以 <?xml> 标签开头,另外可能还包含一些其他属性,比如版本号,如<?xml version="1.0" ?>。下面这个简单的测试可以检查一段文本是否为 XML 文档。

代码语言:javascript复制
mysql> set @s:='<?xml version="1.0" encoding="UTF-8" ?>
    '> <wsdl:definitions targetNamespace="http://tips.cf"
    '> xmlns:impl="http://tips.cf" xmlns:intf="http://tips.cf"
    '> xmlns:apachesoap="http://xml.apache.org/xml-soap"';
Query OK, 0 rows affected (0.00 sec)

mysql> set @r:='<\?xml.*\?>';
Query OK, 0 rows affected (0.00 sec)

mysql> select regexp_extract(@s, @r, '') s, regexp_extract_index(@s, @r, 0, '') i;
 ----------------------------------------- ------ 
| s                                       | i    |
 ----------------------------------------- ------ 
| <?xml version="1.0" encoding="UTF-8" ?> | 1    |
 ----------------------------------------- ------ 
1 row in set (0.00 sec)

        该模式似乎管用。<?xml 匹配 <?xml,.* 匹配随后的任意文本(.的零次或多次重复出现),?> 匹配结尾的 ?>。但是,这个测试非常不准确。在下面的例子里,采用同样的模式来匹配在 <?xml> 标签之前包含额外内容的文本。

代码语言:javascript复制
mysql> set @s:='This is bad, real bad!
    '> <?xml version="1.0" encoding="UTF-8" ?>
    '> <wsdl:definitions targetNamespace="http://tips.cf"
    '> xmlns:impl="http://tips.cf" xmlns:intf="http://tips.cf"
    '> xmlns:apachesoap="http://xml.apache.org/xml-soap"';
Query OK, 0 rows affected (0.00 sec)

mysql> select regexp_extract(@s, @r, '') s, regexp_extract_index(@s, @r, 0, '') i;
 ----------------------------------------- ------ 
| s                                       | i    |
 ----------------------------------------- ------ 
| <?xml version="1.0" encoding="UTF-8" ?> | 24   |
 ----------------------------------------- ------ 
1 row in set (0.00 sec)

        模式 <?xml.*?> 匹配到的是第 2 行文本。因为 XML 文档的起始标签出现在了第 2 行,所以这肯定不是有效的 XML 文档,将其作为 XML 文档来处理会导致各种问题。这里需要的测试是能够确保 XML 文档的起始标签 <?xml> 出现在字符串最开始处,而这正是 ^ 元字符大显身手的地方。

代码语言:javascript复制
mysql> set @s:='<?xml version="1.0" encoding="UTF-8" ?>
    '> <wsdl:definitions targetNamespace="http://tips.cf"
    '> xmlns:impl="http://tips.cf" xmlns:intf="http://tips.cf"
    '> xmlns:apachesoap="http://xml.apache.org/xml-soap"';
Query OK, 0 rows affected (0.00 sec)

mysql> set @r:='^\s*<\?xml.*\?>';
Query OK, 0 rows affected (0.00 sec)

mysql> select regexp_extract(@s, @r, '') s, regexp_extract_index(@s, @r, 0, '') i;
 ----------------------------------------- ------ 
| s                                       | i    |
 ----------------------------------------- ------ 
| <?xml version="1.0" encoding="UTF-8" ?> | 1    |
 ----------------------------------------- ------ 
1 row in set (0.00 sec)

        ^ 匹配一个字符串的开头位置,所以 ^s* 匹配字符串的开头和随后的零个或多个空白字符,这解决了<?xml>标签前允许出现的空格、制表符、换行符的问题。作为一个整体,模式 ^s*<?xml.*?> 不仅能匹配带有任意属性的 XML 起始标签,还可以正确处理空白字符。

        虽然模式 ^s*<?xml.*?> 解决了上例中的问题,但那只是因为这个例子里的 XML 文档并不完整而已。如果采用完整的 XML 文档,就会看到贪婪型量词的典型表现。所以,这个例子很好地说明了什么时候该使用 .*? 代替 .*。$ 的用法也差不多,它可以用来检查 Web 页面结尾的 </html>

0 人点赞