靜下心来--重温正则表达式(二)

2023-11-06

   在 靜下心来–重温正则表达式(一)这篇文章中,我们重点介绍了正则表达式的一些基础概念,以及在 String、RegExp 的原型上涉及到正则表达式常用 4 个的方法( repalce、 match、test、exec),最后介绍了正则表达式的两种匹配方式:贪婪匹配、惰性匹配。
  以上这些部分都是针对正则表达式的内容的匹配,并没有涉及到位置的匹配。在这篇文章中,重点介绍下针对位置的匹配:断言匹配,以及正则版本的匹配原理:回溯法,最后介绍一些正则表达式的优化。

1、零宽断言匹配

   “零宽断言”听起来很古怪,满足:字你都认识,连起来就不明白是什么意思。

用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像\b,^,$那样用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们也被称为零宽断言。

   断言:可以理解为正则表达式可以指明在指定的内容的前面或后面会出现满足指定规则的内容。
   零宽:就是没有宽度,在正则表达式中,断言只是匹配位置,不占字符,也就是说,匹配结果里是不会返回断言本身。
   看到这里,有没有这种感觉:断言就是在正则表达式中写 if 判断条件。 由于可以匹配位置的前后,满足匹配内容和不满足匹配内容,交叉组合后共有四种断言。

名称 表达式 最终结果与位置关系 是否满足匹配内容pattern 兼容性
正向先行断言 (?=pattern) 在断言位置前 -
负向先行断言 (?!pattern) 在断言位置前 -
正向后行断言 (?<=pattern) 在断言位置后 IE、Safari不支持
负向后行断言 (?<!pattern) 在断言位置后 IE、Safari不支持

注意:

  1. 需要重点注意,后行断言存在浏览器兼容性问题,具体可以查阅 RegExp 兼容性
  2. 零宽断言只作为条件匹配位置,最终的匹配结果中并不返回零宽断言匹配的内容==

1.1、 正向先行断言 (?=pattern)

   正向先行断言,英文名称为 positive lookahead。其具体形式如下:

/pattern1(?=pattern2)/

 其中 pattern1、 pattern2 都是由元字符、限定符、修饰符、原子组等组成的正则表达式。 pattern1 : 匹配内容、 pattern2 : 匹配位置

   通俗的将,正向先行断言:首先满足 pattern2,并获取匹配位置,在此位置之前匹配 pattern1,最终结果返回 pattern1 匹配的内容。

const str = 'abc123abc12345abc1234'
const regx = /abc/g
let result = null 
while((result = regx.exec(str)) !== null ){
  console.log(result)
}

// ['abc', index: 0, input: 'abc123abc12345abc1234', groups: undefined]
// ['abc', index: 6, input: 'abc123abc12345abc1234', groups: undefined]
// ['abc', index: 14, input: 'abc123abc12345abc1234', groups: undefined]


const regx = /abc(?=12345)/g
let result = null 
while((result = regx.exec(str)) !== null ){
  console.log(result)
}

// ['abc', index: 6, input: 'abc123abc12345abc1234', groups: undefined]

   如果没有增加零宽断言,则在上例中,就会有 3 个 abc 的字符串被匹配成功。加上零宽断言 (?=12345),意味着只能匹配 12345 前边的 abc。上例中,最终返回的结果只有 abc (零宽断言不作为内容返回),匹配的位置 index 是 6, 也说明只有第二个 abc 满足零宽断言的条件。

1.2、 负向先行断言 (?!pattern)

   负向先行断言,英文名称为 negative lookahead 。其具体形式如下:

/pattern1(?!pattern2)/

 其中 pattern1、 pattern2 都是由元字符、限定符、修饰符、原子组等组成的正则表达式。 pattern1 : 匹配内容、 pattern2 : 匹配位置

   正向先行断言是寻找满足 pattern2 的位置,而负向先行断言则相反,寻找的是不满足 pattern 的位置。这是二者唯一的区别。

const str = 'abc123abc12345abc1234'
const regx = /abc(?!12345)/g
let result = null 
while((result = regx.exec(str)) !== null ){
  console.log(result)
}

