这里是 Mastering Lookahead and Lookbehind 文章的简单翻译,这篇文章是在本身搜索问题的时候stackoverflow上回答问题的人推荐的,看完以为写得很不错。这里的简单翻译是指略去了一些js不具有的内容,再者原文实在是太长了,因此也去掉了一些没有实质内容的话,同时也加入了不少本身的理解。若是须要深刻理解js的断言机制,仍是推荐先去看完MDN的基础再去看这篇文章(http://www.rexegg.com/regex-lookarounds.html)效果会比较好。html
一开始是对零宽断言的简单概念介绍,略去。正则表达式
密码须要知足四个条件:数组
最初的设想就是在字符串的开头先行检测四次,每次检测每一个条件。app
这里文章用 \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. )ide
咱们想在字符串的开头断言,所以须要用^作一个锚点定位,不须要重复声明开头,因此把^从断言中拿出来:函数
^(?=\w{6,10}$)
留意到,虽然咱们已经用先行断言检测了整个字符串,可是咱们的位置尚未变,正则验证锚点依然停留在字符串的开头位置,只是作了先行判断。意味着咱们还能够继续检测整个字符串。性能
检测小写字母最容易想到的写法是 .*[a-z]
,可是这种写法 .* 一开始就会匹配到字符串的结尾,致使回溯,容易想到的写法是 .*?[a-z]
这会致使更多的回溯。推荐的写法是 [^a-z]*[a-z]
(当须要用到包含某些字符时,能够参考这种通用的写法),将条件加入先行断言:(?=[^a-z]*[a-z])
,所以正则变成:优化
^(?=\w{6,10}$)(?=[^a-z]*[a-z])
断言里面依然没有匹配任何字符,两个断言的位置是能够互换的。atom
相似条件二: (?=(?:[^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
。有几种写法:
(?!Q)\w
\w
匹配了一个字符。这个写法不只容易理解,也容易附加拓展,好比不包含Q和K,那么就是:(?![QK])\w`
后行断言:
\w(?<!Q)
限制标志(token)的匹配范围。
举个例子,若是想要匹配不以{END}开头的任何字符,能够用:
(?:(?!{END}).)*
每个.
标志都被(?!{END})
调整,断言点标志不能是{END}
的开头,这个技巧叫tempered greedy token
另一种方案有点过于复杂,略去。
在第一个#START#
出现后匹配后面的全部字符写法:
(?<=#START#).*
或者匹配字符串的全部字符,除了#END#
.*?(?=#END#)
两个断言能够合并:
(?<=#START#).*?(?=#END#)
给你一个文件,里面都是驼峰命名的电影标题,好比HaroldAndKumarGoToWhiteCastle
,为了方便阅读,须要在大小写之间插入空格,下面的正则匹配这些位置:
(?<=[a-z])(?=[A-Z])
在编辑器的正则匹配查找中,能够用这个去匹配这些位置,并用空格代替。(这里能想到/[a-z][A-Z]/g
一样可以查找,可是找到的不是位置,因此替换起来就不是那么方便了。
相似上面的例子,就能够分割大小写之间的位置,在不少语言中,用split函数加上正则能够返回一个单词数组。
有时候须要在同一个单词里作屡次匹配,举个例子,想在ABCD
中匹配ABCD,BCD,CD和D,能够用:
(?=(\w+))
这个还蛮好理解的,会匹配四个位置,"","A",,"","B","","C","","D",""。不过至于说怎么提取这四个部分,还没找到合适的方法。
零宽断言,锚点,边界在包含标志的正则表达式中,容许正则引擎返回匹配的字符串。举个例子(?<=start_)\d+
,正则引擎会返回数字,可是不包括前缀start_
。
下面是一些应用:
即相似密码验证例子
相似插入空格例子
相似插入空格例子
同一个单词里作屡次匹配例子
零宽断言有两个选择去定位,在文本前和文本后,通常来说,其中一个性能更高。
\d+(?= dollars)
和(?=\d+ dollars)\d+
都匹配100 dallars
中的100
,可是前者性能更佳,由于他只匹配\d+
一次。(这里写一下本身对第二个式子的理解,第二个式子实际上是先断言当前位置的后面是\d+ dollars
,而后匹配断言中的字符串中的\d+
)。
\d+(?! dollars)
和(?!\d+ dollars)\d+
都匹配100 pesos
中的100
,可是前者性能更佳,同上。
后面还有两个后行断言的例子,js不支持就不列举了。
这些例子的不一样在于匹配的先后。这里的说明不是要就纠结于位置,只是可以知道并感受到这样写正则的效率,经过练习,会慢慢熟悉这些不一样并写出性能更高的正则。
这个部分涉及到的是零宽断言的嵌套,这里只说明一下里面举的例子,由于js不支持后行断言,这里讲的东西做用就不大了。
匹配下划线之间的数字:_12_
,有不少方法,文中提出的新方法是:
(?<=_(?=\d{2}_))\d+
即,当前位置前面断言匹配了下划线_
,同时下划线的后面断言匹配了\d{2}_,即整个后行断言匹配的是_\d{2}_
,而当前的位置在_
和\d{2}
之间,后面用\d+
匹配数字。
匹配后面至多有一个下划线的数字:
\d+(?=_(?!_))
还有一种不太优雅的写法是:\d+(?=(?!__)_)
匹配前面至多有一个下划线的数字:
(?<=(?<!_)_)\d+
还有一种不太优雅的写法是:(?<=_(?<!__))\d+
即多个嵌套,这个有点复杂,就是超过一次嵌套,多个条件一块儿判断。这里就不列举了,能够看看这个例子:
(?<=(?<!(?<!X)_)_)\d+
表示数字前缀不能是多个下划线,除了X__
这种状况。
_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了。
匹配一个包含一个单词的字符串,里面有一位数字:
^(?=\D*\d)\w+$
这里须要考虑的问题是^
锚点是否有必要。
这里的重点在于^
可以减小错误的次数,若是没有^
,正则引擎会在每一个位置都去匹配,只有在全部位置都错误后才会返回错误,可是加了^
,只要开头匹配错误引擎就会中止。虽然在匹配成功的状况下,两种状况返回是同样的,可是在性能上差异却很大。
不过有时候咱们但愿正则引擎匹配多个位置,好比上面的例子:(?=(\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
水平有限,错误欢迎指正,转载请注明出处。