1. 程式人生 > >正則表示式 與 或 非

正則表示式 與 或 非

正則表示式的與或非

我們都知道,寫正則表示式有點像搭積木,複雜的功能總可以拆分開來,由不同的元素(也就是子表示式)對應,再用合適的關係將它們組合起來,就可以完成。在這一節,我們講解常見的與或非關係的表達。

“與”是最簡單的關係,它表示若干個元素必須同時相繼出現,比如匹配單詞cat,其實就是要求字元c、字元a和字元t必須同時連續出現。

正則表示式表達“與”關係非常簡單,直接連續寫出相繼出現的元素就可以,我們可以想象,再各個元素之間,存在看不見的連線操作符·,比如上面匹配單詞cat的正則表示式,就是『cat』,我們可以將它想象為『c·a·t』。

“與”關係也不限於字元之間,任何子表示式都可以用它來連線,如果我們把上面單詞中的a替換為字元組『[au]』,表示式就變為『c[au]t』,你可以想象為『c·[au]·t』。

“或”是正則表示式靈活性的重要體現,我們可以規定某個位置的文字的“多種可能”,比如要匹配cat或是cut,在正則表示式看來,就是“字元c,然後是a或u,然後是t”。

如果“或”的多種可能都是單個字元(一般要求ASCII字元,中文字元等多位元組字元的情況,可以參考本書專門論述的章節,此處僅以ASCII字元為例),就可以用字元組來表達“或”的關係,比如上面的cat或者cut的情況,正則表示式寫做『c[au]t』,其原理如下:

更復雜的情況是“或”的多種可能,並非都是單個字元,有些可能是多個字元。比如,我們可以看一個更復雜的例子,不僅要匹配cut,還要匹配c開頭、t結尾的單詞chart、conduct和court。也就是說,在開頭的c,結尾的t之間“可能”出現的是:u

haronducour。

遇到這種情況,就不應使用字元組,而應當使用多選分支『(…|…)』,將各個“可能選項”列在多選分支中。於是,正則表示式變為『c(u|har|onduc|our)t』,其原理如下:

關於多選分支,還有兩點要補充:

多選分支也可用於“每個選擇都是單個字元”的情況,比如『c[au]t』寫成『c(a|u)t』是沒錯的,但字元組的效率要遠高於多選分支,所以,在這種情況下,推薦使用字元組『c[au]t』;

預設的多選分支『(…|…)』使用的括號是會捕獲文字的,也就是說,括號內的表示式真正匹配成功的文字會記錄下來,匹配完成之後可以提取出來,具體到上面的例子,就是我們有辦法在匹配完成後“提取”出u或har或onduc或our。但許多時候,我們需要的只是整個表示式的匹配,而不關心“匹配時到底選擇的哪種可能情況”,在這種情況下,我們稍加修改,使用“不捕獲文字的括號”,可以提高效率。不捕獲文字的寫法也很簡單,只是在開擴號之後加上字元『?:』,也就是『(?:

…|…)』,具體到上面的例子,就應該寫成『c(?:u|har|onduc|our)t』。這樣做雖然繁瑣點,但效率有保障,閱讀起來也不困難,我推薦養成這種習慣,只要用到了括號,就想想是否真的要捕獲括號內表示式匹配的文字,如果不需要,就是用不捕獲文字的括號。

“非”看起來簡單,其實是最複雜的,以下分幾種情況討論。

首先討論針對字元的“非”:不容許出現某個或某幾個字元。這是最簡單的情況,直接用排除型字元組就可以對付,仍然用上面的例子,如果要匹配的單詞是c開頭、t結尾,中間有一個字元,但不能是u(也就是說,整個單詞不能是cut),直接用『c[^u]t』就可以了,若中間的字元不能是a或u(也就是說,整個單詞不能是cat或cut),則表示式改為『c[^au]t』。

如果你認真讀過關於排除型字元組的章節,肯定會知道,這個表示式能匹配的只是cot之類的單詞,因為中間的排除型字元組『[^cu]』必須匹配一個字元。可是,如果我們還想要匹配chart、conduct和court,怎麼辦?最簡單的想法是去掉排除型字元組的長度限制,改成『c[^au]+t』——不幸的是,這樣行不通,因為這個表示式的意思是:c和t之間,是由多於一個“除a或u之外的字元“構成的,而chart、conduct和court,都包含a或u。

我們回頭仔細看看這個“非”的邏輯,我們發現,其實我們要否定的是“單個出現的a或u”,而不僅僅是“出現的a或u”,所以才出現這樣的問題,要解決這個問題,就應當把意思準確表達出來,變成“在結尾的t之前,不容許只出現一個a或u”。想到這一步,我們就可以用否定順序環視『(?!…)』來解決了,它表示“在這個位置向右,不容許出現子表示式能夠匹配的文字,我們把子表示式規定為『[au]t\b』(最後的『\b』很重要,它出現在t之後,保證t是單詞的結尾子母)。

有了這點限制,匹配a和t之間文字的表示式就隨意很多了,我們可以用匹配單詞字元的簡記法『\w』表示,於是整個表示式就變成了『c(?![au]t\b)\w+t』。請注意,這裡出現的並不是排除型字元組『[^au]』,而是普通的字元組『[au]』,因為否定順序環視『(?!…)』本身已經表示了“否定”的功能。

如果我們再進一步,“整個匹配文字中都不能出現字串cat”,要怎麼辦呢?許多人的思路就是借鑑處理“或”關係的思路:既然字元組對應單個字元的情況,多選分支對應多個字元的情況,那麼在否定時也是這樣。可惜,正則表示式並沒有提供與多選分支對應的“否定”結構,那麼,應該怎麼辦呢?

解決的辦法還是得依靠否定順序環視——“整個匹配文字中都不能出現字串cat”,換句話說,就是“在文字中的任意位置,向右,都不能出現該字串”。因此,我們用兩個錨點『^』和『$』,分別匹配整個字串的開頭和結尾位置,再用否定順序環視『(?!cat)』表達“不能出現字串cat”。

即便知道了原理,也不見得能寫對正則表示式,比如『^(?!cat).+$』就是不正確的,因為它只限定了在文字的開頭(也就是『^』)右邊不能出現cat,而我們真正要做的是,在文字的每一個位置右邊,都不能出現cat,所以應該改成『^((?!cat).)+$』;但這還說不上完美,根據前面提到的關於括號捕獲的知識,因為此處並不需要括號捕獲的文字,所以最好使用非捕獲型括號『(?:…)』,最終我們得到的表示式就是『^(?:(?!cat).)+$』。