杰克工作室 发表于 2023-2-22 20:57

正则基础之 环视 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>(?&lt;=Expression)</strong></p>
                        </td>
                        <td>
                        <p>逆序肯定环视,表示所在位置左侧能够匹配Expression</p>
                        </td>
                </tr>
                <tr>
                        <td>
                        <p><strong>(?&lt;!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>&nbsp;对于环视的叫法,有的文档里叫预搜索,有的叫什么什么断言的,这里使用了更多人容易接受的《精通正则表达式》中&ldquo;环视&rdquo;的叫法,其实叫什么无所谓,只要知道是什么作用就是了,就这么几个语法规则,&nbsp;还是很容易记的</p>

<h1>2、环视匹配原理</h1>

<p>&nbsp;环视是正则中的一个难点,对于环视的理解,可以从应用和原理两个角度理解,如果想理解得更清晰、深入一些,还是从原理的角度理解好一些,正则匹配基本原理参考 NFA引擎匹配原理。</p>

<p>上面提到环视相当于对&ldquo;所在位置&rdquo;附加了一个条件,环视的难点在于找到这个&ldquo;位置&rdquo;,这一点解决了,环视也就没什么秘密可言了。</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>&nbsp;</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&lt;p&gt;one&lt;/p&gt;bb&lt;div&gt;two&lt;/div&gt;cc</strong></p>

<p>正则表达式:<strong>&lt;</strong><strong>(?!/?p\b)</strong><strong>[^&gt;]+</strong><strong>&gt;</strong></p>

<p>这个正则的意义就是匹配除&lt;p&hellip;&gt;或&lt;/p&gt;之外的其余标签。</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>首先由字符&ldquo;<strong>&lt;</strong>&rdquo;取得控制权,从位置0开始匹配,由于&ldquo;<strong>&lt;</strong>&rdquo;匹配&ldquo;<strong>a</strong>&rdquo;失败,在位置0处整个表达式匹配失败,第一次迭代匹配失败,正则引擎向前传动,由位置1处开始尝试第二次迭代匹配。</p>

<p>重复以上过程,直到位置2,&ldquo;<strong>&lt;</strong>&rdquo;匹配&ldquo;<strong>&lt;</strong>&rdquo;成功,控制权交给&ldquo;<strong>(?!/?p\b)</strong>&rdquo;;&ldquo;<strong>(?!/?p\b)</strong>&rdquo;子表达式取得控制权后,进行内部子表达式的匹配。首先由&ldquo;<strong>/?</strong>&rdquo;取得控制权,尝试匹配&ldquo;<strong>p</strong>&rdquo;失败,进行回溯,不匹配,控制权交给&ldquo;<strong>p</strong>&rdquo;;由&ldquo;<strong>p</strong>&rdquo;来尝试匹配&ldquo;<strong>p</strong>&rdquo;,匹配成功,控制权交给&ldquo;<strong>\b</strong>&rdquo;;由&ldquo;<strong>\b</strong>&rdquo;来尝试匹配位置4,匹配成功。此时子表达式匹配完成,&ldquo;<strong>/?p\b</strong>&rdquo;匹配成功,那么环视表达式&ldquo;<strong>(?!/?p\b)</strong>&rdquo;就匹配失败。在位置2处整个表达式匹配失败,新一轮迭代匹配失败,正则引擎向前传动,由位置3处开始尝试下一轮迭代匹配。</p>

<p>在位置8处也会遇到一轮&ldquo;<strong>/?p\b</strong>&rdquo;匹配&ldquo;<strong>/p</strong>&rdquo;成功,而导致环视表达式&ldquo;<strong>(?!/?p\b)</strong>&rdquo;匹配失败,从而导致整个表达式匹配失败的过程。</p>

<p>重复以上过程,直到位置14,&ldquo;<strong>&lt;</strong>&rdquo;匹配&ldquo;<strong>&lt;</strong>&rdquo;成功,控制权交给&ldquo;<strong>(?!/?p\b)</strong>&rdquo;;&ldquo;<strong>/?</strong>&rdquo;尝试匹配&ldquo;<strong>d</strong>&rdquo;失败,进行回溯,不匹配,控制权交给&ldquo;<strong>p</strong>&rdquo;;由&ldquo;<strong>p</strong>&rdquo;来尝试匹配&ldquo;<strong>d</strong>&rdquo;,匹配失败,已经没有备选状态可供回溯,匹配失败。此时子表达式匹配完成,&ldquo;<strong>/?p\b</strong>&rdquo;匹配失败,那么环视表达式&ldquo;<strong>(?!/?p\b)</strong>&rdquo;就匹配成功。匹配的结果是位置15,然后控制权交给&ldquo;<strong>[^&gt;]+</strong>&rdquo;;由&ldquo;<strong>[^&gt;]+</strong>&rdquo;从位置15进行尝试匹配,可以成功匹配到&ldquo;<strong>div</strong>&rdquo;,控制权交给&ldquo;<strong>&gt;</strong>&rdquo;;由&ldquo;<strong>&gt;</strong>&rdquo;来匹配&ldquo;<strong>&gt;</strong>&rdquo;。</p>

<p>此时正则表达式匹配完成,报告匹配成功。匹配结果为&ldquo;<strong>&lt;div&gt;</strong>&rdquo;,开始位置为14,结束位置为19。其中&ldquo;<strong>&lt;</strong>&rdquo;匹配&ldquo;<strong>&lt;</strong>&rdquo;,&ldquo;<strong>(?!/?p\b)</strong>&rdquo;匹配位置15,&ldquo;<strong>[^&gt;]+</strong>&rdquo;匹配字符串&ldquo;<strong>div</strong>&rdquo;,&ldquo;<strong>&gt;</strong>&rdquo;匹配&ldquo;<strong>&gt;</strong>&rdquo;。</p>

<h2>逆序环视基础</h2>

<p>对于逆序肯定环视<strong>(?&lt;=Expression)</strong>来说,当子表达式<strong>Expression</strong>匹配成功时,<strong>(?&lt;=Expression)</strong>匹配成功,并报告<strong>(?&lt;=Expression)</strong>匹配当前位置成功。</p>

<p>对于逆序否定环视<strong>(?&lt;!Expression)</strong>来说,当子表达式<strong>Expression</strong>匹配成功时,<strong>(?&lt;!Expression)</strong>匹配失败;当子表达式<strong>Expression</strong>匹配失败时,<strong>(?&lt;!Expression)</strong>匹配成功,并报告<strong>(?&lt;!Expression)</strong>匹配当前位置成功;</p>

<p>顺序环视相当于在当前位置右侧附加一个条件,所以它的匹配尝试是从当前位置开始的,然后向右尝试匹配,直到某一位置使得匹配成功或失败为止。而逆序环视的特殊处在于,它相当于在当前位置左侧附加一个条件,所以它不是在当前位置开始尝试匹配的,而是从当前位置左侧某一位置开始,匹配到当前位置为止,报告匹配成功或失败。</p>

<p>顺序环视尝试匹配的起点是确定的,就是当前位置,而匹配的终点是不确定的。逆序环视匹配的起点是不确定的,是当前位置左侧某一位置,而匹配的终点是确定的,就是当前位置。</p>

<p>所以顺序环视相对是简单的,而逆序环视相对是复杂的。这也就是为什么大多数语言和工具都提供了对顺序环视的支持,而只有少数语言提供了对逆序环视支持的原因。</p>

<p>JavaScript中只支持顺序环视,不支持逆序环视。</p>

<p>Java中虽然顺序环视和逆序环视都支持,但是逆序环视只支持长度确定的表达式,逆序环视中量词只支持&ldquo;?&rdquo;,不支持其它长度不定的量词。长度确定时,引擎可以向左查找固定长度的位置作为起点开始尝试匹配,而如果长度不确定时,就要从位置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>&lt;div&gt;a test&lt;/div&gt;</strong></p>

<p>正则表达式:<strong>(?&lt;=&lt;div&gt;)</strong><strong>[^&lt;]+</strong><strong>(?=&lt;/div&gt;)</strong></p>

<p>这个正则的意义就是匹配&lt;div&gt;和&lt;/div&gt;标签之间的内容,而不包括&lt;div&gt;和&lt;/div&gt;标签本身。</p>

<p>匹配过程:</p>

<p>首先由&ldquo;<strong>(?&lt;=&lt;div&gt;)</strong>&rdquo;取得控制权,从位置0开始匹配,由于位置0是起始位置,左侧没有任何内容,所以&ldquo;<strong>&lt;div&gt;</strong>&rdquo;必然匹配失败,从而环视表达式&ldquo;<strong>(?&lt;=&lt;div&gt;)</strong>&rdquo;匹配失败,导致整个表达式在位置0处匹配失败。第一轮迭代匹配失败,正则引擎向前传动,由位置1处开始尝试第二次迭代匹配。</p>

<p>直到传动到位置5,&ldquo;<strong>(?&lt;=&lt;div&gt;)</strong>&rdquo;取得控制权,向左查找5个位置,由位置0开始匹配,由&ldquo;<strong>&lt;div&gt;</strong>&rdquo;匹配&ldquo;<strong>&lt;div&gt;</strong>&rdquo;成功,从而&ldquo;<strong>(?&lt;=&lt;div&gt;)</strong>&rdquo;匹配成功,匹配的结果为位置5,控制权交给&ldquo;<strong>[^&lt;]+</strong>&rdquo;;&ldquo;<strong>[^&lt;]+</strong>&rdquo;从位置5开始尝试匹配,匹配&ldquo;<strong>a test</strong>&rdquo;成功,控制权交给&ldquo;<strong>(?=&lt;/div&gt;)</strong>&rdquo;;由&ldquo;<strong>&lt;/div&gt;</strong>&rdquo;匹配&ldquo;<strong>&lt;/div&gt;</strong>&rdquo;成功,从而&ldquo;<strong>(?=&lt;/div&gt;)</strong>&rdquo;匹配成功,匹配结果为位置11。</p>

<p>此时正则表达式匹配完成,报告匹配成功。匹配结果为&ldquo;<strong>a test</strong>&rdquo;,开始位置为5,结束位置为11。其中&ldquo;<strong>(?&lt;=&lt;div&gt;)</strong>&rdquo;匹配位置5,&ldquo;<strong>[^&lt;]+</strong>&rdquo;匹配&ldquo;<strong>a test</strong>&rdquo;,&ldquo;<strong>(?=&lt;/div&gt;)</strong>&rdquo;匹配位置11。</p>

<p>逆序否定环视的匹配过程与上述过程类似,区别只是当<strong>Expression</strong>匹配失败时,逆序否定表达式<strong>(?&lt;!Expression)</strong>才匹配成功。</p>

<p>到此环视的匹配原理已基本讲解完,环视也就没有什么秘密可言了,所需要的,也只是多加练习而已。</p>

<h1>3、环视应用</h1>

<p>今天写累了,暂时就给出一个环视的综合应用实例吧,至于环视的应用场景和技巧,后面再整理。</p>

<p>需求:数字格式化成用&ldquo;,&rdquo;的货币格式。</p>

<p>正则表达式:<strong>(?&lt;=\d)(?&lt;!\.\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>&nbsp;&nbsp;&nbsp; richTextBox2.Text += &quot;源字符串:&quot; + d.ToString().PadRight(15) + &quot;格式化:&quot; + Regex.Replace(d.ToString(), @&quot;(?&lt;=\d)(?&lt;!\.\d*)(?=(?:\d{3})+(?:\.\d+|$))&quot;, &quot;,&quot;) + &quot;\n&quot;;</p>

<p>}</p>

<p>输出结果:</p>

<p>源字符串:0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 格式化:0</p>

<p>源字符串:12&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 格式化:12</p>

<p>源字符串:123&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 格式化:123</p>

<p>源字符串:1234&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 格式化:1,234</p>

<p>源字符串:12345&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 格式化:12,345</p>

<p>源字符串:123456&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 格式化:123,456</p>

<p>源字符串:1234567&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 格式化:1,234,567</p>

<p>源字符串:123456789&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 格式化:123,456,789</p>

<p>源字符串:1234567890&nbsp;&nbsp;&nbsp;&nbsp; 格式化:1,234,567,890</p>

<p>源字符串:12.345&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 格式化:12.345</p>

<p>源字符串:123.456&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 格式化:123.456</p>

<p>源字符串:1234.56&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 格式化:1,234.56</p>

<p>源字符串:12345.6789&nbsp;&nbsp;&nbsp;&nbsp; 格式化:12,345.6789</p>

<p>源字符串:123456.789&nbsp;&nbsp;&nbsp;&nbsp; 格式化:123,456.789</p>

<p>源字符串:1234567.89&nbsp;&nbsp;&nbsp;&nbsp; 格式化:1,234,567.89</p>

<p>源字符串:12345678.9&nbsp;&nbsp;&nbsp;&nbsp; 格式化:12,345,678.9</p>

<p>实现分析:</p>

<p>首先根据需求可以确定是把一些特定的位置替换为&ldquo;,&rdquo;,接下来就是分析并找到这些位置的规律,并抽象出来以正则表达式来表示。</p>

<p>1、&nbsp;&nbsp; 这个位置的左侧必须为数字</p>

<p>2、&nbsp;&nbsp; 这个位置右侧到出现&ldquo;.&rdquo;或结尾为止,必须是数字,且数字的个数必须为3的倍数</p>

<p>3、&nbsp;&nbsp; 这个位置左侧相隔任意个数字不能出现&ldquo;.&rdquo;</p>

<p>由以上三条,就可以完全确定这些位置,只要实现以上三条,组合一下正则表达式就可以了。</p>

<p>根据分析,最终匹配的结果是一个位置,所以所有子表达式都要求是零宽度。</p>

<p>1、&nbsp;&nbsp; 是对当前所在位置左侧附加的条件,所以要用到逆序环视,因为要求必须出现,所以是肯定的,符合这一条件的子表达式即为&ldquo;<strong>(?&lt;=\d)</strong>&rdquo;</p>

<p>2、&nbsp;&nbsp; 是对当前所在位置右侧附加的条件,所以要用到顺序环视,也是要求出现,所以是肯定的,是数字,且个数为3的倍数,即&ldquo;<strong>(?=(?:\d{3})*)</strong>&rdquo;,到出现&ldquo;.&rdquo;或结尾为止,即&ldquo;<strong>(?=(?:\d{3})*(?:\.|$))</strong>&rdquo;</p>

<p>3、&nbsp;&nbsp; 是对当前所在位置左侧附加的条件,所以要用到逆序环视,因为要求不能出现,所以是否定的,即&ldquo;<strong>(?&lt;!\.\d*)</strong>&rdquo;</p>

<p>因为零宽度的子表达式是非互斥的,最后匹配的都是同一个位置,所以先后顺序是不影响最后的匹配结果的,可以任意组合,只是习惯上把逆序环视写在左侧,顺序环视写在右侧。</p>

杰克工作室 发表于 2024-3-25 09:24

<h2>一、?=、?!、?<=、?<!、?:的解释</h2>

<p>&nbsp;</p>

<h3>1. 先看一下比较官方的解释</h3>

<ul>
        <li>(?=pattern):正向先行断言,表示匹配位置后面必须紧跟着满足&nbsp;pattern&nbsp;的字符串,但不包括这个字符串在匹配结果中。</li>
        <li>(?!pattern):负向先行断言,表示匹配位置后面不能紧跟着满足&nbsp;pattern&nbsp;的字符串,也不包括这个字符串在匹配结果中。</li>
        <li>(?&lt;=pattern):正向后行断言,表示匹配位置前面必须是满足&nbsp;pattern&nbsp;的字符串,但不包括这个字符串在匹配结果中。</li>
        <li>(?&lt;!pattern):负向后行断言,表示匹配位置前面不能是满足&nbsp;pattern&nbsp;的字符串,也不包括这个字符串在匹配结果中。</li>
        <li>(?:pattern):非捕获型分组,表示将&nbsp;pattern&nbsp;包含在一个分组中,但不把这个分组的匹配结果保存到分组编号中。这个分组通常用于表示可选的或重复的子表达式,或者是限制量词的作用范围,而不需要把它们的匹配结果单独提取出来。</li>
</ul>

<p>&nbsp;</p>

<h3>2. 再看一下比较通俗易懂的解释:</h3>

<ul>
        <li>RegExp1(?=RegExp2)&nbsp; 匹配后面是RegExp2&nbsp;的&nbsp;RegExp1</li>
        <li>RegExp1(?!RegExp2)&nbsp; 匹配后面不是RegExp2&nbsp;的&nbsp;RegExp1</li>
        <li>(?&lt;=RegExp2)RegExp1&nbsp; 匹配前面是RegExp2&nbsp;的&nbsp;RegExp1</li>
        <li>(?&lt;!RegExp2)RegExp1&nbsp; 匹配前面不是RegExp2&nbsp;的&nbsp;RegExp1</li>
        <li>(?:RegExp)&nbsp; 这个等下单独解释,与上面的不太一样</li>
</ul>

<p>是不是有点明白了,其实?=、?!、?<=、?<!的意思可以理解为&nbsp;if 判断,即只有先通过它们(RegExp2)的判断之后,才可以获取到正则(RegExp1)的匹配结果。</p>

<p>&nbsp;</p>

<h3>3. 零宽度断言</h3>

<p>?=、?!、?<=、?<!其实就是正则表达式中的<strong>零宽度断言</strong>,以上面的举例来解释&darr;</p>

<p>&nbsp;RegExp2匹配到的内容是不会返回的,也不会消耗匹配到的字符,只会返回RegExp1的匹配结果,这就是零宽度断言,零宽度断言在正则表达式中非常有用,因为它们可以在不改变匹配结果的情况下,对匹配位置前后的内容进行限制或判断。</p>

<p>&nbsp;</p>

<h3>4. ?: 的解释</h3>

<p>(?:) 并不是零宽度断言,而是非捕获组,它跟普通的括号 () 的区别在于,它不会保存匹配到的内容,但是它仍然会消耗字符并返回匹配内容,只是不会保存匹配结果。</p>

<ul>
        <li>()表示捕获分组,它会把匹配到的内容保存到内存中,开发者可以使用$n(n是一个数字)来代表第n个()中匹配到的内容</li>
        <li>(?:)表示非捕获组,它匹配的内容不会被保存,所以无法使用$n获取,但也因为没有被保存所以节省了一部分内存空间</li>
</ul>

<p>&nbsp;</p>

<h2>二、举例</h2>

<p>&nbsp;</p>

<h3>?=</h3>

<pre>
&#39;我喜欢苹果&#39;.replace(/我喜欢(?=苹果)/,&#39;我讨厌&#39;) // 匹配 我喜欢苹果 中的 我喜欢 并替换为 我讨厌,因为是零宽度断言所以不包含苹果,故结果为 我讨厌苹果
&#39;我喜欢橘子&#39;.replace(/我喜欢(?=苹果)/,&#39;我讨厌&#39;) // 我喜欢后面不是苹果,所以这里正则未通过,匹配不到任何内容,故结果仍为 我喜欢橘子</pre>

<p>&nbsp;</p>

<h3>?!</h3>

<pre>
&#39;我喜欢苹果&#39;.replace(/我喜欢(?!苹果)/,&#39;我讨厌&#39;) // 匹配后面不是苹果的我喜欢,正则未通过,故结果仍为 我喜欢苹果
&#39;我喜欢橘子&#39;.replace(/我喜欢(?!苹果)/,&#39;我讨厌&#39;) // 正则通过,匹配到 我喜欢 进行替换,因为是零宽度断言所以橘子不在匹配结果中,故结果为 我讨厌橘子</pre>

<p>&nbsp;</p>

<h3>?&lt;=</h3>

<pre>
&#39;我喜欢苹果&#39;.replace(/(?&lt;=我喜欢)苹果/,&#39;西红柿&#39;) // 匹配到 苹果 ,故结果为 我喜欢西红柿
&#39;我喜欢橘子&#39;.replace(/(?&lt;=我喜欢)苹果/,&#39;西红柿&#39;) // 匹配不通过,故结果仍为 我喜欢橘子</pre>

<p>&nbsp;</p>

<h3>?&lt;</h3>

<pre>
&#39;我讨厌苹果&#39;.replace(/(?&lt;!我喜欢)苹果/,&#39;西红柿&#39;) // 匹配到 苹果 ,故结果为 我讨厌西红柿
&#39;我喜欢苹果&#39;.replace(/(?&lt;!我喜欢)苹果/,&#39;西红柿&#39;) // 匹配不通过,故结果仍为 我喜欢苹果</pre>

<p>&nbsp;</p>

<h3>?:</h3>

<pre>
&#39;hello world&#39;.replace(/(?:hello) (world)/,&#39;$1&#39;) // 匹配内容为hello world,但是hello并没有被保存,因此$1取的是world,故结果为world</pre>

<p>&nbsp;</p>

<h2>三、特殊情况</h2>

<p>正则平时我们很少会自己写,一般都是复制别人的~~~(别人的才是最好的)。然后就经常看到一种写法,比如:</p>

<p>/(?=.*){5,10}/</p>

<p>这时候可能有些人就想,咦,(?=)不都是符合条件后匹配它前面的内容吗?这里为什么能放在开头 呢,他前面没内容啊?其实大家可以这么理解,当(?=)前面没有内容,或者说(?=)被放在正则开头使用时,(?=)的作用就相当于检索全部内容是否符合它的要求,如果不符合也就没必要继续向后匹配了,这就很像if判断,只有当条件为true时,才能执行后面的内容。</p>

<p>所以这里的正则意为:先检查内容中是否至少包含一个大写字母,如果有,则继续检查并匹配5~10个大小写字母,将这5~10个大小写字母作为结果返回。</p>

<p>&nbsp;</p>

<h2>四、实例应用</h2>

<p>姓名脱敏(添加*号)</p>

<pre>
&#39;李小龙&#39;.replace(/(?&lt;=[\u4e00-\u9fa5])[\u4e00-\u9fa5]/g, &#39;*&#39;) // 李**</pre>

<p>手机号/银行账号脱敏</p>

<pre>
&#39;13912345678&#39;.replace(/(?&lt;=\d{3})\d(?=\d{3})/g, &#39;*&#39;) // 139*****678</pre>

<p>强密码规则校验</p>

<pre>
// 密码不能为空,8-30位,至少包含一个大写字母、小写字母、数字、特殊字符
/^(?=.*)(?=.*)(?=.*)(?=.*[\W_])(?!.*[\u4e00-\u9fa5])(?!\s){8,30}$/</pre>

<p>&nbsp;</p>

<h2>&ldquo;?&rdquo;的几种用法</h2>

<ul>
        <li>&ldquo;?&rdquo;元字符规定其前导对象必须在目标对象中连续出现零次或一次。</li>
        <li>当该字符紧跟在任何一个其他限制符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串&ldquo;oooo&rdquo;,&ldquo;o+?&rdquo;将匹配单个&ldquo;o&rdquo;,而&ldquo;o+&rdquo;将匹配所有&ldquo;o&rdquo;。</li>
        <li>(?:pattern) &mdash;&mdash;匹配pattern但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用或字符&ldquo;(|)&rdquo;来组合一个模式的各个部分是很有用。例如&ldquo;industr(?:y|ies)&rdquo;就是一个比&ldquo;industry|industries&rdquo;更简略的表达式。</li>
        <li>(?=pattern)&mdash;&mdash;正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,&ldquo;Windows(?=95|98|NT|2000)&rdquo;能匹配&ldquo;Windows2000&rdquo;中的&ldquo;Windows&rdquo;,但不能匹配&ldquo;Windows3.1&rdquo;中的&ldquo;Windows&rdquo;。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。</li>
        <li>(?!pattern)&mdash;&mdash;正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如&ldquo;Windows(?!95|98|NT|2000)&rdquo;能匹配&ldquo;Windows3.1&rdquo;中的&ldquo;Windows&rdquo;,但不能匹配&ldquo;Windows2000&rdquo;中的&ldquo;Windows&rdquo;。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始</li>
        <li>(?&lt;=pattern)&mdash;&mdash;反向肯定预查,与正向肯定预查类拟,只是方向相反。例如,&ldquo;(?&lt;=95|98|NT|2000)Windows&rdquo;能匹配&ldquo;2000Windows&rdquo;中的&ldquo;Windows&rdquo;,但不能匹配&ldquo;3.1Windows&rdquo;中的&ldquo;Windows&rdquo;。</li>
        <li>(?&lt;!pattern)&mdash;&mdash;反向否定预查,与正向否定预查类拟,只是方向相反。例如&ldquo;(?&lt;!95|98|NT|2000)Windows&rdquo;能匹配&ldquo;3.1Windows&rdquo;中的&ldquo;Windows&rdquo;,但不能匹配&ldquo;2000Windows&rdquo;中的&ldquo;Windows&rdquo;。</li>
        <li>(?i)&mdash;&mdash;该表达式右边的字符忽略大小写</li>
        <li>(?-i)&mdash;&mdash;该表达式右边的字符区分大小写</li>
        <li>(?i:x)&mdash;&mdash;x 忽略大小写</li>
        <li>(?-i:x)&mdash;&mdash;x 区分大小写</li>
        <li>?和懒惰匹配&mdash;&mdash;尽可能少的匹配,例如:源字符串str=&ldquo;dxxddxxd&rdquo;中,d\w*?会匹配 dx,而d\w*?d会匹配 dxxd。</li>
</ul>

<p>&nbsp;</p>

<h2>总结&nbsp;</h2>

<p>源文地址:https://www.jb51.net/program/3178780y8.htm</p>

杰克工作室 发表于 2024-3-25 09:40

<h1>正则表达式(?=)正向先行断言实战案例</h1>
x(?=y)称为先行断言(Positive&nbsp;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>那么回到该题的答案中,先让我们看看&nbsp;<strong>(?=.*?)&nbsp;</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>&nbsp;而后面的<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(&#39;3.14&#39;) // [&quot;14&quot;]
// [&quot;14&quot;]
</pre>

<p>上面代码中,正则表达式指定,只有不在小数点前面的数字才会被匹配,因此返回的结果就是14。</p>

<p>&ldquo;先行否定断言&rdquo;中,括号里的部分是不会返回的。</p>

<pre>
var m = &#39;abd&#39;.match(/b(?!c)/);
m // [&#39;b&#39;]
</pre>

<p>上面的代码使用了先行否定断言,b不在c前面所以被匹配,而且括号对应的d不会被返回。</p>

<p><br />
源文地址:https://www.jb51.net/article/267757.htm</p>
页: [1]
查看完整版本: 正则基础之 环视 Lookaround