// ['abc', index: 0, input: 'abc123abc12345abc1234', groups: undefined]
// ['abc', index: 14, input: 'abc123abc12345abc1234', groups: undefined]

   还是相同的例子,如果改为负向先行断言的话,则匹配的结果是:不是 12345 前边的 abc(同样零宽断言不作为内容返回)。

1.3、 正向后行断言 (?<=pattern)

   上边讨论的都是先行断言,也就是匹配条件之前的内容。接下来 2 部分来讨论下后行断言,后行断言则匹配的是条件之后的内容。先来看看正向后行断言。

/(?<=pattern2)pattern1/

 其中 pattern1、 pattern2 都是由元字符、限定符、修饰符、原子组等组成的正则表达式。 pattern1 : 匹配内容、 pattern2 : 匹配位置

   正向后行断言:首先满足 pattern2,并获取匹配位置,在此位置之后匹配 pattern1,最终结果返回 pattern1 匹配的内容。

const str = 'abc123abc12345abc1234'
const regx = /(?<=12345)abc/g
let result = null 
while((result = regx.exec(str)) !== null ){
  console.log(result)
}

// ['abc', index: 14, input: 'abc123abc12345abc1234', groups: undefined]

   还是上边的例子,这次返回的结果是匹配:12345 后边的 abc, 最终返回的是最后一个 abc (同样零宽断言不作为内容返回)

1.4、 负向后行断言 (?<!pattern)

   了解到正向后行断言之后,就能很好的理解负向后行断言。

/(?<!pattern2)pattern1/

 其中 pattern1、 pattern2 都是由元字符、限定符、修饰符、原子组等组成的正则表达式。 pattern1 : 匹配内容、 pattern2 : 匹配位置

   负向后行断言:首先不满足 pattern2,并获取匹配位置,在此位置之后匹配 pattern1,最终结果返回 pattern1 匹配的内容。

const str = 'abc123abc12345abc1234'
const regx = /(?<!12345)abc/g
let result = null 
while((result = regx.exec(str)) !== null ){
  console.log(result)
}

// ['abc', index: 0, input: 'abc123abc12345abc1234', groups: undefined]
// ['abc', index: 6, input: 'abc123abc12345abc1234', groups: undefined]

   同样,还是上边的例子,这次返回的结果是匹配:不是12345 后边的 abc, 最终返回的是前 2 个 abc (同样零宽断言不作为内容返回)。其中第一个 abc 前边没有内容同样满足不是 12345 这个条件。

1.5 小结

   零宽断言就是位置和条件的组合:

  1. 先行断言(?= 、?!)匹配条件之前的内容,后行断言(?<=、 ?<!)匹配条件之后的内容。
  2. 正向断言(?= 、?<=)匹配的是满足的条件的情况,负向断言(?!、?<!)匹配的时候不满足条件的情况。

   另外有2点需要注意:

  1. 零宽断言仅做位置匹配,不做为内容返回;
  2. 后行断言存在浏览器兼容性问题。

2、匹配原理 - 回溯法

回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

2.1、贪婪匹配的回溯

   在上一章节中介绍过,正则表达式默认时贪婪匹配,会尽可能多的匹配,这可能出现尽可能多的匹配的时候,会导致后续的匹配无法完成,就需要通过回溯来解决。

const str = '"abc"de'
const regx = /".*"/

贪婪匹配-回溯.png

【注】:图片来源于JavaScript正则表达式迷你书

   在正则表达式中的 . 匹配除换行符以外的任意字符,* 匹配任意次数,贪婪模式下 .* 组合起来会一直匹配到文本的行末尾。如图所示在第 1 步 匹配到 " 之后,从第 2 步到第 8 步,一直匹配到文本结尾,但此时正则表达式无法匹配成功,因此回溯到上一节点, 也就是第 9 步回到第 7 步,发现还是不能匹配成功,继续回溯,直到第 11 步 回溯到第 5 步的状态,然后继续匹配 ", 最终匹配成功。 具体的匹配过程也是查看:https://regex101.com/r/XuBOzX/1

