【读书】正则指引-3-括号

前端之家收集整理的这篇文章主要介绍了【读书】正则指引-3-括号前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

分组

小括号(以下称为括号)可以提供的第一个功能就是分组(grouping)
通过之前的学习,可以知道量词用于限定之前元素的出现次数,这个元素可能是一个字符,也可能是一个字符组,还可能是一个表达式。如果把一个表达式用括号包围起来,就形成了括号内的表达式,而这种表达式通常被称为“子表达式”。
换句话说,子表达式就是利用括号分组功能形成的表达式。
比如,(ab)+表示希望字符串ab重复出现一次以上。(对比ab+的含义)
分组的作用是可以准确表达“长度只能是m或 n”的语义。
分组功能使用场景:
  • 身份证号码的准确匹配 -- ^[1-9]\d{14}(\d{2}[0-9x])?$
  • Open tag的(较)准确匹配 -- ^<[^/]([^>]*[^/])?>$
  • Web 服务中的 URL rewrite 功能 -- 略
  • E-mail地址匹配 -- ^[-\w]{0,64}@([-a-zA-Z0-9]{1,63}\.)*[-a-zA-Z0-9]{1,63}$
上述每一项的匹配规则,以及后文中其他正则匹配规则,请自行参考《正则指引》书中说明。

多选结构

解决15和18位身份证匹配问题其实有两种思路。第一种(上面给出的正则表达式),是将18位号码多出的3位“合并”到匹配15位号码的表达式中。第二种,则是直接将15位和18位身份证号分开处理。同样还是使用括号,但使用的是括号的第二个功能多选结构(alternative)
多选结构的形式是(…|…),即在括号内以竖线|分隔开多个子表达式,这些子表达式也叫做多选分支。在一个多选结构内,多选分支的数目没有限制。在匹配时,整个多选结构被视为单个元素,只要其中某个子表达式能够匹配,整个多选结构的匹配就成功。如果所有子表达式都不能匹配,则整个多选结构匹配失败。
所以,身份证号码匹配的第二种方式为:([1-9]\d{14}|[1-9]\d{14}\d{2}[0-9x])
多选结构使用场景:
  • 匹配IP地址的一段 -- ([0-9]|[0-9]{2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])
  • 匹配手机号 -- (0|\+86)?(13[0-9]|15[0-356]|18[025-9])\d{8}
  • 匹配月、日、小时、分钟 -- 略
  • tag的准确匹配 -- <(‘[^’]*’|”[^”]*”|[^’”>])+>
关于多选结构的补充说明:
  • 多选结构的一般表示法是(option1|option2),一般会同时使用括号()和竖线|,但是如果没有括号,只出现竖线,仍然是正确的多选结构。在多选结构中,竖线用来分隔多选结构,而括号用来规定整个多选结构的范围,如果没有出现括号,则将整个表达式视为一个多选结构。所以ab|cd等价于(ab|cd)。推荐明确写出两端的括号,可以更加形象,也能避免一些隐晦的错误(因为竖线的优先级很低)。
  • 多选分支并不等于字符组,虽然看起来类似字符组,如[abc]能匹配的字符串和(a|b|c)一样。从理论上讲,可以完全用多选结构来替换字符组,但这种做法不被推荐。理由:首先,[abc]比(a|b|c)简洁,字符组有范围表示法;其次,大多数情况下,[abc]比(a|b|c)的效率要高的多。反过来,多选结构不一定能对应到字符组。因为字符组的每个“分支”的长度相同,而且只能是单个字符;而多选结构的每个“分支”的长度没有限制,甚至可以是复杂的表达式,比如(abc|b+c*ab),字符组对这种匹配完全无能为力。
  • 字符组有排除型字符组,但多选结构却没有对应的结构来表达该语义。
  • 多选分支的排列是有讲究的。例如,表达式(jeff|jeffrey),用它匹配jeffrey,结果是jeff还是jeffrey呢?这个问题其实没有标准答案,大多数语言的多选结构都会按照优先选择最左侧的分支来进行匹配。
  • 在平时使用中,如果出现多选结构,应当尽量避免多选分支中存在重复匹配,因为这样会大大增加回溯的计算量。

引用分组

括号不仅仅能把有联系的元素归拢起来并分组,还有其他作用,即使用括号之后,正则表达式会保存每个分组真正匹配的文本,等到匹配完成后,可以通过group(num)之类的方法“引用”分组在匹配时捕获的内容。其中num表示对应的编号,括号分组的编号规则是从左向右计数,从1开始。这就是括号的第三个功能捕获分组(capturing group)。这种括号叫做捕获型括号

一般来说,正则表达式匹配完成之后,都会得到一个表示“匹配结果”的对象,对它调用获取分组的方法,传入分组编号num,就可以得到对应分组匹配的文本。num的编号是从1开始的,不过编号为0的分组也是默认存在的,其对应整个表达式匹配的文本。
关于括号存在嵌套的问题:无论括号如何嵌套,分组的编号都是根据开括号出现顺序来计数的,开括号是从左向右起第多少个开括号,整个括号分组的编号就是多少。

引用分组是使用场景:
  • 提取HTML中的所有超链接的地址和文本 -- <a\s+href\s*=\s*[“’]?([^”’\s]+)[“’]?>([^<]+)</a>
  • 提取HTML中的标题相关内容 -- <head>([^<]+)</head>
  • 提取HTML中的图片链接相关内容 -- <img\s+[^>]*?src=[‘”]?([^”’\s]+)[‘”]?[^>]*>
