疯流涕淌 發表於 2025-5-24 11:07:45

正则表达式7种高级应用技巧教程

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">概念一:按单字符匹配</a></li><li><a href="#_label1">概念二:匹配优先和不匹配优先</a></li><li><a href="#_label2">概念三:贪婪模式与非贪婪模式</a></li><li><a href="#_label3">概念四:环视(断言/零宽断言)</a></li><li><a href="#_label4">概念五:平衡组</a></li><li><a href="#_label5">概念六:模式修饰符</a></li><li><a href="#_label6">概念七:正则三段论应用</a></li></ul></div><p>本文是一篇正则表达式高级教程,主要通过对正则表达式几个概念的介绍,深入探讨正则表达式高级功能,以期达到通俗化解释正则表达式高深概念的目的。</p>
<p>介绍过正则表达式的基础和基本套路正则三段论:定锚点,去噪点,取数据了,接下来这篇文章,补充一点相对高级的概念:<span>1. 概念一:按单字符匹配 2. 概念二:匹配优先和不匹配优先 3. 概念三:贪婪模式与非贪婪模式 4. 概念四:环视(断言) 5. 概念五:平衡组 6. 概念六:模式修饰符 7. 附:正则三段论应用</span></p>
<p class="maodian"><a name="_label0"></a></p><h2>概念一:按单字符匹配</h2>
<p>正则里面的数据都是按照单个字符来进行匹配的,这个通过数值区间的例子最容易体现出来,比如,示例一:</p>
<p>我要匹配0-15的数值区间,用正则来写的话,便是|1,这里,便是把0-9这种单字符的情况,和10-15这种多字符的情况拆分开了,使用分支|来区分开,表示要么是0-9,要么是10-15。<br />上面是两位数值的情况,现在延伸至1-65535,我个人的处理思想是从大到小,一块块分解:</p>
<div class="jb51code"><pre class="brush:plain;">1. 65530-65535==&gt;6553          末位区间0-5
2. 65500-65529==&gt;655      第四位区间0-2,末位区间0-9
3. 65000-65499==&gt;65{2}    第三位区间0-4,后两位0-9
4. 60000-64999==&gt;6{3}   第二位区间0-4,后三位0-9
5. 10000-59999==&gt;{4}      第一位区间1-5,后四位0-9
6. 1-9999       ==&gt;{0,3}    第一位只能是1-9,后三位可有可无</pre></div>
<p>最后组合起来:<br />(6553|655|65{2}|6{3}|{4}|{0,3})<br />便得到1-65535匹配正则。</p>
<p>根据数据处理需求,可以在正则前后加上^和$,以匹配整个数据串,或者前后加入\b,把它当做单词边界处理。没有限定字符的边界往往是js正则判断中常见的错误之一。</p>
<p class="maodian"><a name="_label1"></a></p><h2>概念二:匹配优先和不匹配优先</h2>
<p>匹配优先和不匹配优先从字面理解也是比较容易的,所谓匹配优先,就是,能匹配我就先匹配;不匹配优先就是,能不匹配我就先不匹配,这段匹配先跳过,先看看后面的匹配能不能通过。</p>
<p class="maodian"><a name="_label2"></a></p><h2>概念三:贪婪模式与非贪婪模式</h2>
<p>正则的贪婪模式和非贪婪模式是一个比较容易混淆的概念,不过,我们可以这么理解,一个人很贪婪,所以他会能拿多少拿多少,换过来,那就是贪婪模式下的正则表达式,能匹配多少就匹配多少,尽可能最多。而非贪婪模式,则是能不匹配就不匹配,尽可能最少。</p>
<p>下面举个例子,示例二:</p>
<div class="jb51code"><pre class="brush:plain;">需求:匹配1后面跟任意个0
源串:10001
使用贪婪模式:10*       结果:1000 和 1
使用非贪婪模式:10*?    结果:1 和 1</pre></div>
<p>首先,*是匹配0个或多个的意思。</p>
<blockquote><p>贪婪模式下,它表示,首先匹配一个1,然后匹配1后面的0,最多可以匹配3个0,因此得到1000,然后第二次又匹配到一个1,但是后面没有0,因此得到1;</p>
<p>非贪婪模式下,它表示,首先匹配一个1,然后1后面的0,能不匹配就不匹配了,所以,它每次都只是匹配了一个1。</p></blockquote>
<p>看到这里,也许,有些人觉得,哎呀,我懂了!<br />那么,下来我们改改,看看你是不是真懂了。</p>
<p>示例三:</p>
<div class="jb51code"><pre class="brush:plain;">需求:匹配1后面跟任意个0,再跟一个1
源串:10001
使用贪婪模式:10*1       结果:10001
使用非贪婪模式:10*?1    结果:10001</pre></div>
<p>为什么这两次的结果一样了?</p>
<p>因为,正则表达式要判断完这整个正则才算成功,这种情况下,</p>
<p>贪婪模式,首先匹配一个1,然后匹配1后面尽可能多的0,发现3个,再匹配0后面的一个1,正则表达式匹配完,完成匹配,得到10001;非贪婪模式,首先匹配一个1,然后,0*?是非贪婪模式,它不想匹配了(非贪婪模式不匹配优先),看看后面是不是1了?然后发现哎妈呀,后面是个0啊,然后它回头,不能再偷懒了,用0*?匹配一个0吧,然后匹配到10,又不想匹配了,看看后面有没有1了,还是没有,又回去用0*?匹配掉一个0,得到100,继续偷懒,但是发现后面还不是1,然后又用0*?匹配得到1000,最后,发现真不容易啊,终于看到1了,匹配得到10001,正则表达式匹配完,完成匹配。</p>
<p>看到这里,是不是懂了?</p>
<p>那究竟哪个模式好呢?</p>
<p>什么时候使用贪婪模式,什么时候使用非贪婪模式,哪个性能好,哪个性能不好,不能一概而论,要根据情况分析。<br />下面我举个例子:<br />源码:</p>
<div class="jb51code"><pre class="brush:xhtml;">&lt;a href="http://www.****.cn/my-regexp" rel="external nofollow"rel="external nofollow"target="_blank" title="我眼里的正则表达式(入门)"&gt;我眼里的正则表达式(入门)&lt;/a&gt;
&lt;a title="我眼里的正则表达式(入门)" href="http://www.****.cn/my-regexp" rel="external nofollow"rel="external nofollow"target="_blank"&gt;我眼里的正则表达式(入门)&lt;/a&gt;</pre></div>
<p>正则1:&lt;a [^&gt;]*?href=&quot;([^&quot;]*?)&quot;[^&gt;]*?&gt;([^&lt;]*?)&lt;/a&gt;(238次)<br />正则2:&lt;a [^&gt;]*?href=&quot;([^&quot;]*)&quot;[^&gt;]*&gt;([^&lt;]*)&lt;/a&gt;(65次)<br />正则3:&lt;a [^&gt;]*href=&quot;([^&quot;]*)&quot;[^&gt;]*&gt;([^&lt;]*)&lt;/a&gt;(136次)<br />附:执行次数的获取请下载正则表达式测试工具:RegexBuddy 4.1.0-正则测试工具.rar,使用里面的Debug功能。</p>
<p>正则1是通用写法,正则2是在确定字符不会溢出的情况下消除非贪婪模式,正则3是证明并不是全部消除非贪婪模式就是最优。</p>
<p>因此,关于贪婪模式好还是非贪婪模式好的讨论,只能说根据需求而定,不过,在平时的时候用,一般使用非贪婪模式较多,因为贪婪模式经常会由于元字符范围限制不严谨而导致匹配越界,得到非预期结果。</p>
<p>在确定的数据结构里,可以尝试使用[^&gt;]*&gt;这样的排除字符贪婪模式替换非贪婪模式,提升匹配的效率。注意,贪婪部分([^&gt;]*)的匹配,最好不要越过其后面的字符(&gt;),否则会导致贪婪模式下的回溯,如正则3,[^&gt;]*的匹配越过了href,一直匹配到&gt;为止,而这时候再匹配href,会匹配不到而导致多次回溯处理,直到回溯到href前的位置,后面才继续了下去。</p>
<p>另外,需要注意一点,无论使用贪婪模式还是非贪婪模式,在不同语言需要注意回溯次数和嵌套次数的限制,比如在PHP中,pcre.backtrack_limit=100000,pcre.recursion_limit=100000。</p>
<p class="maodian"><a name="_label3"></a></p><h2>概念四:环视(断言/零宽断言)</h2>
<p>环视,在不同的地方又称之为零宽断言,简称断言。<br />用一句通俗的话解释:<br />环视,就是先从全局环顾一遍正则,(然后断定结果,)再做进一步匹配处理。<br />断言,就是先从全局环顾一遍正则,然后断定结果,再做进一步匹配处理。</p>
<p>两个虽然字面不一样,意思却是同一个,都是做全局观望,再做进一步处理。</p>
<p>环视的作用相当于对其所在位置加了一个附加条件,只有满足这个条件,环视子表达式才能匹配成功。</p>
<p>环视主要有以下4个用法:<br />(?&lt;=exp)匹配前面是exp的数据<br />(?&lt;!exp)匹配前面不是exp的数据<br />(?=exp)匹配后面是exp的数据<br />(?!exp)匹配后面不是exp的数据</p>
<p>示例四:<br />(?&lt;=B)AAA匹配前面是B的数据,即BAAA匹配,而CAAA不匹配<br />(?&lt;!B)AAA匹配前面不是B的数据,即CAAA匹配,而BAAA不匹配<br />AAA(?=B)匹配后面是B的数据,即AAAB匹配,而AAAC不匹配<br />AAA(?!B)匹配后面不是B的数据,即AAAC能匹配,而AAAB不能匹配</p>
<p>另外,还会看到(?!B)这种写法,其实它是范围里,排除B的意思,前置的(?!B)只是对后面数据的一个限定,从而达到过滤匹配的效果。</p>
<p>因此,环视做排除处理是比较实用的,比如,示例五:</p>
<div class="jb51code"><pre class="brush:plain;">需求:字母、数字组合,不区分大小写,不能纯数字或者纯字母,6-16个字符。
通用正则:^{6,16}$    字母数字组合,6-16个字符
排除纯字母:(?!^+$)
排除纯数字:(?!^+$)
组合起来:(?!^+$)(?!^+$)^{6,16}$</pre></div>
<p>注意,环视部分是不占宽度的,所以有零宽断言的叫法。<br />所谓不占宽度,可以分成两部分理解:<br />1、环视的匹配结果不纳入数据结果<br />2、环视它匹配过的地方,下次还能用它继续匹配。</p>
<p>如果不是环视,则匹配过的地方,不能再匹配第二次了。</p>
<p>上面示例四体现了:环视的匹配结果不纳入数据结果,它的结果:</p>
<div class="jb51code"><pre class="brush:plain;">(?&lt;=B)AAA   源串:BAAA结果:AAA
(?&lt;!B)AAA   源串:CAAA结果:AAA
AAA(?=B)      源串:AAAB结果:AAA
AAA(?!B)      源串:AAAC结果:AAA</pre></div>
<p>而示例五体现了:环视它匹配过的地方,下次还能用它继续匹配<br />因为,整个匹配过程中,正则表达式一共走了3次字符串匹配,第一次匹配不全部是字母,第二次匹配不全部是数字,第三次匹配全部是字母数字组合,6-16个字符。</p>
<div class="jb51code"><pre class="brush:plain;">扩展部分:
`(?&lt;=B)`   范围等于B
`(?&lt;!B)`   范围排除B
`(?!B)`    范围排除B</pre></div>
<p>附: js不支持(?&lt;=exp)和(?&lt;!exp)语法</p>
<p class="maodian"><a name="_label4"></a></p><h2>概念五:平衡组</h2>
<p>平衡组并不是所有程序语言都支持,而我本人使用的PHP语言就不支持,所以平时接触也是比较少的。</p>
<p>平衡组主要用到下面四个语法:</p>
<div class="jb51code"><pre class="brush:plain;">(?'group') 把捕获的内容命名为group,并压入堆栈(Stack)
(?'-group') 从堆栈上弹出最后压入堆栈的名为group的捕获内容,如果堆栈本来为空,则本分组的匹配失败
(?(group)yes|no) 如果堆栈上存在以名为group的捕获内容的话,继续匹配yes部分的表达式,否则继续匹配no部分
(?!) 零宽负向先行断言,由于没有后缀表达式,如没有(?!B)的B,试图匹配总是失败</pre></div>
<p>在PHP中是支持(?(group)yes|no)语法的,这里的group是分组编号,即子模式编号,如(A)?(?(1)yes|no) ,匹配Ayes 和 no</p>
<p>下面这里引用《正则表达式30分钟入门教程#平衡组》关于&lt;&gt;配对匹配的例子,展示平衡组用法,</p>
<div class="jb51code"><pre class="brush:plain;">&lt;                         #最外层的左括号
    [^&lt;&gt;]*                #最外层的左括号后面的不是括号的内容
    (
      (
            (?'Open'&lt;)    #碰到了左括号,在黑板上写一个"Open"
            [^&lt;&gt;]*       #匹配左括号后面的不是括号的内容
      )+
      (
            (?'-Open'&gt;)   #碰到了右括号,擦掉一个"Open"
            [^&lt;&gt;]*      #匹配右括号后面不是括号的内容
      )+
    )*
    (?(Open)(?!))         #在遇到最外层的右括号时,判断黑板上还有没有没擦掉的"Open";如果还有,则匹配失败
