標籤:ora html 參考 第一個 應聘 其他 處理 cti hand
引言:JS Regex是 JS 學習過程中的一大痛點,繁雜的匹配模式足以讓人頭大,不過其複雜性和其學習難度也賦予了它強大的功能。文章從 JS Regex的正向前瞻說起,實現否定匹配的案例。本文適合有一定 JS Regex基礎的同學,如果對Regex並不瞭解,還需先學習基礎再來觀摩這門否定大法。
一、標籤過濾需求
不知道大家在寫JS有沒有遇到過這樣的情況,當你要處理一串字串時,需要寫一個Regex來匹配當中不是 XXX 的常值內容。聽起來好像略有些奇怪,匹配不是 XXX 的內容,不是 XXX 我匹配它幹嘛啊,我要啥匹配啥不就完了。你還別說,這個玩意還真的有用,不管你遇沒遇到過,反正我是遇到了。具體的需求例如:當你收到一串HTML代碼,需要對這一串HTML代碼過濾,將裡面所有的非<p>標籤都改為<p>。這裡肯定有不少同學就要嫌棄了,“將所有標籤都改為<p>,那就把任意標籤都改為<p>不就完了?”,於是乎一行代碼拍腦袋而生:
1 var str = ‘<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>‘;2 var reg = /<(\/?).*?>/g;3 var newStr = str.replace(reg, "<$1p>");4 console.log(newStr);//<p>,<p>,<p>,<p>,</p>,</p>,</p>,</p>
注意這個方法中有一個引用符 “$1” ,這個的意思引用正則的運算式的第1個分組,可以用$N來表示在Regex中的第N個捕獲的引用。就那上面的例子來說,"(\/?)"這個一個運算式的含義是,"\/"這個字元出現0次或者1次,而$1這個引用呢就相當於和“\/”這個字元門當戶對的大閨女,她已下定決心此生非"\/"不嫁。所以當匹配到有一個“\/”的時候,$1這個引用就把它捕獲下來,從現在起,你的就是我的,我的就是你的啦,因此$1等價於"(\/?)"所匹配到的字元;反之如果沒有匹配到"\/"這個字元,那$1這個引用就得空守閨房,獨立熬過一個又一個漫長的夜晚,因為它內心極度的空虛,所以$1就等價於""(也就是空串)。
這裡先聊了聊引用和捕獲的概念,因為後面還會用到它。那麼話說回來,剛才那一串正則,不是已經完美的實現了需求了嗎?還研究什麼否定匹配啊?各位看官別急,且聽小生慢慢道來。我們都知道,需求這個東西,肯定是會改嘀(???)。現在改一改需求:當你收到一串HTML代碼,需要對這一串HTML代碼過濾,將裡面所有的非<p>或者<div>標籤都改為<p>。WTF?這算哪門子需求?話說我當時也是這種反應。我們現在分析一下這個需求到底要幹嘛,也就是說,保留原HTML代碼中的<p>和<div>,將其他標籤統一修改為<p>。咦...這下可不好弄了,剛才那串代碼看上去貌似行不通了。所以說這時候就只能用排除法了,排除掉<p>和<div>,替換掉其他的標籤。那麼問題也就來了,如何排除?
二、正則前瞻運算式
在Regex當中有個東西叫做前瞻,有的管它叫零寬斷言:
運算式 |
名稱 |
描述 |
(?=exp) |
正向前瞻 |
匹配後面滿足運算式exp的位置 |
(?!exp) |
負向前瞻 |
匹配後面不滿足運算式exp的位置 |
(?<=exp) |
正向後瞻 |
匹配前面滿足運算式exp的位置(JS不支援) |
(?<!exp) |
負向後瞻 |
匹配前面不滿足運算式exp的位置(JS不支援) |
由於 JS 原生不支援後瞻,所以這裡就不研究它了。我們來看看前瞻的作用:
1 var str = ‘Hello, Hi, I am Hilary.‘;2 var reg = /H(?=i)/g;3 var newStr = str.replace(reg, "T");4 console.log(newStr);//Hello, Ti, I am Tilary.
在這個DEMO中我們可以看出正向前瞻的作用,同樣是字元"H",但是只匹配"H"後面緊跟"i"的"H"。就相當於有一家公司reg,這時候有多名"H"人員前來應聘,但是reg公司提出了一個硬條件是必須掌握"i"這項技能,所以"Hello"就自然的被淘汰掉了。
那麼負向前瞻呢?道理是相同的:
1 var str = ‘Hello, Hi, I am Hilary.‘;2 var reg = /H(?!i)/g;3 var newStr = str.replace(reg, "T");4 console.log(newStr);//Tello, Hi, I am Hilary.
在這個DEMO中,我們把之前的正向前瞻換成了負向前瞻。這個正則的意思就是,匹配"H",且後面不能跟著一個"i"。這時候"Hello"就可以成功的應聘了,因為reg公司修改了他們的招聘條件,他們說"i"這門技術會有損公司的企業文化,所以我們不要了。
三、前瞻的非捕獲性
說到這裡,讓我們回到最初的那個需求,讓我們先用負向前瞻來實現第一個需求:將所有非<p>標籤替換為<p>。話說同學們剛學完了負向前瞻,瞭解到了JS的博大精深,心中暗生竊喜,提筆一揮:
1 var str = ‘<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>‘;2 var reg = /<(\/?)(?!p)>/g;3 var newStr = str.replace(reg, "<$1p>");4 console.log(newStr);//<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>
What?為什麼不起作用呢?說好的否定大法呢?這裡就得聊一聊前瞻的一個特性,前瞻是非捕獲性分組,什麼玩意是非捕獲性分組呢?還記得前面那位非"\/"不嫁的大閨女$1嗎,人家為什麼那麼一往情深,是因為她早已將"\/"的心捕獲了起來,而前瞻卻是非捕獲性分組,也就是你捕獲不到人家。也就是說無法通過引用符"\n"或者"$n"來對其引用:
1 var str = ‘Hello, Hi, I am Hilary.‘;2 var reg = /H(?!i)/g;3 var newStr = str.replace(reg, "T$1");4 console.log(newStr);//T$1ello, Hi, I am Hilary.
注意其中輸出的語句,前面我們可以看到,如果引用符沒有匹配到指定的字元,那麼就會顯示空串"",可是這裡是直接顯示了整個引用符"$1"。這是因為前瞻運算式根本就沒有捕獲,沒有捕獲也就沒有引用。
非捕獲性是前瞻的一個基本特徵,前瞻的另外一個特性是不吃字元,意思就是前瞻的作用只是為了匹配滿足前瞻運算式的字元,而不匹配前瞻本身。也就是說前瞻不會修改匹配位置,這麼說我自己都覺得晦澀,我們還是來看看代碼吧︽⊙_⊙︽:
1 var str = ‘Hello, Hi, I am Handsome Hilary.‘;2 var reg = /H(?!i)e/g;3 var newStr = str.replace(reg, "T");4 console.log(newStr);//Tllo, Hi, I am Handsome Hilary.
注意觀察輸出的字串,前瞻的作用僅僅是匹配出滿足前瞻條件的字元"H",匹配出了"Hello"和"Handsome"當中的H,但同時前瞻不會吃字元,也就是不會改變位置,接下來還是會緊接著"H"開始繼續往下匹配,這時候匹配條件是"e",於是"Hello"中的"He"就匹配成功了,而"Handsome"中的"Ha"則匹配失敗。
1. /H(?!i)/g --> Hello, Hi, I am Handsome Hilary.2. /H(?!i)e/g --> Hello, Hi, I am Handsome Hilary.
四、用前瞻實現標籤過濾
既然前瞻是非捕獲性的,而且還不吃字元,那麼瞭解到這些特徵後我們現在終於可以完成我們的需求了吧?因為它不吃字元,所以具體的標籤字元還得由我們自己來吃:
1 var str = ‘<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>‘;2 var reg = /<(\/?)(?!p|\/p).*?>/g;3 var newStr = str.replace(reg, "<$1p>");4 console.log(newStr);//<p>,<p>,<p>,<p>,</p>,</p>,</p>,</p>
聊了這麼半天,終於解決了咱們的第一個需求,注意當中的".*?",雖然這裡匹配的是任一字元,但是別忘了,有了前面的負向前瞻,我們匹配到的都是後面不會緊跟著"p"或者"/p"的字元"<"。
/<(?!p|\/p)/g --> <div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>
注意在這裡用了一個管道符"|"來匹配"\/p",雖然前面已經有了"(\/?)"匹配結束符,但是切記這裡的分組選項不能省略,因為這裡的量詞是可以出現0次。我們來試想一下如果用"/<(\/?)(?!p).*?>/g"來匹配"</p>"這個標籤,當量次匹配到"/"的時候,發現可以匹配,便記錄下來,然後對"/"進行前瞻判斷,但是後面卻接著一個"p"於是不能匹配,丟掉;注意這時"(\/?)"的匹配字元是0個,於是乎轉而對"<"進行前瞻判斷,這裡的"<"後面緊接著的是"/p"而不是"p",於是乎成功匹配,所以這個標籤會被替換掉;而且,由於之前的分組匹配到的字元是0個,也就是沒有匹配到字元,所以後面的引用是個空串。
1 var str = ‘<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>‘;2 var reg = /<(\/?)(?!p).*?>/g;3 var newStr = str.replace(reg, "<$1p>");4 console.log(newStr);//<p>,<p>,<p>,<p>,</p>,</p>,<p>,</p>
完成了第一個過濾需求,那麼第二個過濾需求也就自然而然的完成了,這時候,就算有那麼五六個標籤需要保留,咱們也不用怕了:
1 var str = ‘<div>,<p>,<h1>,<span>,</span>,</h1>,</p>,</div>‘;2 var reg = /<(\/?)(?!p|\/p|div|\/div).*?>/g;3 var newStr = str.replace(reg, "<$1p>");4 console.log(newStr);//<div>,<p>,<p>,<p>,</p>,</p>,</p>,</div>
五、總結
JS 的正向前瞻只是Regex當中一部分,沒相當就這麼一部分還有著這麼多的奧妙呢。
在使用正向前瞻,我們需要注意的是:
- 前瞻是非捕獲性的:其特徵是無法引用。
- 前瞻不消耗字元:前瞻只匹配滿足前瞻運算式的字元,而不匹配其本身。
話說,咱們的需求就到這了嗎?真的就完了嗎?同學們覺得過癮不?有些同學覺得可能差不多了,需要消化一段時間,但是絕對有那麼一部分同學還完全沒過癮呢,沒關係,最後留給大家一道思考題,截止到我寫這篇部落格為止,我還沒有想出一個解決辦法呢(? •_•)?。
需求如下:當你收到一串HTML代碼,需要對這一串HTML代碼過濾,將裡面所有的非<p>或者<div>標籤都改為<p>,並且保留所有標籤的樣式,要求只使用一個Regex,例如:
//輸入var input = ‘<div class="beautiful">,<p class="provocative">,<h1 class="attractive" id="header">,<span class="sexy">,</span>,</h1>,</p>,</div>‘;//輸出var output = ‘<div class="beautiful">,<p class="provocative">,<p class="attractive" id="header">,<p class="sexy">,</p>,</p>,</p>,</div>‘;
如果你有好的解決方案,歡迎在評論區留言,大家一起學習。
參考文獻:
devinran —— 《相愛相殺——正則與瀏覽器的愛恨情仇》
Barret Lee —— 《進階Regex》
JS Regex否定匹配(正向前瞻)