文章大纲

正则表达式中零宽断言?=和?!的区别

2019-06-22 23:02:19

正则在工作中经常用到,可是关于什么零宽断言的,总是看了又忘,几个问号傻傻分不清。今天找出个规律来,争取把它给牢牢记住。


PHP在线执行: http://www.dooccn.com/php/,为方便测试学习,大家可以使用我推荐的这个php在线执行工具。


关于?=、?:的这些叫法,曾经看到有好多种,有前瞻、后顾,也有正反向零宽断言啥的,感觉完全搞复杂化了。

这里暂时先摒弃这些叫法,只关注什么符号对应什么用途,并如何牢牢记住它们,而不容易混淆。


先来个小的汇总:

exp1(?=exp2)    查找exp2前面的exp1
(?<=exp2)exp1   查找exp2后面的exp1
exp1(?!exp2)    查找后面不是exp2的exp1
(?<!=exp2)exp1  查找前面不是exp2的exp1 

记忆诀窍:

  1. 首先都是由问号带头的。我指的是上面四行表达式中括号里的部分。
  2. 区分什么时候放左边,把<=看成左箭头,就是(?<=exp2)是放在exp1左边的。 相反,没有左箭头的(?=exp2),我们就知道是放在exp1的右边了。
  3. 按照第2点,第3行和第4行的位置也同样好记住了。
  4. 第4行中(?<!=exp2)怎么避免写成(?<=!exp2),感叹号!在数学中有非的意思,!=即意味着不等于,这样就不会写错了。

先就这四个我们操练一下。


例子1:获取浏览器cookie中的session_id。当模拟登录时就需要做这个工作。

//这是我随意粘贴的csdn上一篇博客的请求header里的数据
$str = 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: max-age=0
cookie: uuid_tt_dd=10_18458393350-1559972832257-756853; 
dc_session_id=10_1559972832257.674318;
Hm_ct_6bcd52f51e9b3dce32bec4a3997715ac=6525*1*10_18458393350-1559972832257-756853;
_ga=GA1.2.226631537.1560048636;
c-login-auto=13; dc_tos=ptemwv;
Hm_lvt_6bcd52f51e9b3dce32bec4a3997715ac=1561043554,1561043566,1561044398,1561046143;
Hm_lpvt_6bcd52f51e9b3dce32bec4a3997715ac=1561046143 referer: https://blog.csdn.net/csm0912/article/details/81206848 upgrade-insecure-requests: 1 user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.80 Safari/537.36'; preg_match('/(?<=dc_session_id=)[a-zA-Z\d._]+/', $str, $match); var_dump($match);

结果:

array(1) {
[0]=>
string(23) "10_1559972832257.674318"
}


上面代码中[a-zA-Zd._]这里注意下,里面点号和下划线可不用转义,如果是破折号-,就需要\-这样子来转义

例子1中就是上文一开始列举的(?<=exp2)exp1情况,例子1中exp2是固定字符串dc_session_id=,那这里exp2可不可以继续用正则呢?



例子2:获取网页里所有的超链接地址和图片地址

//为了简化网页,下面字符串是我自己随便造的
$str = '<a class="link" href="http://www.zhai14.com">翟码农</a>
<img src="http://www.zhai14.com/media/upload/article/9671d518gy1g14y4dwe36j20ki08u758.jpg">';

preg_match_all('/(?<=[src|href]=")http:\/\/[\w/.]+(?=")/', $str, $match);
var_dump($match);

结果:

array(1) {
[0]=>
array(2) {
[0]=>
string(21) "http://www.zhai14.com"
[1]=>
string(79) "http://www.zhai14.com/media/upload/article/9671d518gy1g14y4dwe36j20ki08u758.jpg"
}
}

上面正则表达式里,我同时用了(?<=exp2) 和(?=exp2)这两种,在(?<=exp2)里,可以看到exp2我继续用了正则[src|href],这就回答了例子1结束时所问的问题了。


使用?<=或?<=这种时,有时正则表达式没写对会报如下错误:

lookbehind assertion is not fixed length at offset ...

这是因为上面这种正则符号后,跟上了不固定宽度的正则字符串。

如果在例子2中,[src|href]后面加上+或*,你就会看到这种错误提示。

在以前缀为条件的表达式后面,指定的字符串必须是固定长度,所以不可含*,+,{1,}等


例子3:公司年会抽奖,老板手机号以56结尾,规则安排以6结尾但倒数第二位不是5的手机号员工直接发奖品,不用参与抽奖。至于发放什么奖品,则按照手机号倒数第三位数字对应的奖品等级来发放。

$arr = array(
'18018234576',
'18308723897',
'13243435679',
'13476767736',
//虽然6结尾,可惜倒数第二位跟老板相同,不好意思,你要去抽奖了
'13389890656',
);

$bonus_user = array(); //存储直接领取奖品的用户
foreach($arr as $mobile ){
$t = preg_match('/(?<=[\d]{8})(\d)(?!5)\d6$/', $mobile, $match);
if( !empty($match) ){
$bonus_user[$mobile] = $match[1];
}

}
var_dump($bonus_user);

以下正则表达式的解释:

(?<=[\d]{8})(\d)(?!5)\d6

开头(?<=[\d]{8})匹配8位数字,但是不捕获,就是结果不需要显示这8位。

中间(\d),代表手机号第9位,就是判定用户领取什么奖品的。用括号,表示分组,就是结果会单独显示这一位数据,避免进一步用代码处理。

(?!5), 表示匹配的第9位之后不能是紧跟着5的,也就是排除第十位是5的。

最后,\d6表示匹配手机号的最后两位,最后一位要求是6.

其实最后这两位我们也不需要捕获的,可我暂不清楚怎么写,所以只好在第9位用了分组(即加了括号),来获取我想要的数据。


结果:

array(2) {
[18018234576]=>
string(1) "5"
[13476767736]=>
string(1) "7"
}

18018234576, 13476767736这两位员工,就直接可以发放5等奖和7等奖的奖品了。

注意,这里(?!5)这部分,我一开始写成(?!=5), 看下面我重新描述的4种情况,唯独这一种是不带有等号=的。

第4种情况,暂时没想到好例子,就不举了,自行操练吧。


ok,想必你应该弄懂了,也记住了上面四种表达式,我们再顺便把他们的叫法也给理解一下。


网上搜了下,有下面这一种叫法:

零宽度正预测先行断言  exp1(?=exp2)    查找exp2前面的exp1
零宽度正回顾后发断言 (?<=exp2)exp1 查找exp2后面的exp1
零宽度负预测先行断言 exp1(?!exp2) 查找后面不是exp2的exp1
零宽度负回顾后发断言 (?<!=exp2)exp1 查找前面不是exp2的exp1

梳理一下,发现其实这种叫法也不难记,只是以前没沉下心来花心思而已:

记忆诀窍:所有都开头叫零宽度,就像表达式都是问号?带头的一样,意思就是不捕获结果。有感叹号!的,叫负,反之叫正。预测,就是放在右边的,回顾就是放在左边的。先行跟预测固定搭配,回顾跟后发固定搭配



我要评论
评论列表