lanlin / notes

个人笔记
https://github.com/lanlin/notes/issues
30 stars 0 forks source link

PCRE零宽断言 #100

Open lanlin opened 3 years ago

lanlin commented 3 years ago

(一) 前后左右作为计算机术语名词的含义

计算机里面的前后、左右、上下等等的方位名词,往往与我们中文环境中的意思是不太一样,所以理解起来很别扭。

如 “向前兼容”、“向上兼容” 是指老版本的数据或者API可以与新版本通用。

而 “向后兼容”、“向下兼容” 是指新版本的数据或者API可以与老版本通用。

在【正则】中,前 = 右,后 = 左,那么前瞻(向前)就是向右,后顾(回顾)就是向左

0 a 1 b 2 c 3

例如,对于上面 “0a1b2c3” 这个字符串而言, 0 是在 a 的后面,1 是在 a 的前面。

需要特别注意的是,前瞻或者后顾是可以配合 “肯定” 或者 “否定” 条件一起执行的。

这就是所谓的 【正前瞻】【负前瞻】【正回顾】【负回顾】。

其中的 【正】表示“肯定”,也相当于等于或者全等于的意思,【负】就是取反,也就是不等于的意思。

搞清了这些基本概念就不会被一大堆的正则术语名词搞懵了。

lanlin commented 3 years ago

(二)正则表达式是如何执行的

执行原理

正则表达式的执行顺序是从左到右依次【分段】执行,这里的段是指一个一个的小规则单元。

字符串的匹配过程是逐个字符进行的,每完成一个字符的匹配,【指针】向前移动一个位置。

需要注意的是,指针所在的位置与字符的位置并不是一一对应的,而是前后的关系,这对于理解【零宽】至关重要。

image

什么叫指针

"abc"    // 字符串
"0 a 1 b 2 c 3"  // 数字为指针可以移动到的位置

如上面的代码 "abc" 字符串为例,指针可以在 0,1,2,3 四个地方来回移动。

也就是说每个字符前后的位置就是指针可以移到的位置。

可以简单的理解为当前匹配 “走” 到哪儿了,我们就在哪个字符前面或者后面的位置加个标记。

表示下一个匹配就从这个标记的位置处开始,这个标记就是【指针】。

指针的作用

指针的作用是标记字符串匹配到了哪个位置,它决定了接下来的匹配从哪个地方开始。

lanlin commented 3 years ago

(三)什么是非零宽?

【非零宽】是个【抢劫犯】,以抢占字符为荣。

凡是我【非零宽】规则走过的地方、扫描过的字符,

我不管你是不是跟我匹配,我都宣布你已经阵亡了,你的地盘都是我的了,这就是【非零宽】。

它会把它经手过的字符统统占有了,我抢到就是我的,不让后面的规则再来扫描它们了,我管事管的宽,所以我就叫【非零宽】。

'abc'
/ab/

如上面的示例,'abc' 是一个字符串, /ab/ 是一个正则。

结合上面第二条说的执行原理,那么位置数据就是 "0a1b2c3",一开始指针位于起始位置 0。

这个时候从规则中取出第一个小单元 /a/,因为默认非特殊类型的匹配都是 【前瞻】类型的,也就是说都是向右的方向。

image

那么 0 的位置下面第一个字符是 a, 这恰好与 /a/ 相匹配,完成小单元匹配,正则引擎将【指针】往前移动到位置 1。

image

接下来的 /b/ 就直接从位置 1 开始匹配下一个字符,顺利匹配到 b,然后【指针】顺利移动到位置 2。

image

这个时候 'abc' 中的 'ab' 都被标记为已完成匹配,接下来的匹配不能再让 'ab' 两个字符参与了,得从 'c' 开始了。

所以,/ab/ 这种扫描到哪里就把【指针】搬到哪里的类型,就是非零宽。

匹配步骤详见: https://regex101.com/r/K3iDO9/1/debugger

lanlin commented 3 years ago

(四)什么是零宽?

【零宽】是个【偷窥狂】,以悄悄偷窥前后字符为荣。

我【零宽】可不像上面那个抢劫犯,占有欲那么强。

对于经手过的字符我从来不私自占有,美的东西是要大家一起欣赏的嘛。

万花丛中过,片叶不沾身,这就是【非零宽】。

它每次都会从【指针】所在的位置伸出脑袋去看前后有没有字符是符合它的审美标准的。

一旦发现之后就会大声嚷嚷,快看啊我前面(后面)有几个绝色字符,非常漂亮。

'abc'
/(?=bc)/

(?= pattern) 是一个【正前瞻(零宽正向先行断言)】。

如上的例子,同样的位置数据 "0a1b2c3"。

image

一上来,当指针在 0 位置的时候, (?=bc) 就往右(前瞻)伸出脑袋来看:我的右边有 bc 么?

结果这个时候看到了字符 a, 看了两眼,不是它喜欢的类型,就又把脑袋缩回到 0 的位置了。

image

这个时候【正则引擎】把【指针】移动到 1 的位置,这家伙马上又伸出脑袋去看,这下子发现右边有 bc 好开心。

image

但是,我不能占有她们的地盘,我也不能捕获她们,我只是看看就好,就又把脑袋缩回到 1 的位置。

image

引擎继续移动指针,它继续这么浪下去,到头来它什么都没捞着。。。

所以,这就是【零宽】,即使它把脑袋直接伸长(扫描)到字符串末尾,它也不会对【指针】有半分的移动。

匹配步骤详见:https://regex101.com/r/9mISYE/1/debugger

lanlin commented 3 years ago

(五)总结

零与非零:

1.【非零宽】就是我扫描到哪里,我就把【指针】插到哪里,凡是我经过的【都是我的】。

2.【零宽】就是【指针】插到哪里,我就从哪里开始左右偷窥,然后乖乖的回到【指针】这里,凡是我经过的【都不是我的】。

零宽特点 描述解释
不消耗字符 扫描完了指针还是在原地,不会把扫描过的字符“占有”
不捕获字符 扫描完了什么都不会获取

零宽断言:

主要有【正前瞻】【负前瞻】【正回顾】【负回顾】四种类型的零宽断言。不同的人叫法不一样,我只取简单易记的这种。

【正向】=【是】表示肯定类型的匹配,相当于 ==

【负向】=【非】表示否定类型的匹配,相当于 !=

【先行】=【右】表示向前方找,向右方找

【后发】=【左】表示向后方找,向左方找

表达式 术语名词 功能描述
(?=pattern) 【正前瞻】(零宽正向先行断言) 从当前指针处,往【右】边找,找【是】 pattern 的
(?!pattern) 【负前瞻】(零宽负向先行断言) 从当前指针处,往【右】边找,找【非】pattern 的
(?<=pattern) 【正回顾】(零宽正向后发断言) 从当前指针处,往【左】边找,找【是】pattern 的
(?<!pattern) 【负回顾】(零宽负向后发断言) 从当前指针处,往【左】边找,找【非】pattern 的