零占位

在这里,有人要说我了,说,你上一节最后的时候说到“就像刚才那段被我们拿来演示的段落,我们要把里面的所有html标签过滤掉,但是又想保留标签中的文字,该如何搞?”,最后还搞出一个莫名其妙的从没听说过的“零宽断言”啥的出来,不是装B么?我想去掉所有的HTML标签还不简单,直接一个 /<.+?>/gi 就搞定了!

好吧,我承认你是人才,这的确是去掉HTML标签最简单的办法之一:寻找所有的“<”开头“>”结尾的标记,然后干掉。那我为了在本节中继续发表我关于“零宽断言”的教程,请允许我换一个更严谨的案例吧,现在我们要在前几节的蓝色例句“<span style=”color:Red”>作为一个搞<a href=”xxx1″>WEB技术</a>的人才,你现在牛逼了<img src=”./p_w_picpaths/xxx.jpg” />,终于开始用  <a href=”xxx2″>正则表达式了</a></span><img src=”./p_w_picpaths/xxxxxxxx.jpg” />”中,将所有的src属性的内容提取出来,请问怎么办?
按照原来的方法,我们的逻辑思路应该是:
1、寻找”src=”关键字的位置
2、匹配该位置后面跟的内容
3、遇到双引号或者单引号就结束匹配。(单双引号需要用转义字符\来转义)
于是,我们的正则表达式就写成了这样: /src\=\”.*?\”/gi

src1.png

但是……但是……怎么就不对劲儿呢?哥是要想提取src中的属性内容,他娘的怎么把 src=”“ 这个讨厌的东西也提出来了呢?如果我要向运用到实际中,难道我还需要用replace函数再过滤掉 “”src=” ”和“””么?  那也太土了吧?
我们是多么需要一个能用来检验是存在某个匹配内容,但是又零占位的东西啊!这样的话,我们只需要能检测到src=的位置 ,但又不把它匹配出来了!