贪婪匹配-回溯- regx101.png

   有上例可知,由于 .* 导致了贪婪匹配,使得正则表达式发生了多次回溯,会非常影响正则表达式的匹配效率。

2.2、惰性匹配的回溯

   大部分的回溯是由贪婪匹配带来,但也并不是说惰性匹配不会带来回溯。在上一文章中,介绍惰性匹配的实例如下:

const string = "12345";
const regex = /^(\d{1,3}?)(\d{1,3})$/;
// => ['12345', '12', '345', index: 0, input: '12345', groups: undefined]
//知道你不贪、很知足,但是为了整体匹配成,没办法,也只能给你多塞点了。因此最后 \d{1,3}? 匹配的字
//符是 "12",是两个数字,而不是一个。

惰性匹配 - 回溯.png

【注】:图片来源于JavaScript正则表达式迷你书

   首先匹配开始位置(^),由于第一个原子组是惰性匹配,因此只匹配了一个数字,紧接着第二个原子组贪婪匹配了3个数字,此时正则表达式匹配结束位置,数字 5 无法满足。因为第二个原子已经匹配到最多内容,都无法满足正则表达式匹配成功,因此需要回溯第一个原子组,为了满足正则表达式匹配成功,因此只能让第一个原子组多匹配一些内容。具体的匹配过程可以参考: https://regex101.com/r/erceTr/1

惰性匹配 - 回溯-regx101.png

2.3、分支的回溯

   分支也是惰性匹配的,比如 /can|candy/,去匹配字符串 “candy”,得到的结果是 “can”,因为分支会一个一个尝试,如果前面的满足了,后面就不会再试验了。分支结构,可能前面的子模式会形成了局部匹配,如果接下来表达式整体不匹配时,仍会继续尝试剩下的分支。这种尝试也可以看成一种回溯。看如下实例:

const string = "candy";
const regex = /^(?:can|candy)$/;  // candy

分支回溯.png

【注】:图片来源于JavaScript正则表达式迷你书

   其匹配过程如下,由于第一个分支,只能局部匹配 can ,但不能满足正则表达式匹配成功(因为还需要满足匹配起始位置和结束位置),于是匹配过程就回溯到第二个分支,具体的匹配过程可以查看: https://regex101.com/r/flTRP5/1

分支回溯-regx-101.png

3、正则表达式的优化

   日常开发工作中,有2个需要在编码过程中需要考虑的。首先是准确性,也就是能够实现预期的功能;其次就要考虑一下代码执行的效率。正则表达式的编写也同样要考虑这 2 个问题,关于准确性掌握好前边所述内容,基本问题不大,此处不再赘叙。
   由于正则表达式通过回溯匹配会引发一些效率问题,因此并不是所有的问题都必须用正则表达式去处理,能够是使用字符串 API 快速解决的问题,就不该正则出马。如果必须使用正则表达式来完成,那么接下来的几点优化建议,或许能排上用场

3.1、常见的优化方法

  1. 使用具体型字符组来代替通配符,来消除回溯

  在 2.1章节(贪婪匹配的回溯)中由于使用了 .* , 使得正则表达式在匹配过程中发生了多次回溯。如果能够改用正则表达式:/“[^”]*"/,就能够消除不必要的回溯。

  1. 使用非捕获型分组

  在前边的章节中多次提到了原子组,可以用于正则表达式在匹配的过程中捕获子表达式的匹配结果,同时通过反向引用可以复用之前的原子组,原子组捕获的数据需要占用内存来保存它们。实际过程中,编写正则表达式时,有时候添加括号并不是为了需要捕获原子组内容,而是为了可读性,此时可以通过: (?:pattern) 的方法来取消原子组的捕获,具体实例可以参考前边章节。

  1. 提取分支公共部分

   在 2.3章节(分支的回溯)中,由于前边分支的局部匹配,使得整个匹配过程回溯到第二个分支,而第二个分支中含有第一个分支的局部内容,这部分内容在第二个分支中需要重新匹配一次,因此可以提取分支公共部分。如上例可以修改为:
