这里是 Mastering Lookahead and Lookbehind 文章的简单翻译,这篇文章是在自己搜索问题的时候stackoverflow上回答问题的人推荐的,看完觉得写得很不错。这里的简单翻译是指略去了一些js不具备的内容,再者原文实在是太长了,所以也去掉了一些没有实质内容的话,同时也加入了很多自己的理解。如果需要深入理解js的断言机制,还是推荐先去看完MDN的基础再去看这篇文章(http://www.rexegg.com/regex-lookarounds.html)效果会比较好。

一开始是对零宽断言的简单概念介绍,略去。

先行断言例子:简单密码验证

密码需要满足四个条件:

  1. 6到10个单字字符 \w
  2. 至少包含一个小写字母 [a-z]
  3. 至少包含三个大写字母 [A-Z]
  4. 至少包含一个数字 \d

最初的设想就是在字符串的开头先行检测四次,每次检测每个条件。

条件一

这里文章用 \A 匹配字符串开头,用 \z 匹配字符串结尾,和 js 不一样,改了一下

第一个条件很简单:^\w{6,10}$。加入先行断言:(?=^\w{6,10}$),先行断言:在字符串开头的位置后面,是6到10个字符,以及字符串的结尾。

(at the current position in the string, what follows is the beginning of the string, six to ten word characters, and the very end of the string. )

我们想在字符串的开头断言,因此需要用做一个锚点定位,不需要重复声明开头,所以把从断言中拿出来:

^(?=\w{6,10}$)

留意到,虽然我们已经用先行断言检测了整个字符串,但是我们的位置还没有变,正则验证锚点依然停留在字符串的开头位置,只是做了先行判断。意味着我们还可以继续检测整个字符串。

条件二

检测小写字母最容易想到的写法是 .*[a-z],但是这种写法 .* 一开始就会匹配到字符串的结尾,导致回溯,容易想到的写法是 .*?[a-z] 这会导致更多的回溯。推荐的写法是 [^a-z]*[a-z](当需要用到包含某些字符时,可以参考这种通用的写法),将条件加入先行断言:(?=[^a-z]*[a-z]) ,因此正则变成:

^(?=\w{6,10}$)(?=[^a-z]*[a-z])

断言里面依然没有匹配任何字符,两个断言的位置是可以互换的。

条件三

类似条件二: (?=(?:[^A-Z]*[A-Z]){3})

正则变成了:

^(?=\w{6,10}$)(?=[^a-z]*[a-z])(?=(?:[^A-Z]*[A-Z]){3})

条件四

类似的:(?=\D*\d)

正则变成了:

^(?=\w{6,10}$)(?=[^a-z]*[a-z])(?=(?:[^A-Z]*[A-Z]){3})(?=\D*\d)

此时,我们在字符串开头断言,并先行检测了四次判读了四种条件,依然没有匹配任何字符,但是验证了密码。

匹配有效字符串

检查完毕后,正则检测的位置依然停留在字符串开头,可以用一个简单的.*去匹配整个字符串,因为不管.*匹配到了什么,都是经过验证的。因此:

^(?=\w{6,10}$)(?=[^a-z]*[a-z])(?=(?:[^A-Z]*[A-Z]){3})(?=\D*\d).*

微调:移除一个条件

检查这个正则里的先行断言,可以留意到\w{6,10}$这个表达式检查了字符串的所有字符,因此可以用他匹配整个字符串而不是用.*,因此可以减少一个先行判断简化正则:

^(?=[^a-z]*[a-z])(?=(?:[^A-Z]*[A-Z]){3})(?=\D*\d)\w{6,10}$

总结这个结果,如果检查n个条件,正则至多需要n-1个先行判断。甚至能够把几个先行判断合并。

实际上,除了\w{6,10}$刚好匹配了整个字符串外,其他的几个先行判断也可以通过改写匹配整个字符串,比如(?=\D*\d)可以加一个简单的.*$匹配到字符串结尾:

^(?=\w{6,10}$)(?=[^a-z]*[a-z])(?=(?:[^A-Z]*[A-Z]){3})\D*\d.*$

此外,为什么要在.*后面加$,难道不能匹配到字符串结尾么?因为点符号不匹配换行符(除非在DOTALL mode下,即点匹配所有),因此.*只能匹配到第一行的末尾,如果有换行则无法匹配到,$保证了我们不仅到达一行的结尾,也到达了字符串的结尾。

在这个正则表达式里,开头的(?=\w{6,10}$)已经匹配到了结尾,所以后面的$不是很必要。

先行断言的位置几乎没有影响

在这个例子里,因为三个先行断言都没有改变位置,所以可以互换。虽然结果没有影响,但是会影响性能,应该把容易验证失败的先行断言放在前面。

实际上,我们把^放在前面就是考虑了这个情况,因为^也没有匹配任何字符移动正则匹配锚点,他也可以和其他先行断言互换,但是这会带来问题。

首先,在DOTALL mode下,后行负向断言(?<!.)可以匹配开头,即前面没有任何字符,非DOTALL mode下,有(?<![\D\d])匹配开头。

现在假设把^放在第四个位置,在三个先行断言后,这时如果第三个断言失效了,那么正则引擎会到第二个位置继续从第一个先行断言匹配,就这样不停地改变位置匹配直到全部位置都失败。虽然只要匹配到^就不会从其他位置继续判断,但是正则引擎因为提前失败而无法到达^

放第一位时,除了开头位置外,其他位置在第一次匹配^就失败了,因此效率高些。

零宽断言没有改变位置

这里是一些初学者常犯的错误。

比如用A(?=5)匹配AB25,不理解地方在于先行断言里的5是紧跟A后的位置,如果要匹配后面的位置,需要用(?=[^5]*5)

A(?=5)(?=[A-Z])匹配A5B,依然是位置不变问题,应该是用A(?=5[A-Z])

零宽断言的用法

验证

即上面密码验证的例子,即一个字符串满足多个条件。每个条件都是检测整个字符串。

限制字符范围

比如匹配非Q字符外的单字字符\w。有几种写法:

  1. 字符减法,[\w-[Q]](js不支持)
  2. [_0-9a-zA-PR-Z]
  3. [^\WQ]

    先行断言写法:(?!Q)\w

    在先行断言当前位置后面不是Q后,\w匹配了一个字符。这个写法不仅容易理解,也容易附加拓展,比如不包含Q和K,那么就是:
(?![QK])\w`

后行断言:

\w(?<!Q)

Tempering the scope of a token 标志范围调整

限制标志(token)的匹配范围。

举个例子,如果想要匹配不以{END}开头的任何字符,可以用:

(?:(?!{END}).)*

每一个.标志都被(?!{END})调整,断言点标志不能是{END}的开头,这个技巧叫tempered greedy token

另外一种方案有点过于复杂,略去。

Delimiter 分隔符

在第一个#START#出现后匹配后面的所有字符写法:

(?<=#START#).*

或者匹配字符串的所有字符,除了#END#

.*?(?=#END#)

两个断言可以合并:

(?<=#START#).*?(?=#END#)

Inserting Text at a Position 在位置插入文本

给你一个文件,里面都是驼峰命名的电影标题,比如HaroldAndKumarGoToWhiteCastle,为了方便阅读,需要在大小写之间插入空格,下面的正则匹配这些位置:

(?<=[a-z])(?=[A-Z])

在编辑器的正则匹配查找中,可以用这个去匹配这些位置,并用空格代替。(这里能想到/[a-z][A-Z]/g同样能够查找,但是找到的不是位置,所以替换起来就不是那么方便了。

Splitting a String at a Position 在某位置分割字符串

类似上面的例子,就可以分割大小写之间的位置,在很多语言中,用split函数加上正则可以返回一个单词数组。

Finding Overlapping Matches 查找重叠匹配

有时候需要在同一个单词里做多次匹配,举个例子,想在ABCD中匹配ABCD,BCD,CD和D,可以用:

(?=(\w+))

这个还蛮好理解的,会匹配四个位置,"","A",,"","B","","C","","D",""。不过至于说怎么提取这四个部分,还没找到合适的方法。

Zero-Width Matches 0宽度匹配

零宽断言,锚点,边界在包含标志的正则表达式中,允许正则引擎返回匹配的字符串。举个例子(?<=start_)\d+,正则引擎会返回数字,但是不包括前缀start_

下面是一些应用:

Validation 验证

即类似密码验证例子

Inserting 插入

类似插入空格例子

Splitting 分割

类似插入空格例子

Overlapping Matches 重叠匹配

同一个单词里做多次匹配例子

Positioning the Lookaround 零宽断言定位

零宽断言有两个选择去定位,在文本前和文本后,一般来讲,其中一个性能更高。

Lookahead 先行断言

\d+(?= dollars)(?=\d+ dollars)\d+都匹配100 dallars中的100,但是前者性能更佳,因为他只匹配\d+一次。(这里写一下自己对第二个式子的理解,第二个式子其实是先断言当前位置的后面是\d+ dollars,然后匹配断言中的字符串中的\d+)。

Negative Lookahead 先行负向断言

\d+(?! dollars)(?!\d+ dollars)\d+都匹配100 pesos中的100,但是前者性能更佳,同上。

后面还有两个后行断言的例子,js不支持就不列举了。

这些例子的不同在于匹配的前后。这里的说明不是要就纠结于位置,只是能够知道并感觉到这样写正则的效率,通过练习,会慢慢熟悉这些不同并写出性能更高的正则。

Lookarounds that Look on Both Sides: Back to the Future

这个部分涉及到的是零宽断言的嵌套,这里只说明一下里面举的例子,因为js不支持后行断言,这里讲的东西作用就不大了。

匹配下划线之间的数字:_12_,有很多方法,文中提出的新方法是:

(?<=_(?=\d{2}_))\d+

即,当前位置前面断言匹配了下划线_,同时下划线的后面断言匹配了\d{2}_,即整个后行断言匹配的是_\d{2}_,而当前的位置在_\d{2}之间,后面用\d+匹配数字。

Compound Lookahead and Compound Lookbehind 复合先行和复合后行

在标志后至多有一个字符

匹配后面至多有一个下划线的数字:

\d+(?=_(?!_))

还有一种不太优雅的写法是:\d+(?=(?!__)_)

标志前至多有一个字符

匹配前面至多有一个下划线的数字:

(?<=(?<!_)_)\d+

还有一种不太优雅的写法是:(?<=_(?<!__))\d+

Multiple Compounding 多重复合

即多个嵌套,这个有点复杂,就是超过一次嵌套,多个条件一起判断。这里就不列举了,可以看看这个例子:

(?<=(?<!(?<!X)_)_)\d+

表示数字前缀不能是多个下划线,除了X__这种情况。

The Engine Doesn't Backtrack into Lookarounds……because they're atomic

_rabbit _dog _mouse DIC:cat:dog:mouse

在这个字符串中,DIC后面是允许的动物名,我们要匹配前面_tokens中在允许动物名内的。

_(\w+)\b(?=.*:\1\b)

获得_dog_mouse

翻转一下:

_(?=.*:(\w+)\b)\1\b

这样只匹配到了_mouse

这个地方很神奇,稍微讲一下。第一个正则还蛮好理解的每次正向断言都拿前面的\1捕获去匹配后面,按从左往右多次匹配结果到两个结果。第二个正则就特殊,捕获是放在正向断言里的,正向断言由于贪婪匹配会直接到了_mouse的下划线后的位置,然后正则引擎跳出正向断言去匹配\1,匹配到mouse成功。匹配结束。这里的重点是,正则引擎并不能在正向判断里面回溯,只要跳出了正向断言,就不会再进去。因此这里的正向断言只会匹配到mouse。我一开始想到加个非贪婪,那么就只会匹配到cat了。

Fixed-Width, Constrained-Width and Infinite-Width Lookbehind 负向断言,略去

Lookarounds (Usually) Want to be Anchored

匹配一个包含一个单词的字符串,里面有一位数字:

^(?=\D*\d)\w+$

这里需要考虑的问题是^锚点是否有必要。

这里的重点在于^能够减少错误的次数,如果没有^,正则引擎会在每个位置都去匹配,只有在所有位置都错误后才会返回错误,但是加了^,只要开头匹配错误引擎就会停止。虽然在匹配成功的情况下,两种情况返回是一样的,但是在性能上差别却很大。

One Exception: Overlapping Matches

不过有时候我们希望正则引擎匹配多个位置,比如上面的例子:(?=(\w+))。在ABCD中匹配了四次,获得了四个我们想要的结果。

后记

后记提到了上面讲到的[^a-z]*[a-z]优化为[^a-z]*+[a-z],不过一看就知道js不支持,这个的优化点在于,如果发现匹配不成功,有些不够智能的引擎会回溯前面的非小写字符,去匹配后面的小写字母这样显而易见的无效回溯。

这篇文章的大致解释就到这里,后面需要在了解一下关于正则引擎的问题了。

翻译文章来源:

http://www.rexegg.com/regex-lookarounds.html

本文来源:JuFoFu

本文地址:http://www.cnblogs.com/JuFoFu/p/7719916.html

水平有限,错误欢迎指正,转载请注明出处。

深入JS正则先行断言的更多相关文章

  1. JS 正则中环视(断言)应用 -- 数字千分符

    介绍一下顺序环视 (?=...) 和逆序环视 (?<=...) 方便不想看长文的人,如果在支持 ES2018 的环境中整数可以这样使用: String(12345678).replace(/(? ...

  2. js正则:零宽断言

    JavaScript正则表达式零宽断言 var str="abnsdfZL1234nvcncZL123456kjlvjkl"var reg=/ZL(\d{4}|\d{6})(?!\ ...

  3. js正则之零宽断言

    我们学到的正则表达式匹配,都是有“宽度”的,使用 \w+. 匹配下面文本,会将 . 一同匹配: regular. expression. 如果不想匹配符号,只匹配一个位置,就要用到“零宽断言”(匹配宽 ...

  4. JS正则密码复杂度校验之:至少有多种字符中的其中几种

    概述 续接上文的密码校验要求: 这个需求有两个难点,一,是如何使用正则匹配所有半角英文标点符号,二,是如何验证密码段中在要求的四种(大写字母,小写字母,数字,标点符号)类型中至少存在三种. 第一个难点 ...

  5. [AaronYang]那天有个小孩跟我说Js正则

    按照自己的思路学习Node.Js 随心出发.突破正则冷门知识点,巧妙复习正则常用知识点 标签:AaronYang  茗洋  Node.Js 正则 Javascript 本篇博客地址:http://ww ...

  6. [js]正则篇

    一.正则基本概念 1.一种规则.模式.文本处理工具 2.强大的字符串匹配工具 3.在js中常与字符串函数配合使用 二.js正则写法 正则在js中以正则对象存在: (1)var re=new RegEx ...

  7. 零宽度正预测先行断言是什么呢,看msdn上的官方解释定义

    最近为了对html文件进行源码处理,需要进行正则查找并替换.于是借着这个机会把正则系统地学一下,虽然以前也用过正则,但每次都是临时学一下混过关的.在学习的过程中还是遇到不少问题的,特别是零宽断言(这里 ...

  8. JS正则汇总

    1.基本定义: \s:用于匹配单个空格符,包括tab键和换行符; \S:用于匹配除单个空格符之外的所有字符; \d:用于匹配从0到9的数字; \w:用于匹配字母,数字或下划线字符; \W:用于匹配所有 ...

  9. 关于JS正则——你知道多少?

    正则表达式 1. 使用正则 创建正则表达式有两种方式,一种是以字面量方式创建,另一种是使用RegExp构造函数来创建. var expression = / pattern / flags; var ...

随机推荐

  1. JFinal 添加Druid插件

    第一步:添加依赖 <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</a ...

  2. appium+python的APP自动化(2)

    上节说到安卓上的测试环境都安装好了,这个时候要安装python了 1python的安装 https://www.python.org/15官网下载python2.7(3.0以上也行,个人爱好),安装也 ...

  3. Python面试题(练习三)

    1.MySQL索引种类 1.普通索引 2.唯一索引 3.主键索引 4.组合索引 5.全文索引 2.索引在什么情况下遵循最左前缀的规则? 最左前缀原理的一部分,索引index1:(a,b,c),只会走a ...

  4. drf解决跨域问题 使用 django-corse-headers扩展

    跨域CORS 使用django-corse-headers扩展 安装 pip install django-cors-headers 添加应用 INSTALLED_APPS = ( ... 'cors ...

  5. sublime3 Package Control和 中文安装

    sublime3中文版需要使用PackageControl,所以首先需要安装PackageControl 一.PackageControl安装: 1.点击Preferences > Browse ...

  6. UVALive 5029 字典树

    E - Encoded Barcodes Crawling in process...Crawling failedTime Limit:3000MS    Memory Limit:0KB    6 ...

  7. BZOJ5299 [Cqoi2018]解锁屏幕 【状压dp】

    题目链接 BZOJ5299 题解 就一个毒瘤卡常题..写了那么久 设\(f[i][s]\)表示选了集合\(s\)中的点,最后一个是\(i\),进行转移 要先预处理出两点间的点,然后卡卡常就可以过了 # ...

  8. linux下源代码分析和阅读工具比较

    Windows下的源码阅读工具Souce Insight凭借着其易用性和多种编程语言的支持,无疑是这个领域的“带头大哥”.Linux/UNIX环境下呢?似乎仍然是处于百花齐放,各有千秋的春秋战国时代, ...

  9. 算法复习———dijkstra求次短路(poj3255)

    题目: Description Bessie has moved to a small farm and sometimes enjoys returning to visit one of her ...

  10. 类复制 MemberwiseClone与Clone(深 浅 Clone)

    MemberwiseClone 方法创建一个浅表副本,具体来说就是创建一个新对象,然后将当前对象的非静态字段复制到该新对象.如果字段是值类型的,则对该字段执行逐位复制.如果字段是引用类型,则复制引用但 ...