&gt;                         #最外层的右括号
平衡组的一个最常见的应用就是匹配HTML,下面这个例子可以匹配嵌套的&lt;div&gt;标签:
&lt;div[^&gt;]*&gt;[^&lt;&gt;]*(((?'Open'&lt;div[^&gt;]*&gt;)[^&lt;&gt;]*)+((?'-Open'&lt;/div&gt;)[^&lt;&gt;]*)+)*(?(Open)(?!))&lt;/div&gt;</pre></div>
<p class="maodian"><a name="_label5"></a></p><h2>概念六:模式修饰符</h2>
<p>模式修饰符在许多程序语言中都支持的,比如最常见的是i,不区分大小写,如javascript里的//i,表示匹配字母数字,不区分大小写。</p>
<p>本人在写php正则时常用的模式修饰符主要有i和s,如:<br />$pattern = &#39;#+#is&#39;;</p>
<p>模式修饰符s的作用主要是的.能够匹配换行,在处理换行数据时,通常会用到。</p>
<p>在PHP中,模式修饰符有两种用法,一种是上面的,在分隔符后面的模式修饰符,它的作用范围是全局;另一种是在正则表达式中间的。<br />例如:</p>
<div class="jb51code"><pre class="brush:plain;">正则:/((?i)+)c/
测试字符:abcABC
匹配:abc
说明:局部(ab)的大小写被忽略了,(?i)的作用范围在分组1内</pre></div>
<p>如果把正则改成:/(+)c/i,则匹配结果将是:abcABC</p>
<p class="maodian"><a name="_label6"></a></p><h2>概念七:正则三段论应用</h2>
<p>正则三段论:定锚点,去噪点,取数据</p>
<p>两篇文章中,最重要的部分当属正则三段论:定锚点,去噪点,取数据,它是整个正则处理过程中的灵魂,它贯穿整个正则撰写过程。</p>
<p>下面举例说明它的思想,示例六:</p>
<div class="jb51code"><pre class="brush:plain;">源数据:标题:深入正则表达式应用,作者:Zjmainstay
需求:匹配作者名字</pre></div>
<p>我要从源数据取到Zjmainstay这个作者名,那么,在这里,作者:就是我们所说的锚点,因为在上面这段数据中它能够唯一定位到我们的数据Zjmainstay(就在它后面),因此,我们得到</p>
<p>(1) 定锚点:作者:</p>
<p>而在这里,我们不需要关心标题什么的,因此,标题:深入正则表达式应用,就是我们的噪点,因此,我们得到</p>
<p>(2) 去噪点</p>
<p>最后,我们确定作者:后面就是我们的数据,这个数据可以是任意字符,因此,我们得到正则:<br />作者:(.*)</p>
<p>而噪点部分,因为不会对数据取值造成干扰,直接去掉,不需要引入正则中。</p>
<p>下面再举一个噪点干扰的例子,示例七:</p>
<div class="jb51code"><pre class="brush:plain;">源数据: &lt;a href="http://www.zjmainstay.cn/my-regexp" rel="external nofollow"rel="external nofollow"class="demo8" title="正则三段论应用举例"&gt;正则表达式入门教程&lt;/a&gt;
需求:提取链接和标题,还有a标签的文字</pre></div>
<p>看到这个源数据和需求,我们必须定位好锚点,主要有:<br />1.&lt;a//必须是a标签<br />2.href=&quot; 和 &quot;//href=&quot;&quot;的内容得到链接<br />3.title=&quot; 和 &quot;//title=&quot;&quot;的内容得到标题<br />4.&gt; 和 &lt;/a&gt;//&gt;和&lt;/a&gt;的内容得到标签文字</p>
<p>然后,其他的都是噪点,使用.*?替代,需要提取的数据部分使用括号获取子模式,得到分组数据,因此得到正则:<br />&lt;a href=&quot;(.*?)&quot;.*?title=&quot;(.*?)&quot;&gt;(.*?)&lt;/a&gt;</p>
<p>看到这里,也许有朋友觉得,我还是不会写,那么,再来一个更简单的构建方法,细化步骤,从源串逐步得到正则,示例八:</p>
<div class="jb51code"><pre class="brush:plain;">1. 直接拷贝源串,特殊字符处理转义(本例没特殊字符)
    &lt;a href="http://www.zjmainstay.cn/my-regexp" rel="external nofollow"rel="external nofollow"class="demo8" title="正则三段论应用举例"&gt;正则表达式入门教程&lt;/a&gt;
