正则基础之 环视 Lookaround
<h1>1、环视基础</h1><p>环视只进行子表达式的匹配,不占有字符,匹配到的内容不保存到最终的匹配结果,是零宽度的。环视匹配的最终结果就是一个位置。</p>
<p>环视的作用相当于对所在位置加了一个附加条件,只有满足这个条件,环视子表达式才能匹配成功。</p>
<p>环视按照方向划分有顺序和逆序两种,按照是否匹配有肯定和否定两种,组合起来就有四种环视。顺序环视相当于在当前位置右侧附加一个条件,而逆序环视相当于在当前位置左侧附加一个条件。</p>
<table border="1" style="width:819px">
<thead>
<tr>
<th scope="col">
<p><strong>表达式</strong></p>
</th>
<th scope="col">
<p><strong>说明</strong></p>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<p><strong>(?<=Expression)</strong></p>
</td>
<td>
<p>逆序肯定环视,表示所在位置左侧能够匹配Expression</p>
</td>
</tr>
<tr>
<td>
<p><strong>(?<!Expression)</strong></p>
</td>
<td>
<p>逆序否定环视,表示所在位置左侧不能匹配Expression</p>
</td>
</tr>
<tr>
<td>
<p><strong>(?=Expression)</strong></p>
</td>
<td>
<p>顺序肯定环视,表示所在位置右侧能够匹配Expression</p>
</td>
</tr>
<tr>
<td>
<p><strong>(?!Expression)</strong></p>
</td>
<td>
<p>顺序否定环视,表示所在位置右侧不能匹配Expression</p>
</td>
</tr>
</tbody>
</table>
<p> 对于环视的叫法,有的文档里叫预搜索,有的叫什么什么断言的,这里使用了更多人容易接受的《精通正则表达式》中“环视”的叫法,其实叫什么无所谓,只要知道是什么作用就是了,就这么几个语法规则, 还是很容易记的</p>
<h1>2、环视匹配原理</h1>
<p> 环视是正则中的一个难点,对于环视的理解,可以从应用和原理两个角度理解,如果想理解得更清晰、深入一些,还是从原理的角度理解好一些,正则匹配基本原理参考 NFA引擎匹配原理。</p>
<p>上面提到环视相当于对“所在位置”附加了一个条件,环视的难点在于找到这个“位置”,这一点解决了,环视也就没什么秘密可言了。</p>
<h2>顺序环视匹配过程</h2>
<p>对于顺序肯定环视<strong>(?=Expression)</strong>来说,当子表达式<strong>Expression</strong>匹配成功时,<strong>(?=Expression)</strong>匹配成功,并报告<strong>(?=Expression)</strong>匹配当前位置成功。</p>
<p>对于顺序否定环视<strong>(?!Expression)</strong>来说,当子表达式<strong>Expression</strong>匹配成功时,<strong>(?!Expression)</strong>匹配失败;当子表达式<strong>Expression</strong>匹配失败时,<strong>(?!Expression)</strong>匹配成功,并报告<strong>(?!Expression)</strong>匹配当前位置成功;</p>
<p>顺序肯定环视的例子已在NFA引擎匹配原理中讲解过了,这里再讲解一下顺序否定环视。</p>
<p> </p>
<p><img alt="" src="data/attachment/forum/202302/22/2023-02-22_20-57-15_765.jpg" style="height:128px; width:447px" /></p>
<p>源字符串:<strong>aa<p>one</p>bb<div>two</div>cc</strong></p>
<p>正则表达式:<strong><</strong><strong>(?!/?p\b)</strong><strong>[^>]+</strong><strong>></strong></p>
<p>这个正则的意义就是匹配除<p…>或</p>之外的其余标签。</p>
<p>匹配过程:</p>
<p><img alt="" src="data/attachment/forum/202302/22/2023-02-22_20-57-15_148.jpg" style="height:78px; width:477px" /></p>
<p>首先由字符“<strong><</strong>”取得控制权,从位置0开始匹配,由于“<strong><</strong>”匹配“<strong>a</strong>”失败,在位置0处整个表达式匹配失败,第一次迭代匹配失败,正则引擎向前传动,由位置1处开始尝试第二次迭代匹配。</p>
<p>重复以上过程,直到位置2,“<strong><</strong>”匹配“<strong><</strong>”成功,控制权交给“<strong>(?!/?p\b)</strong>”;“<strong>(?!/?p\b)</strong>”子表达式取得控制权后,进行内部子表达式的匹配。首先由“<strong>/?</strong>”取得控制权,尝试匹配“<strong>p</strong>”失败,进行回溯,不匹配,控制权交给“<strong>p</strong>”;由“<strong>p</strong>”来尝试匹配“<strong>p</strong>”,匹配成功,控制权交给“<strong>\b</strong>”;由“<strong>\b</strong>”来尝试匹配位置4,匹配成功。此时子表达式匹配完成,“<strong>/?p\b</strong>”匹配成功,那么环视表达式“<strong>(?!/?p\b)</strong>”就匹配失败。在位置2处整个表达式匹配失败,新一轮迭代匹配失败,正则引擎向前传动,由位置3处开始尝试下一轮迭代匹配。</p>
<p>在位置8处也会遇到一轮“<strong>/?p\b</strong>”匹配“<strong>/p</strong>”成功,而导致环视表达式“<strong>(?!/?p\b)</strong>”匹配失败,从而导致整个表达式匹配失败的过程。</p>
<p>重复以上过程,直到位置14,“<strong><</strong>”匹配“<strong><</strong>”成功,控制权交给“<strong>(?!/?p\b)</strong>”;“<strong>/?</strong>”尝试匹配“<strong>d</strong>”失败,进行回溯,不匹配,控制权交给“<strong>p</strong>”;由“<strong>p</strong>”来尝试匹配“<strong>d</strong>”,匹配失败,已经没有备选状态可供回溯,匹配失败。此时子表达式匹配完成,“<strong>/?p\b</strong>”匹配失败,那么环视表达式“<strong>(?!/?p\b)</strong>”就匹配成功。匹配的结果是位置15,然后控制权交给“<strong>[^>]+</strong>”;由“<strong>[^>]+</strong>”从位置15进行尝试匹配,可以成功匹配到“<strong>div</strong>”,控制权交给“<strong>></strong>”;由“<strong>></strong>”来匹配“<strong>></strong>”。</p>
<p>此时正则表达式匹配完成,报告匹配成功。匹配结果为“<strong><div></strong>”,开始位置为14,结束位置为19。其中“<strong><</strong>”匹配“<strong><</strong>”,“<strong>(?!/?p\b)</strong>”匹配位置15,“<strong>[^>]+</strong>”匹配字符串“<strong>div</strong>”,“<strong>></strong>”匹配“<strong>></strong>”。</p>
<h2>逆序环视基础</h2>
<p>对于逆序肯定环视<strong>(?<=Expression)</strong>来说,当子表达式<strong>Expression</strong>匹配成功时,<strong>(?<=Expression)</strong>匹配成功,并报告<strong>(?<=Expression)</strong>匹配当前位置成功。</p>
<p>对于逆序否定环视<strong>(?<!Expression)</strong>来说,当子表达式<strong>Expression</strong>匹配成功时,<strong>(?<!Expression)</strong>匹配失败;当子表达式<strong>Expression</strong>匹配失败时,<strong>(?<!Expression)</strong>匹配成功,并报告<strong>(?<!Expression)</strong>匹配当前位置成功;</p>
<p>顺序环视相当于在当前位置右侧附加一个条件,所以它的匹配尝试是从当前位置开始的,然后向右尝试匹配,直到某一位置使得匹配成功或失败为止。而逆序环视的特殊处在于,它相当于在当前位置左侧附加一个条件,所以它不是在当前位置开始尝试匹配的,而是从当前位置左侧某一位置开始,匹配到当前位置为止,报告匹配成功或失败。</p>
<p>顺序环视尝试匹配的起点是确定的,就是当前位置,而匹配的终点是不确定的。逆序环视匹配的起点是不确定的,是当前位置左侧某一位置,而匹配的终点是确定的,就是当前位置。</p>
<p>所以顺序环视相对是简单的,而逆序环视相对是复杂的。这也就是为什么大多数语言和工具都提供了对顺序环视的支持,而只有少数语言提供了对逆序环视支持的原因。</p>
<p>JavaScript中只支持顺序环视,不支持逆序环视。</p>
<p>Java中虽然顺序环视和逆序环视都支持,但是逆序环视只支持长度确定的表达式,逆序环视中量词只支持“?”,不支持其它长度不定的量词。长度确定时,引擎可以向左查找固定长度的位置作为起点开始尝试匹配,而如果长度不确定时,就要从位置0开始尝试匹配,处理的复杂度是显而易见的。</p>
<p>目前只有.NET中支持不确定长度的逆序环视。</p>
<h2>逆序环视匹配过程</h2>
<p><img alt="" src="data/attachment/forum/202302/22/2023-02-22_20-57-15_976.jpg" style="height:123px; width:351px" /></p>
<p>源字符串:<strong><div>a test</div></strong></p>
<p>正则表达式:<strong>(?<=<div>)</strong><strong>[^<]+</strong><strong>(?=</div>)</strong></p>
<p>这个正则的意义就是匹配<div>和</div>标签之间的内容,而不包括<div>和</div>标签本身。</p>
<p>匹配过程:</p>
<p>首先由“<strong>(?<=<div>)</strong>”取得控制权,从位置0开始匹配,由于位置0是起始位置,左侧没有任何内容,所以“<strong><div></strong>”必然匹配失败,从而环视表达式“<strong>(?<=<div>)</strong>”匹配失败,导致整个表达式在位置0处匹配失败。第一轮迭代匹配失败,正则引擎向前传动,由位置1处开始尝试第二次迭代匹配。</p>
<p>直到传动到位置5,“<strong>(?<=<div>)</strong>”取得控制权,向左查找5个位置,由位置0开始匹配,由“<strong><div></strong>”匹配“<strong><div></strong>”成功,从而“<strong>(?<=<div>)</strong>”匹配成功,匹配的结果为位置5,控制权交给“<strong>[^<]+</strong>”;“<strong>[^<]+</strong>”从位置5开始尝试匹配,匹配“<strong>a test</strong>”成功,控制权交给“<strong>(?=</div>)</strong>”;由“<strong></div></strong>”匹配“<strong></div></strong>”成功,从而“<strong>(?=</div>)</strong>”匹配成功,匹配结果为位置11。</p>
<p>此时正则表达式匹配完成,报告匹配成功。匹配结果为“<strong>a test</strong>”,开始位置为5,结束位置为11。其中“<strong>(?<=<div>)</strong>”匹配位置5,“<strong>[^<]+</strong>”匹配“<strong>a test</strong>”,“<strong>(?=</div>)</strong>”匹配位置11。</p>
<p>逆序否定环视的匹配过程与上述过程类似,区别只是当<strong>Expression</strong>匹配失败时,逆序否定表达式<strong>(?<!Expression)</strong>才匹配成功。</p>
<p>到此环视的匹配原理已基本讲解完,环视也就没有什么秘密可言了,所需要的,也只是多加练习而已。</p>
<h1>3、环视应用</h1>
<p>今天写累了,暂时就给出一个环视的综合应用实例吧,至于环视的应用场景和技巧,后面再整理。</p>
<p>需求:数字格式化成用“,”的货币格式。</p>
<p>正则表达式:<strong>(?<=\d)(?<!\.\d*)(?=(?:\d{3})+(?:\.\d+|$))</strong></p>
<p>测试代码:</p>
<p>double[] data = new double[] { 0, 12, 123, 1234, 12345, 123456, 1234567, 123456789, 1234567890, 12.345, 123.456, 1234.56, 12345.6789, 123456.789, 1234567.89, 12345678.9 };</p>
<p>foreach (double d in data)</p>
<p>{</p>
<p> richTextBox2.Text += "源字符串:" + d.ToString().PadRight(15) + "格式化:" + Regex.Replace(d.ToString(), @"(?<=\d)(?<!\.\d*)(?=(?:\d{3})+(?:\.\d+|$))", ",") + "\n";</p>
<p>}</p>
<p>输出结果:</p>
<p>源字符串:0 格式化:0</p>
<p>源字符串:12 格式化:12</p>
<p>源字符串:123 格式化:123</p>
<p>源字符串:1234 格式化:1,234</p>
<p>源字符串:12345 格式化:12,345</p>
<p>源字符串:123456 格式化:123,456</p>
<p>源字符串:1234567 格式化:1,234,567</p>
<p>源字符串:123456789 格式化:123,456,789</p>
<p>源字符串:1234567890 格式化:1,234,567,890</p>
<p>源字符串:12.345 格式化:12.345</p>
<p>源字符串:123.456 格式化:123.456</p>
<p>源字符串:1234.56 格式化:1,234.56</p>
<p>源字符串:12345.6789 格式化:12,345.6789</p>
<p>源字符串:123456.789 格式化:123,456.789</p>
<p>源字符串:1234567.89 格式化:1,234,567.89</p>
<p>源字符串:12345678.9 格式化:12,345,678.9</p>
<p>实现分析:</p>
<p>首先根据需求可以确定是把一些特定的位置替换为“,”,接下来就是分析并找到这些位置的规律,并抽象出来以正则表达式来表示。</p>
<p>1、 这个位置的左侧必须为数字</p>
<p>2、 这个位置右侧到出现“.”或结尾为止,必须是数字,且数字的个数必须为3的倍数</p>
<p>3、 这个位置左侧相隔任意个数字不能出现“.”</p>
<p>由以上三条,就可以完全确定这些位置,只要实现以上三条,组合一下正则表达式就可以了。</p>
<p>根据分析,最终匹配的结果是一个位置,所以所有子表达式都要求是零宽度。</p>
<p>1、 是对当前所在位置左侧附加的条件,所以要用到逆序环视,因为要求必须出现,所以是肯定的,符合这一条件的子表达式即为“<strong>(?<=\d)</strong>”</p>
<p>2、 是对当前所在位置右侧附加的条件,所以要用到顺序环视,也是要求出现,所以是肯定的,是数字,且个数为3的倍数,即“<strong>(?=(?:\d{3})*)</strong>”,到出现“.”或结尾为止,即“<strong>(?=(?:\d{3})*(?:\.|$))</strong>”</p>
<p>3、 是对当前所在位置左侧附加的条件,所以要用到逆序环视,因为要求不能出现,所以是否定的,即“<strong>(?<!\.\d*)</strong>”</p>
<p>因为零宽度的子表达式是非互斥的,最后匹配的都是同一个位置,所以先后顺序是不影响最后的匹配结果的,可以任意组合,只是习惯上把逆序环视写在左侧,顺序环视写在右侧。</p>
<h2>一、?=、?!、?<=、?<!、?:的解释</h2>
<p> </p>
<h3>1. 先看一下比较官方的解释</h3>
<ul>
<li>(?=pattern):正向先行断言,表示匹配位置后面必须紧跟着满足 pattern 的字符串,但不包括这个字符串在匹配结果中。</li>
<li>(?!pattern):负向先行断言,表示匹配位置后面不能紧跟着满足 pattern 的字符串,也不包括这个字符串在匹配结果中。</li>
<li>(?<=pattern):正向后行断言,表示匹配位置前面必须是满足 pattern 的字符串,但不包括这个字符串在匹配结果中。</li>
<li>(?<!pattern):负向后行断言,表示匹配位置前面不能是满足 pattern 的字符串,也不包括这个字符串在匹配结果中。</li>
<li>(?:pattern):非捕获型分组,表示将 pattern 包含在一个分组中,但不把这个分组的匹配结果保存到分组编号中。这个分组通常用于表示可选的或重复的子表达式,或者是限制量词的作用范围,而不需要把它们的匹配结果单独提取出来。</li>
</ul>
<p> </p>
<h3>2. 再看一下比较通俗易懂的解释:</h3>
<ul>
<li>RegExp1(?=RegExp2) 匹配后面是RegExp2 的 RegExp1</li>
<li>RegExp1(?!RegExp2) 匹配后面不是RegExp2 的 RegExp1</li>
<li>(?<=RegExp2)RegExp1 匹配前面是RegExp2 的 RegExp1</li>
<li>(?<!RegExp2)RegExp1 匹配前面不是RegExp2 的 RegExp1</li>
<li>(?:RegExp) 这个等下单独解释,与上面的不太一样</li>
</ul>
<p>是不是有点明白了,其实?=、?!、?<=、?<!的意思可以理解为 if 判断,即只有先通过它们(RegExp2)的判断之后,才可以获取到正则(RegExp1)的匹配结果。</p>
<p> </p>
<h3>3. 零宽度断言</h3>
<p>?=、?!、?<=、?<!其实就是正则表达式中的<strong>零宽度断言</strong>,以上面的举例来解释↓</p>
<p> RegExp2匹配到的内容是不会返回的,也不会消耗匹配到的字符,只会返回RegExp1的匹配结果,这就是零宽度断言,零宽度断言在正则表达式中非常有用,因为它们可以在不改变匹配结果的情况下,对匹配位置前后的内容进行限制或判断。</p>
<p> </p>
<h3>4. ?: 的解释</h3>
<p>(?:) 并不是零宽度断言,而是非捕获组,它跟普通的括号 () 的区别在于,它不会保存匹配到的内容,但是它仍然会消耗字符并返回匹配内容,只是不会保存匹配结果。</p>
<ul>
<li>()表示捕获分组,它会把匹配到的内容保存到内存中,开发者可以使用$n(n是一个数字)来代表第n个()中匹配到的内容</li>
<li>(?:)表示非捕获组,它匹配的内容不会被保存,所以无法使用$n获取,但也因为没有被保存所以节省了一部分内存空间</li>
</ul>
<p> </p>
<h2>二、举例</h2>
<p> </p>
<h3>?=</h3>
<pre>
'我喜欢苹果'.replace(/我喜欢(?=苹果)/,'我讨厌') // 匹配 我喜欢苹果 中的 我喜欢 并替换为 我讨厌,因为是零宽度断言所以不包含苹果,故结果为 我讨厌苹果
'我喜欢橘子'.replace(/我喜欢(?=苹果)/,'我讨厌') // 我喜欢后面不是苹果,所以这里正则未通过,匹配不到任何内容,故结果仍为 我喜欢橘子</pre>
<p> </p>
<h3>?!</h3>
<pre>
'我喜欢苹果'.replace(/我喜欢(?!苹果)/,'我讨厌') // 匹配后面不是苹果的我喜欢,正则未通过,故结果仍为 我喜欢苹果
'我喜欢橘子'.replace(/我喜欢(?!苹果)/,'我讨厌') // 正则通过,匹配到 我喜欢 进行替换,因为是零宽度断言所以橘子不在匹配结果中,故结果为 我讨厌橘子</pre>
<p> </p>
<h3>?<=</h3>
<pre>
'我喜欢苹果'.replace(/(?<=我喜欢)苹果/,'西红柿') // 匹配到 苹果 ,故结果为 我喜欢西红柿
'我喜欢橘子'.replace(/(?<=我喜欢)苹果/,'西红柿') // 匹配不通过,故结果仍为 我喜欢橘子</pre>
<p> </p>
<h3>?<</h3>
<pre>
'我讨厌苹果'.replace(/(?<!我喜欢)苹果/,'西红柿') // 匹配到 苹果 ,故结果为 我讨厌西红柿
'我喜欢苹果'.replace(/(?<!我喜欢)苹果/,'西红柿') // 匹配不通过,故结果仍为 我喜欢苹果</pre>
<p> </p>
<h3>?:</h3>
<pre>
'hello world'.replace(/(?:hello) (world)/,'$1') // 匹配内容为hello world,但是hello并没有被保存,因此$1取的是world,故结果为world</pre>
<p> </p>
<h2>三、特殊情况</h2>
<p>正则平时我们很少会自己写,一般都是复制别人的~~~(别人的才是最好的)。然后就经常看到一种写法,比如:</p>
<p>/(?=.*){5,10}/</p>
<p>这时候可能有些人就想,咦,(?=)不都是符合条件后匹配它前面的内容吗?这里为什么能放在开头 呢,他前面没内容啊?其实大家可以这么理解,当(?=)前面没有内容,或者说(?=)被放在正则开头使用时,(?=)的作用就相当于检索全部内容是否符合它的要求,如果不符合也就没必要继续向后匹配了,这就很像if判断,只有当条件为true时,才能执行后面的内容。</p>
<p>所以这里的正则意为:先检查内容中是否至少包含一个大写字母,如果有,则继续检查并匹配5~10个大小写字母,将这5~10个大小写字母作为结果返回。</p>
<p> </p>
<h2>四、实例应用</h2>
<p>姓名脱敏(添加*号)</p>
<pre>
'李小龙'.replace(/(?<=[\u4e00-\u9fa5])[\u4e00-\u9fa5]/g, '*') // 李**</pre>
<p>手机号/银行账号脱敏</p>
<pre>
'13912345678'.replace(/(?<=\d{3})\d(?=\d{3})/g, '*') // 139*****678</pre>
<p>强密码规则校验</p>
<pre>
// 密码不能为空,8-30位,至少包含一个大写字母、小写字母、数字、特殊字符
/^(?=.*)(?=.*)(?=.*)(?=.*[\W_])(?!.*[\u4e00-\u9fa5])(?!\s){8,30}$/</pre>
<p> </p>
<h2>“?”的几种用法</h2>
<ul>
<li>“?”元字符规定其前导对象必须在目标对象中连续出现零次或一次。</li>
<li>当该字符紧跟在任何一个其他限制符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串“oooo”,“o+?”将匹配单个“o”,而“o+”将匹配所有“o”。</li>
<li>(?:pattern) ——匹配pattern但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用或字符“(|)”来组合一个模式的各个部分是很有用。例如“industr(?:y|ies)”就是一个比“industry|industries”更简略的表达式。</li>
<li>(?=pattern)——正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,“Windows(?=95|98|NT|2000)”能匹配“Windows2000”中的“Windows”,但不能匹配“Windows3.1”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。</li>
<li>(?!pattern)——正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如“Windows(?!95|98|NT|2000)”能匹配“Windows3.1”中的“Windows”,但不能匹配“Windows2000”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始</li>
<li>(?<=pattern)——反向肯定预查,与正向肯定预查类拟,只是方向相反。例如,“(?<=95|98|NT|2000)Windows”能匹配“2000Windows”中的“Windows”,但不能匹配“3.1Windows”中的“Windows”。</li>
<li>(?<!pattern)——反向否定预查,与正向否定预查类拟,只是方向相反。例如“(?<!95|98|NT|2000)Windows”能匹配“3.1Windows”中的“Windows”,但不能匹配“2000Windows”中的“Windows”。</li>
<li>(?i)——该表达式右边的字符忽略大小写</li>
<li>(?-i)——该表达式右边的字符区分大小写</li>
<li>(?i:x)——x 忽略大小写</li>
<li>(?-i:x)——x 区分大小写</li>
<li>?和懒惰匹配——尽可能少的匹配,例如:源字符串str=“dxxddxxd”中,d\w*?会匹配 dx,而d\w*?d会匹配 dxxd。</li>
</ul>
<p> </p>
<h2>总结 </h2>
<p>源文地址:https://www.jb51.net/program/3178780y8.htm</p>
<h1>正则表达式(?=)正向先行断言实战案例</h1>
x(?=y)称为先行断言(Positive look-ahead),x只有在y前面才匹配,y不会被计入返回结果,比如要匹配后面跟着百分号的数字,可以写成/\d+(?=%)/,这篇文章主要给大家介绍了关于正则表达式(?=)正向先行断言的相关资料,需要的朋友可以参考下
<p>最近在练习正则表达式,遇到了一道很有意思的题,题目如下</p>
<p><img alt="" src="data/attachment/forum/202403/25/a2e3d4007301e409e81f2161fa2f06fb.png" aid="823" /></p>
<p>我的答案如下</p>
<pre>
(?=.*?)(?=.*?\d)(?=.*?).{8,}</pre>
<p>对于这个答案的理解得先从正向先行断言的语法开始说起。</p>
<p>正向先行断言的语法格式如下</p>
<pre>
expression1(?=expression2)
# 查找expression2前面的expression1</pre>
<p>当然这个<strong>expression1</strong>也可以不写(也就是为<strong>空白符</strong>)</p>
<p>例子如下</p>
<p><img alt="" src="data/attachment/forum/202403/25/e695ac974979f45e90298c165ecb52f2.jpg" aid="819" /></p>
<p>该正则表达式的意思为:寻找<strong>abcd</strong>字符串前的<strong>123456</strong>字符串。</p>
<p>这里也提一个有意思的地方</p>
<p><img alt="" src="data/attachment/forum/202403/25/107193cf0842b693440dd4473d0554f6.png" aid="820" /></p>
<p><img alt="" src="data/attachment/forum/202403/25/0b90fa08c4fb2d571314fcea1ef32489.png" aid="818" /></p>
<p>以上两个正则表达式中的<strong>/\d+/gm</strong>和<strong>/123456/gm</strong>其实都能匹配<strong>123456</strong>这个字符串,但在正向先行断言中,前者会匹配每个数字前面的<strong>空白符</strong>,后者将123456字符串当成一个整体,只匹配这个整体前面的<strong>空白符。</strong></p>
<p>这里面的原理还需要等我研究一下,估计是跟底层代码的实现有关,我猜测是<strong>(?=\d+)</strong>在匹配的时候会将每个数字单独提取出然后向前比较。</p>
<p>那么回到该题的答案中,先让我们看看 <strong>(?=.*?) </strong>是什么意思。</p>
<p><img alt="" src="data/attachment/forum/202403/25/6b4c9022b767a3a9936e2d1c2f25c52d.png" aid="821" /></p>
<p>很明显上图匹配了大写字母<strong>A</strong>前面的所有<strong>空白符</strong></p>
<p>其中的<strong>.*?</strong>代表大写字母及其前面的字符串且为懒惰匹配</p>
<p><img alt="" src="data/attachment/forum/202403/25/c1a43116980b11be68a1c75ca62e4814.png" aid="822" /></p>
<p>那么<strong>(?=.*?)(?=.*?\d)</strong>的意思就有点套娃了,按我的理解就是对于<strong>(?=.*?\d)</strong>而言把<strong>(?=.*?)</strong>当成<strong>expression1</strong>,对于<strong>(?=.*?)</strong>而言就是把<strong>空白符</strong>当成<strong>expression1</strong>。</p>
<p>那么这个正则表达式就表示为:在寻找到<strong>每个大写字母前面的所有空白符</strong>的基础上还要满足:这些空白符都在<strong>每个数字前面的所有空白符</strong>这个匹配集合中。相当于是<strong>两个空白符集合的交集</strong>。</p>
<p>所以<strong>(?=.*?)(?=.*?\d)(?=.*?)</strong>相当于是<strong>每个大写字母、小写字母、数字前面的所有空白字符的交集</strong>。</p>
<p><img alt="" src="data/attachment/forum/202403/25/ca906a37aef939094fb6065aa2d197f7.png" aid="824" /></p>
<p> 而后面的<strong>.{8,}</strong>则匹配这些空白字符后面<strong>至少八位字符(贪婪匹配)</strong>。</p>
<p><img alt="" src="data/attachment/forum/202403/25/0673d78c7bf2a267a0c99dc8c726ec87.jpg" aid="825" /></p>
<p><strong>附:先行否定断言</strong></p>
<p>x(?!y)称为先行否定断言(Negative look-ahead),x只有不在y前面才匹配,y不会被计入返回结果。比如,要匹配后面跟的不是百</p>
<p>分号的数字,就要写成/\d+(?!%)/。</p>
<pre>
/\d+(?!\.)/.exec('3.14') // ["14"]
// ["14"]
</pre>
<p>上面代码中,正则表达式指定,只有不在小数点前面的数字才会被匹配,因此返回的结果就是14。</p>
<p>“先行否定断言”中,括号里的部分是不会返回的。</p>
<pre>
var m = 'abd'.match(/b(?!c)/);
m // ['b']
</pre>
<p>上面的代码使用了先行否定断言,b不在c前面所以被匹配,而且括号对应的d不会被返回。</p>
<p><br />
源文地址:https://www.jb51.net/article/267757.htm</p>
页:
[1]