零宽断言
伟大的正则表达式创建者们,是坏神(fashion)的,他们为我们提供了四个工具,分别叫做:正向零宽断言(包含零宽度正预测先行断言、零宽度正回顾后发断言)、负向零宽断言(包含零宽度负预测先行断言、零宽度负回顾后发断言)。先别晕,下面会很通俗易懂的告诉大家这是怎么回事——
回到刚才的话题,如何快速的提取出“src=”"” 中的内容,而又能不包括“src=”“”本身呢?下面是见证奇迹的时时刻!
/(?<=(src\=\”).*?(?=\”/gi

 

src2.png

看到没得!加了一个简单的符号,就成功的实现了你刚才脑海中在纠结的那个正则表达式+replace的无敌组合!
我们把 /src\=\”.*?\”/gi 变成了 /(?<=(src\=\”).*?(?=\”/gi

所有的变化,只是多了一个括号、一个问号、一个等号而已!

(?=exp) 零宽度正预测先行断言,它试探性的断言自身出现的位置的后面能匹配表达式exp,但并不将自身包含进去。比如 .* (?=\” ,匹配以“””结尾的一串除换行符以外的任意字符。
(?<=exp) 零宽度正回顾后发断言,它试探性的断言自身出现的位置的前面能匹配表达式exp,但并不将自身包含进去。比如(?<=(src\=\”.*
会匹配以 src=” 开头的一串除换行符以外的任意字符。

所谓“零宽”,就是,他不会包含自己,也就是上一节末尾说的,他不是那种我们之前见到的“需要消耗位置的一些匹配”,他只是用来查找一个位置,但自己并不占据任何一个位置。
需求总是层出不穷的,如果想确认一下某个句子中是否不包含某个字符,但仅仅是确认一下,并不想去让这个被确认的字符占据一个位置,怎么办呢?我在前面说过了,有正就有负,所以,下面我们会提到负向零宽断言
先不说语法,我们先给大家介绍另外一个在第一节见过但是还没用过的元字符 \b ,并看看这个元字符带来的一个纠结的案例——

元字符 : \b 匹配单词的开始或结束,例如要匹配 一个包含 ge 的单词, 就用 /\b\w*ge\w\b/g

ge.png



新需求是,还是利用上面那个蓝色的万能例句,现在我们要找在里面找一个单词,它在任意位置包含一个字母g,但是又不能在g后面跟着一个e。
按照逻辑,我们这样构建: /\b\w*g[^e]\w*\b/g

/      #匹配开始
\b    #匹配单词的开始
\w*   #匹配“g”前可能存在的一串字母或数字或下划线或汉字,可以为
g      #在任意位置匹配一个g
[^e]  #g后面不能是e
\w*   配“g”后可能存在的一串字母或数字或下划线或汉字,可以为空
\b    #匹配单词的结束
/g   #匹配结束
经过肉眼观察,在例句中,有如下六个个单词包含g:两个jpg、两个img、两个 p_w_picpaths,我们应该是能匹配到两个”jpg”和两个“img”的(存在包含g的单词“p_w_picpaths”,但是已经因为g后包含e,所以匹配不上),但是,一测试,出悲剧了!看图——

ge2.png

诡异了! 难道 \b 不能正确的匹配到一个单词么? 退一步说,就算它没有匹配到两个“jpg”也就罢了,在匹配“img”的时候,居然连后面的” src“也匹配出来了!中间还包含一个空格!!!
其实,问题很简单。因为主要问题就出在 [^e] 上,我们天真的试图用这个东西来匹配一个非e的字符,却匹配到了非e的空格
“。事实上,是我们错了。
我们想,如果能有一个特殊的语法,能让我们检测到g后面的位置是否存在一个e,但是并不去占掉这个位置该多好呀!这样不就不会匹配到后面的空格了么?
答案是,有!这个真有!
事实上,它的语法跟前面提到的正向零宽断言是一样一样一样滴,只不过,它是反的!怎么个反法呢?——

(?!exp),  叫零宽度负预测先行断言,断言此位置的后面不能匹配表达式exp。比方说:\b((?!ge)\w)+\b匹配不包含连续字符串ge的单词,\d{2}(?!\d)匹配两位数字,而且这两位数字的后面不能再是数字。
(?<!exp),叫零宽度负回顾后发断言,断言此位置的前面不能匹配表达式exp:

所以,我们将正则表达式改一下,改成这样:/\b\w*g(?!e)\w*\b/g
问题顺利解决!

ge3.png

现在,我们再回到本节最开始被你们鄙视的那个问题,我决定把它修改的更严谨一点,好让我们来做一个融合分组和与非、零宽断言综合练习:现在我要将所有成对出现的html标签中的内容提取出来。比方,在这个万能的蓝色例句中,我们从html中提取出的内容应该是这个样子:“作为一个搞WEB技术的人才,你现在牛逼了<img src=”./p_w_picpaths/xxx.jpg”/>,终于开始用正则表达式了”,请问,怎么搞?
按照以前我们所说的讨论,我们的思路应该是:
1、先构建出一个能匹配成对出现的html标签的正则表达式
2、将匹配标签本身部分的正则语句段修改成零宽断言语法
根据思路1,我们直接使用上一节已经构建出的 /<(\w*)[^>\/*]*>.*<\/\1>/gi
因为这个式子结构很简单,分成三个部分: <(\w*)[^>\/*]*> 匹配标签头、.* 匹配标签中包含的内容、 <\/\1> 匹配标签尾。所以,我们只要把标签头尾部分的两个部分分别作正向零宽后发断言和正向零宽先行断言就好了,修改成这样:

<(\w*)[^>\/*]*> → (?<=<(\w*)[^>\/*]*>

<\/\1> → (?=<\/\1>
组成一个整体 /(?<=<(\w*)[^>\/*]*>.*(?=<\/\1>/gi ,然后检验一下,发现——


beiju.png



这是一个教训,因为根据计算机的最基础的原理——确定性,在做零宽断言的时候,不能包含通配符*、?、+。所以,我们要匹配一个需要用到通配符来匹配标签内部属性的正则表达式,用零宽断言,此路不通!
那,什么是确定的呢?经过观察,发现所有的标签内文字总是包含在“>”和“<”之间!这个是绝对确定的!
所以我们构建一个正则并表达式如下:

/(?<=>;).*?(?=<;)/gi

太好了!这次终于有点靠谱了!我们已经匹配上了所有成对标签中包含的纯文本内容。


okla~~.png

但还是有BUG,只匹配到了纯文本,在<span>的内部的两个<a>标签之外,还有一个内关闭的不符合“成对”规范的<img />标签没有匹配上!继续搞。
我试了试 /(?<=>;).*?(?=<\/)/gi,它匹配的是以下蓝色字体:
<span style=”color:Red”>作为一个搞<a href=”xxx1″>WEB技术</a>的人才,你现在牛逼了<img src=”./p_w_picpaths/xxx.jpg”/>,终于开始用  <a href=”xxx2″>正则表达式了</a></span><img src=”./p_w_picpaths/xxxxxxxx.jpg”/>

<span style=”color:Red”>作为一个搞<a href=”xxx1″>WEB技术</a>的人才,你现在牛逼了<img src=”./p_w_picpaths/xxx.jpg”/>,终于开始用  <a href=”xxx2″>正则表达式了</a></span> <img src=”./p_w_picpaths/xxxxxxxx.jpg”/>

 

又试了试 /(?<=>;).*(?=<;)/gi,它匹配的是以下蓝色字体:

看来凭我的水平是没办法写出这个正确的嵌套匹配了,求高人!!!

零宽家族
虽然我在上一个案例中没有给出一个正确的匹配并试图呼唤高人来解决此问题,但是,我还是很有兴趣给大家介绍一下有趣的零宽家族。
除了上面的零宽断言的语法外,我们还有一些之前曾让大家见过面,但是还没来得及告诉大家如何使用它们的属于零宽家族的元字符:

--------------------------------------------------
零宽家族

元字符 功能
\b 匹配单词的开始或结束
^ 匹配字符串的开始
$ 匹配字符串的结束
(?=exp) 零宽度正预测先行断言
(?<=exp) 零宽度正回顾后发断言
(?!exp) 零宽度负预测先行断言
(?<!exp) 零宽度负回顾后发断言

------------------------------------------------

  它们的共同特点就是,可以用来匹配一个东西,但是它匹配完了之后就自动消失了,不会占据一个位置,很有牺牲精神。
举例说明,我们要从一部英文字典中匹配出所有包含元音字母a、e、i的单词们,可以这样做匹配  :
/\b\w*[aei]\w\b/gi

  我们建立一个游戏家族论坛,要求每个人的用户名都是以“asoft_”开头且后面还要跟随4~16个字母和下划线组成的字符串,可以这样做匹配  : /^asoft_\w{4,16}/gi


如果有人注册asoft_tat5cccasoft_adf3,那么只有第一个用户名是合法的,但如果去掉上面表达式中的“^”变成
/asoft_\w{4,16}/gi ,那么两个用户名都能被匹配到。

下一节,将跟大家分享一些常用的案例,并细致的告诉你是如何生成正则表达式的。最后会介绍一下我们还没来得及讲的一些其他语法。