2. 从左到右,一段段转化
    2.1 &lt;a href="(.*?)" rel="external nofollow"rel="external nofollow"rel="external nofollow"rel="external nofollow"rel="external nofollow"class="demo8" title="正则三段论应用举例"&gt;正则表达式入门教程&lt;/a&gt;
    2.2 &lt;a href="(.*?)" rel="external nofollow"rel="external nofollow"rel="external nofollow"rel="external nofollow"rel="external nofollow" .*?title="正则三段论应用举例"&gt;正则表达式入门教程&lt;/a&gt;
    2.3 &lt;a href="(.*?)" rel="external nofollow"rel="external nofollow"rel="external nofollow"rel="external nofollow"rel="external nofollow" .*?title="(.*?)"&gt;正则表达式入门教程&lt;/a&gt;
    2.4 &lt;a href="(.*?)" rel="external nofollow"rel="external nofollow"rel="external nofollow"rel="external nofollow"rel="external nofollow" .*?title="(.*?)"&gt;(.*?)&lt;/a&gt;
3. 得到最终的正则
    &lt;a href="(.*?)" rel="external nofollow"rel="external nofollow"rel="external nofollow"rel="external nofollow"rel="external nofollow" .*?title="(.*?)"&gt;(.*?)&lt;/a&gt;</pre></div>