/^can(?:dy)?$/ 。 又如 /http|https/ 可以修改为 /https?/ , /red|read/ 可以修改成 /rea?d/

  1. 出现可能性大的放左边

  由于正则是从左到右匹配的,把出现概率大的放左边,域名中 .com 的使用是比 .net 多的,所以我们可以写成 .(?:com|net),而不是 .(?:net|com)

3.2、测试工具

   最后给大家推荐 2 个好用的正则表达式网站

   第一个是上文中有用到: https://regex101.com/,可以测试正则表达式的匹配性能,匹配结果,以及调试正则表达式的匹配过程。

   第二个是: https://regexper.com/,可以通过可视化的方式快速展示你的正则表达式。

4、后记

   通过 2 篇文章,我们重温了正则表达式的知识,包括正则表达式的基础概念、匹配方式、断言匹配、匹配原理以及部分优化策略。不同语言使用的正则引擎可能会不同,这个也会带来匹配效率、性能上差异,主要有 DFA (确定型有限自动机)和 NFA (非确定型有限自动机),有兴趣的同学可以自行研究下。也希望这 2 篇文章能够对大家有所帮助,如有不正之处,敬请指正。


参考文献:

  1. https://github.com/qdlaoyao/js-regex-mini-book/blob/master/JavaScript正则表达式迷你书(1.1版).pdf
  2. https://juejin.cn/post/6844903677119954958#heading-1
  3. https://juejin.cn/post/6844903680349585422#heading-0
  4. https://juejin.cn/post/7021672733213720613#heading-23
  5. https://www.bilibili.com/video/BV12J41147fC
  6. https://blog.csdn.net/ybdesire/article/details/78255427
  7. https://www.jianshu.com/p/fb3afbf8da10
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

靜下心来--重温正则表达式(二) 的相关文章

