前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >正则表达式必知必会 - 使用子表达式

正则表达式必知必会 - 使用子表达式

作者头像
用户1148526
发布2023-10-14 09:52:10
1640
发布2023-10-14 09:52:10
举报
文章被收录于专栏:Hadoop数据仓库Hadoop数据仓库

一、理解子表达式

        假设需要找出所有重复的 HTML 不间断空格,将其用其他内容替换。

代码语言:javascript
复制
mysql> set @s:='Hello, my name is Ben Forta, and I am
    '> the author of multiple books on SQL (including
    '> MySQL, Oracle PL/SQL, and SQL Server T-SQL),
    '> Regular  Expressions, and other subjects.';
Query OK, 0 rows affected (0.00 sec)

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

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

          是 HTML 中不间断空格的实体引用(entity reference)。模式  {2,} 应该匹配连续两次或更多次重复出现的 ,结果却事与愿违。为什么会这样?因为{2,}指定的重复次数只作用于紧挨着它的前一个字符,在本例中,那是一个分号。如此一来,该模式可以匹配 ;;;;,但无法匹配  。

二、使用子表达式进行分组

        这就引出了子表达式的概念。子表达式是更长的表达式的一部分,划分子表达式的目的是为了将其视为单一的实体来使用。子表达式必须出现在字符 ( 和 ) 之间。( 和 ) 是元字符,如果需要匹配 ( 和 ) 本身,就必须使用转义序列 \( 和 \)。

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

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

        ( ) 是一个子表达式,它被视为单一的实体。因此,紧随其后的 {2,} 将作用于整个子表达式,而不仅仅是分号。再来看一个例子,这次是用一个正则表达式来查找 IP 地址。IP 地址的格式是以英文句号分隔的 4 组数字,例如 12.159.46.200。因为每组可以包含 1~3 个数字字符,所以这 4 组数字可以统一使用模式 \d{1,3} 来匹配。

代码语言:javascript
复制
mysql> set @s:='Pinging hog.forta.com [12.159.46.200]
    '> with 32 bytes of data:';
Query OK, 0 rows affected (0.00 sec)

mysql> set @r:='\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}';
Query OK, 0 rows affected (0.00 sec)

mysql> select regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s, regexp_extract_index(@s, @r, 0, '') i;
+------+---------------+------+
| c    | s             | i    |
+------+---------------+------+
|    1 | 12.159.46.200 | 24   |
+------+---------------+------+
1 row in set (0.00 sec)

        每个 \d{1,3} 匹配 IP 地址里的一组数字。4 组数字之间由 . 分隔,因此,在正则表达式中要转义为 \.。在这个例子里,模式 \d{1,3}\.(最多匹配3个数字字符和随后的.)连续出现了3次,所以同样可以用重复来表示。下面是同一个例子的另一种写法。

代码语言:javascript
复制
mysql> set @r:='(\\d{1,3}\\.){3}\\d{1,3}';
Query OK, 0 rows affected (0.00 sec)

mysql> select regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s, regexp_extract_index(@s, @r, 0, '') i;
+------+---------------+------+
| c    | s             | i    |
+------+---------------+------+
|    1 | 12.159.46.200 | 24   |
+------+---------------+------+
1 row in set (0.01 sec)

        该模式与之前那个有着同样的效果,但这次使用了另一种语法。将表达式 \d{1,3}\. 放入 ( 和 ) 之中,使其成为一个子表达式。(\d{1,3}\.){3} 表示该子表达式重复出现 3 次(它们对应着 IP 地址里的前 3 组数字),随后的 \d{1,3} 用来匹配 IP 地址里的最后一组数字。

        有些用户喜欢把表达式的某些部分加上括号,形成子表达式,以此提高可读性,因此,上面的模式可以写成 (\d{1,3}\.){3}(\d{1,3})。这种做法完全没有问题,对表达式的实际行为也没有任何不良影响(但根据具体的正则表达式实现,这可能会影响性能)。利用子表达式进行分组非常重要,有必要再来看一个例子,它完全不涉及重复次数问题。下面的例子尝试匹配用户记录中的年份。

代码语言:javascript
复制
mysql> set @s:='ID: 042
    '> SEX: M
    '> DOB: 1967-08-17
    '> Status: Active';
Query OK, 0 rows affected (0.00 sec)

mysql> set @r:='19|20\\d{2}';
Query OK, 0 rows affected (0.00 sec)

mysql> select regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s, regexp_extract_index(@s, @r, 0, '') i;
+------+------+------+
| c    | s    | i    |
+------+------+------+
|    1 | 19   | 21   |
+------+------+------+
1 row in set (0.00 sec)

        这个例子需要构造模式去查找 4 位数的年份,但为了更加准确,明确地将前两位数字限定为 19 和 20。模式里的 | 是 OR(或)操作符,19|20 可以匹配19或20,因此,模式 19|20\d{2} 应该匹配以 19 或 20 开头的 4 位数字(19或20的后面再跟着两位数字)。显然,这样并不管用。因为 | 操作符会查看其左右两边的内容,将模式 19|20\d{2} 解释为 19 或 20\d{2},也就是把 \d{2} 解释为以 20 开头的那个表达式的一部分。换句话说,它匹配的是数字 19 或以 20 开头的任意 4 位数字。最终的结果只匹配到了19。正确答案是把 19|20 划分为一个子表达式。

代码语言:javascript
复制
mysql> set @r:='(19|20)\\d{2}';
Query OK, 0 rows affected (0.00 sec)