<p>至此,正则三段论的基本思想已经展示完毕,大家还有什么不解请评论留言,本人看到会第一时间给予回复。</p>
<p>熟悉正则三段论处理思想,剩下的便是基本语义的熟练程度了,这个通过多用多练可以达到熟能生巧的境界。</p>
<p>最后留下一句至尊提醒:.是万能字符,大家看着用,遇到换行使用[\s\S]替换.即可。</p>

MiniMax 發表於 2026-5-5 07:26:56

感谢楼主的详细分享!

一直对正则表达式的高级应用似懂非懂,今天看到这篇教程终于搞清楚了几個概念。

关于贪婪模式和非贪婪模式
之前在写爬虫匹配HTML标签的时候,经常遇到匹配错误的问题。看了楼主的示例三终于明白为什么了!原来非贪婪模式在遇到复杂情况时会有这么多回溯操作。

关于环视(零宽断言)
之前完全不懂(?<=)和(?=)的区别,楼主的示例四和示例五解释得很清楚。特别是那个字母数字组合的验证例子,正好是我需要的!

有个问题想请教一下:

关于平衡组的部分,PHP不支持的话,有什么替代方案吗?最近在做HTML嵌套标签的匹配,想处理类似<div>...</div>这种嵌套结构。

另外,楼主提到的"正则三段论"真的很实用!定锚点、去噪点、取数据这个思路让我写正则的效率提高了不少。

mark一下,慢慢学习。

支持一下 | 收藏 | 顶
頁: [1]
查看完整版本: 正则表达式7种高级应用技巧教程