随机推荐

  • Eclipse 快捷健

    查询 F3 全局 打开声明 Ctrl G 工作区中的声明 Ctrl shift G 查看变量或方法在工作区的引用 Ctrl Alt H Call Hierarchy 查找出该工程所有调用了该成员变量或方法 Ctrl H 打开搜索对话框 Ct
  • PyQt5 界面预览工具

    简介 一款为了预览PyQt5设计的UI界面而开发的工具 使用时需要结合PyCharm同时使用 下载 PyQt5界面预览工具 参数说明 使用配置 启动PyCharm 找到File gt Settings 打开 找到Tools gt Exter
  • [Java基础]Java中boolean类型到底占用多少个字节?

    1 时间 2017 07 03 07 37 06 YuanMxy 2 问题描述 今天在复习java基础的时候发现一小问题 Java中boolean类型到底占用多少个字节 3 问题解答 1 什么是boolean类型 根据官方文档的描述 htt
  • 在aps.net开发时,改变页面对应的js代码后,重新加载页面后js代码没有加载为最新版本?

    例如页面引用js文件的代码为 在开发人员修改完js代码后 发现重新加载页面时新的js代码不会生效 这是因为页面在向js发出请求时 浏览器发现js文件名和参数没有变化 所以默认 加载了缓存中存在的js代码 可以这样解决这个问题 将页面引用js
  • idea重写接口没有@override_乐字节Java继承|方法重写、super和final关键字

    大家好 乐字节的小乐又来了 上一篇是 乐字节Java JavaBean 继承与权限修饰 也是属于Java继承的 今天继续Java继承 一 方法的重写 父类不满足子类的要求 按需改写 注意 方法签名必须相同 在子类中可以根据需要对从基类中继承
  • 如何进行云主机迁移?看这一篇文章就够了!

    欢迎大家前往腾讯云 社区 获取更多腾讯海量技术实践干货哦 本文由腾讯云计算产品团队发表于云 社区专栏 主机迁移概述 在云计算时代 不管是从IDC上云还是多云直接的迁移 都已经是常见的事宜 而在上云 迁移的方案中 也是有多种的方式能够将主机迁
  • 【颜纠日记】win10开启高性能超频模式,你不知道的N种方法。

    颜纠日记 1 启用游戏模式 Win10 中调整游戏性能最简单的方法 就是启用游戏模式 开启游戏模式 可以通过停止 Windows 更新和一些应用程序的后台活动 来提高游戏帧数 如果不确定是否开启了游戏模式 可以转到 Win I 游戏 游戏模
  • 3D游戏(2)——离散仿真引擎基础

    文章目录 1 简答题 解释 游戏对象 GameObjects 和 资源 Assets 的区别与联系 下载几个游戏案例 分别总结资源 对象组织的结构 指资源的目录组织结构与游戏对象树的层次结构 编写一个代码 使用 debug 语句来验证 Mo
  • Android:WebView加载url网页显示不完整解决办法

    WebView基本用法 如果想要在APP里面加载url网页 或者html代码 首先我们会想到WebView 它的基本用法如下 webview layout xml
  • 基于redis实现延时队列(一)

    背景 最近项目中需要对一条数据 在半小时候更改其状态 类似于提交商城订单半小时后未支付的订单需要更改为超时状态 当然这个解决方案有很多 最好的解决方案是用MQ的死信队列 但由于项目中没有引入MQ 故本文采用的是基于redis与定时器实现该需
  • docker error creating overlay mount to invalid argument 解决方法

    问题原因 由于docker的不同版本在centos上产生的mount问题 1 2 x没有出现这个问题 当使用yum install时 安装的最新版本 1 3 x 会导致overlay2的错误 核心解决方案 etc sysconfig doc
  • Spring Security Oauth2 认证(获取token/刷新token)流程(password模式)

    https blog csdn net bluuusea article details 80284458 1 本文介绍的认证流程范围 本文主要对从用户发起获取token的请求 oauth token 到请求结束返回token中间经过的几个
  • easypoi导出word换行处理

    内容包含换行符 n 导出word时换行符失效 会将换行符 n识别为空格 模板 导出结果 maven
  • Android获取当前时间与星期几 .

    public class DataString private static String mYear private static String mMonth private static String mDay private stat
  • js 判断数组元素是否存在重复项

    方法一 定义测试的数组 1个没有重复元素 1个有重复元素 var arr1 new Array 111 333 222 444 var arr2 new Array aa cc bb dd bb function checkSameElem
  • Java实现一个简单的图书管理系统(内有源码)

    简介 哈喽哈喽大家好啊 之前作者也是讲了Java不少的知识点了 为了巩固之前的知识点再为了让我们深入Java面向对象这一基本特性 就让我们完成一个图书管理系统的小项目吧 项目简介 通过管理员和普通用户的两种操作界面 利用其中的方法以及对象之
  • 光流法介绍

    光流场法的基本思想 在空间中 运动可以用运动场描述 而在一个图像平面上 物体的运动往往是通过图像序列中不同图像灰度分布的不同体现的 从而 空间中的运动场转移到图像上就表示为光流场 Optical Flow Field 光流场反映了图像上每一
  • 华为OD机试 - 字符串加密(Java)

    题目描述 给你一串未加密的字符串str 通过对字符串的每一个字母进行改变来实现加密 加密方式是在每一个字母str i 偏移特定数组元素a i 的量 数组a前三位已经赋值 a 0 1 a 1 2 a 2 4 当i gt 3时 数组元素a i
  • 关于问题【Run-Time Check Failure #2 - Stack around the variable 'data' was corrupted.】

    今天在用VS调代码的时候 会出现这个问题 经过查找以及DEBUG 最终发现是由于建立的数组下标溢出造成的 溢出的下标对应的数组的内容会出现乱码 分析 Run Time Check Failure 2 一般是栈被破坏 你的代码可能有缓冲区溢出
  • 靜下心来--重温正则表达式(二)

    在 靜下心来 重温正则表达式 一 这篇文章中 我们重点介绍了正则表达式的一些基础概念 以及在 String RegExp 的原型上涉及到正则表达式常用 4 个的方法 repalce match test exec 最后介绍了正则表达式的两种