mysql> select regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s, regexp_extract_index(@s, @r, 0, '') i;
+------+------+------+
| c    | s    | i    |
+------+------+------+
|    1 | 1967 | 21   |
+------+------+------+
1 row in set (0.00 sec)

        把选项全都归入一个子表达式里,这样 | 就知道打算匹配的是出现在分组中的选项之一。(19|20)\d{2} 因此正确地匹配到了 1967,其他以 19 或 20 开头的 4 位年份数字自然也得以匹配。对于再往后的一些日期(从现在算起100年内),要是需要修改这段代码,使其也能够匹配以21开头的年份,只要把这个模式改成(19|20|21)\d{2}就可以了。

三、子表达式的嵌套

        子表达式允许嵌套。事实上,子表达式还可以多重嵌套。子表达式嵌套能够构造出功能极其强大的正则表达式,但这难免会让模式变得难以阅读和理解,多少有些让人望而却步。其实大多数嵌套子表达式并没有它们看上去那么复杂。为了演示嵌套子表达式的用法,再去看看刚才那个匹配 IP 地址的例子。

        IP 地址由 4 个字节构成,每组数字的取值范围也就是单个字节的描述范围,即 0~255。(\d{1,3}\.){3}\d{1,3} 这个模式能匹配 345、700、999 这些无效的 IP 地址数字。有一点很重要。写一个能够匹配预期内容的正则表达式并不难。但是写一个能够考虑到所有可能场景,确保将不需要匹配的内容排除在外的正则表达式可就难多了。

        如果有办法设定有效的取值范围,事情会简单得多,但正则表达式只是匹配字符,并不真正了解这些字符的含义。所以就别指望数学运算了。有没有别的办法呢?也许有。在构造一个正则表达式的时候,一定要定义清楚想匹配什么,不想匹配什么。一个有效的 IP 地址中每组数字必须符合以下规则。

  • 任意的 1 位或 2 位数字。
  • 任意的以 1 开头的 3 位数字。
  • 任意的以 2 开头、第二位数字在 0 到 4 之间的 3 位数字。
  • 任意的以 25 开头、第三位数字在 0 到 5 之间的 3 位数字。

        当依次罗列出所有规则之后,模式该是什么样子就变得一目了然了。

代码语言:javascript
复制
mysql> set @s:='Pinging hog.forta.com [12.159.46.200]
tract_index(@s, @r, 0, '') i;
    '> with 32 bytes of data:';
Query OK, 0 rows affected (0.00 sec)

mysql> set @r:='(((25[0-5])|(2[0-4]\\d)|(1\\d{2})|(\\d{1,2}))\\.){3}(((25[0-5])|(2[0-4]\\d)|(1\\d{2})|(\\d{1,2})))';
Query OK, 0 rows affected (0.00 sec)

mysql> select regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s, regexp_extract_index(@s, @r, 0, '') i;
+------+---------------+------+
| c    | s             | i    |
+------+---------------+------+
|    1 | 12.159.46.200 | 24   |
+------+---------------+------+
1 row in set (0.00 sec)

        该模式成功的原因要归功于一系列嵌套子表达式。先来说说由 4 个子表达式构成的 (((25[0-5])| (2[0-4]\d)|(1\d{2})|(\d{1,2}))\.)。(\d{1,2}) 匹配任意的一位或两位数字(0~99);(1\d{2}) 匹配以 1 开头的任意 3 位数字(100~199);(2[0-4]\d) 匹配数字200~249;(25[0-5]) 匹配数字250~255。每个子表达式都出现在括号中,彼此之间以 | 分隔,意思是只需匹配其中某一个子表达式即可,不用全都匹配。随后的 \. 用来匹配 . 字符,它与前 4 个子表达式合起来又构成了一个更大的子表达式(4 组数字选项和 \.),接下来的 {3} 表示该子表达式匹配到的内容要重复 3 次。最后,数值范围又重复出现了一次,这次省略了尾部的 \.,用来匹配 IP 地址里的最后一组数字。通过把每组数字的取值范围都限制在 0 到 255 之间,这个模式准确无误地做到了匹配有效的 IP 地址,排除无效的 IP 地址。值得注意的是,这 4 个表达式如果按照更符合逻辑的顺序书写,反倒是不行的。

代码语言:javascript
复制
mysql> set @r:='(((\\d{1,2})|(1\\d{2})|(2[0-4]\\d)|(25[0-5]))\\.){3}((\\d{1,2})|(1\\d{2})|(2[0-4]\\d)|(25[0-5]))';
Query OK, 0 rows affected (0.00 sec)

mysql> select regexp_count(@s, @r, '') c, regexp_extract(@s, @r, '') s, regexp_extract_index(@s, @r, 0, '') i;
+------+--------------+------+
| c    | s            | i    |
+------+--------------+------+
|    1 | 12.159.46.20 | 24   |
+------+--------------+------+
1 row in set (0.00 sec)

        注意,这次未能匹配结尾的 0。为什么会这样?因为模式是从左到右进行评估的,所以当有 4 个表达式都可以匹配时,首先测试第一个,然后测试第二个,以此类推。只要有任何模式匹配,就不再测试选择结构中的其他模式。在本例中,(\d{1,2}) 匹配结尾的 200 中的 20,因此后面其他模式都没有进行评估。

        像上面这个例子里的正则表达式看起来挺吓人的。理解的关键是要将其分解开,每次只分析一个子表达式,把它搞明白。按照先内后外的原则来进行,而不是从头开始,逐个字符地去阅读。嵌套子表达式其实远没有看上去那么复杂。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-05-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、理解子表达式
  • 二、使用子表达式进行分组
  • 三、子表达式的嵌套
相关产品与服务
云数据库 MySQL
腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档