新手容易犯的错误
提取日期 2010-12-22 这个内容时,正确的正则表达式为
(\d{4})-(\d{2})-(\d{2})
错误的表达式可能被写成
(\d){4}-(\d{2})-(\d{2})

引用分组捕获的文本,不仅仅用于数据提取,也可以用于替换
在各种语言中都有类似substitute(pattern,replacement,string)这种形式的正则替换命令。其中replacement处可以使用引用分组,形式是\num(有些语言中为$num,下文同,本文中只写作\num),其中num为对应分组的编号。同时要明白replacement处为普通字符串,在某些语言中,需要特别指明replacement使用原生字符串才行,否则\num这种引用分组在普通字符串中会被当做不合法的转义序列处理。
另外,如果想在replacement中引用整个表达匹配的文本,不能使用\0,即便使用原生字符串也不行。一种变通策略是给整个表达式加上一对括号,之后使用\1来引用。

反向引用

反向引用(back-reference)主要用于检测重叠出现的内容,即允许在正则表达式内部引用之前的捕获分组匹配的文本。其使用形式也是\num。
反向引用的使用场景:
  • 检查某个单词是否包含重叠出现的字母 -- ^([a-z])\1$
  • 解析HTML代码时匹配简单tag -- <([^>]+)>[\s\S]*?</\1>
  • 解析HTML代码时匹配复杂tag -- <([a-zA-Z0-9]+)(\s[^>]+)?>[\s\S]*?</\1>
  • 处理中文文本 -- 略
注意:反向引用重复的是对应捕获分组匹配的文本,而不是之前的表达式,也就是说,反向引用的是由之前表达式决定的具体文本,而不是符合某种规则的未知文本。

各种引用的记法

上面说过,对分组进行引用的时候使用\num这类表示法,同时还说过,还有另外一些语言使用了不同的表示方式。如下图所示

一般情况下,我们认为$num要好于\num。原因在于,$0可以准确表示“第0个分组(也就是整个表达式匹配的文本)”,而\0则不行,因为不少语言的字符串中,\num本身可能就是一个有意义的转义序列,表示值为num的ASCII码字符,所以\0会被解释成“ASCII编码为0的字符”。

二义性问题:无论采用\num还是$num进行引用都会遇到二义性问题,如果出现了\10,它到底表示第10个捕获分组\10,还是第1个捕获分组\1之后跟着一个字符0?
各种语言的解决方式有所不同,Python和PHP中提供了特定的表示法来专门解决这类问题(细节略)。而Java、Ruby和JavaScript中则是通过制定规则来解决这类问题的:如果是一位数,则引用对应的捕获分组;如果是两位数且存在对应捕获分组时,引用对应的捕获分组,如果不存在对应的捕获分组,则引用一位数编号的捕获分组。
简单思考一下就可以发现,后面这几种语言的策略存在一个问题无法解决:如果存在编号为10的捕获分组,无法用\10表示“编号为1的捕获分组和字符0”,因为此时\10表示的必然是编号为10的捕获分组。在实际开发中,尤其是进行文本替换时有时确实会遇到这个问题,在现有的规则下是无解的。所幸的是,一般情况下,我们根本不会用到太多的捕获分组(即基本上用不到\10以上的分组);另外,已经有越来越多的语言提供了命名分组,它可以彻底解决这个问题。

命名分组

使用数字编号来标识捕获分组存在不够直观,括号多了容易搞错序号,引用时不够方便的问题。所以,一些语言和工具提供了命名分组(named grouping),可以将它看成另一种捕获分组。
命名分组在每种语言中各有不同,此处不做展开。另外,由于历史原因和后向兼容性,即便使用了命名分组,每个命名分组同时也具有数字编号,其编号规则没有变化。

非捕获分组

前面介绍了括号的三种主要用途,需要指出的是,各种用途间不是彼此独立的,而是互相重叠的:单纯的分组可以视为“只包含一个多选分支的多选结构”;整个多选结构也会被视为单个元素,可以由单个量词限定。最重要的是,无论是否需要引用分组,只要出现了括号,正则表达式在匹配时就会把括号内的子表达式存储起来,提供引用。如果并不需要引用,保存这些信息无疑会影响正则表达式的性能;如果表达式比较复杂,要处理的文本又很多,更可能严重影响性能
解决这种问题,正则表达式提供了非捕获分组(non-capturing group),非捕获分组类似普通的捕获分组,只是在开括号后紧跟一个问号和冒号(?:…),这样的括号叫做非捕获型括号。它的作用只是限定量词的作用范围,不捕获任何文本。在引用分组时,分组的编号同样会按照开括号出现的顺序从左向右递增,只是必须以捕获分组为准,非捕获分组会略过。

补充

括号的转义

括号的转义要求和括号相关的所有三个元字符(、)、|都必须转义\(、\)、\|。

URL Rewrite

URL Rewrite是常见web服务器都具备的功能,用来进行网址的转发。例子如下

外部访问 URL
http://www.example.com/blog/2006/12
内部实现
http://www.example.com/blog/posts.PHP?year=2006&month=12

这样重写的好处是隔离了外部接口和内部实现,方便修改;也有利于提供更有意义、更直观的URL。
一般来说,URL Rewrite都是使用转发规则实现的,每条转发规则对应一类URL,以正则表达式解析并提取出所有需要的信息,重组之后再转发。
学习URL Rewrite的使用可以参考当前的各类主流Web服务器的配置,此处略。

总结

本篇将《正则指引》的第三章内容进行了概括总结,下次讲解正则表达式中的断言。

参考资料

《正则指引》:余晟。

猜你在找的正则表达式相关文章