正則與JS中的正則

最近這段時間幫同窗處理一些文檔, 涉及到一些結構化文檔的工做大部分都得使用正則表達式, 以前對於正則的認識大多來源於語言書上那幾頁的介紹, 本身也沒有用過幾回。這裏將我以前感到模糊的概念做個整理。由於對JS瞭解多點,因此也將JS中相關的正則特性概括下。注意本文將正則與JS中的正則分開討論。html

正則引擎

正則表達式的解釋引擎只有兩種,字符驅動(text-directed)和正則驅動(regex-directed),基於這兩種解釋引擎算法的不一樣,它們有時也被稱爲DFA(Deterministic finite automaton 肯定型有窮自動機)與NFA(Non-deterministic finite automaton非肯定型有窮自動機),當前大部分現代語言的正則解釋器採用「正則驅動」引擎,這是由於NFA運行的回溯算法能夠實現諸如‘惰性匹配(lazy quantifiers )’和‘後部引用(backreferences)’ 等很是有用的特性, 而DFA算法也就是字符驅動型引擎並不支持這些特性,固然複雜的算法必須付出性能的代價,字符驅動引擎的性能要強於正則驅動。各類語言中因爲實現正則引擎的具體算法以及調用的正則庫都有不一樣,因此對於正則的支持也不一樣,關於這些概念的參考mark在這裏:
?Regex Engine Internals
?正則表達式匹配解析過程探討分析(正則表達式匹配原理)正則表達式

零寬斷言

零寬斷言(zero-length assertions)這概念直譯過於術語化(都有點像咒語了,囧rz), 邏輯不是太複雜,可是容易混淆模糊,在此記個筆記。零寬的意思是指該位置是不佔寬度的,也就是隻做斷言判斷,但不匹配實際的內容,好比\d(?=\.)這個正向先行斷言(這概念也概括在後)就只匹配點號以前的數字,可是它並不會匹配到這個點號,這個\d(?=\.)括號中的匹配內容也就是零寬了。
零寬斷言分爲四種,分別是:正向先行(Positive Lookahead),正向回顧(Positive Lookbehind),負向先行(Negative Lookahead),負向回顧(Negative Lookbehind)。正向和負向的意思是斷言括號中的內容是匹配仍是不匹配,好比上面栗子?中的(?=...)就是正向先行斷言的寫法,該斷言括號中的內容必須出現, 而負向也就是斷言括號中的內容不能出現,負向先行的寫法是這樣:(?!...)。 先行與回顧的意思是實際匹配的內容在斷言內容的前面仍是後面,如下將逐一羅列這四種概念:算法

正向先行

直接地說「零寬正向先行斷言」所匹配的就是必須出現的斷言內容的前面的內容,以前的點號前數字的栗子?\d(?=\.)已經比較直觀的展現了這個概念,可是須要注意的是:這裏的先行或者回顧是基於位置,而不是字符!也就是說從點號的位置開始向前匹配, 好比咱們把這個正則表達式改爲這樣:(?=\.).注意這裏並無把實際要匹配的內容放在斷言括號的前面,而是放到了後面, 這就匹配了從這個點號位置開始的任意字符(除換行符外),也就匹配到了這個點號, 固然一般這樣寫沒什麼意義, 但便於理解位置的含義。express

正向回顧

理解了前面這個「零寬正向先行斷言」,隨之而來的就比較好理解了。依照這個邏輯,零寬正向回顧斷言所匹配的是必須出現的斷言內容以後的內容,它的語法是:(?<=...) 好比(?<=\.)\w所匹配的就是點號以後的ASCII字符。segmentfault

負向先行

負向與正向意思相反, 正向是斷言內容必須出現,而負向則是斷言內容必須不出現。好比JavaScript and Java這句話中使用Java(?!Script)就只會匹配到Java,由於該正則斷言Java後面不能出現Script數組

負向回顧

正負向與先行回顧的概念都已在前面列出, 負向回顧的理解應該就很順了。負向回顧的語法是:(?<!...)。 好比(?<!Java)Script該正則只匹配不是JavaScriptScript,它就能正確匹配ECMAScriptScript瀏覽器

這裏須要注意:在大多數語言中,先行斷言中能夠有任意的正則語法,好比能夠用+或者*這種不肯定重複的匹配,可是回顧斷言中的限制就比較多,不少語言的回顧斷言中不支持諸如+*這種不定重複的匹配和後部引用,由於正則解釋引擎在匹配回顧斷言的時候,當回顧斷言中的內容匹配成功後再去匹配斷言後的實際內容時,它須要能計算出後退多少步才能判斷出當前匹配是否與這個正則徹底匹配。有些語言壓根就不支持回顧斷言,好比JS;而 PHP, Delphi, R和 Ruby只支持在回顧斷言中加入選擇匹配,但選擇匹配的內容長度必須相同,形如[ab|ba|cd];Java回顧斷言限制很少,但沒法在回顧斷言中加入不定數量的重複,也就是不能在回顧斷言中使用+; .Net支持在回顧斷言中使用任意正則語法。(以上內容參考自下面的第一個mark參考中,可能存在時效的問題, 若有錯誤還請大神指出。函數

正則零寬斷言更多參考mark:
?Lookahead and Lookbehind Zero-Length Assertions性能

各類語言對於正則不一樣支持參考:
?Comparison of regular expression enginesgoogle

單行模式與多行模式

經過設置正則表達式後的修飾符(flag)可開啓對應的匹配模式:s(單行模式)和m(多行模式)。單行模式與多行模式這兩名字容易讓人誤覺得二者是互相排斥的, 也就是誤覺得開啓了多行模式,那麼就不是單行模式,而實際上單行模式或多行模式的開關並不會影響另外一個,兩個模式能夠同時用

單行模式

開關單行模式影響的是元字符.的匹配。單行模式開啓時,元字符.匹配包括換行符\n在內的任意字符,單行模式關閉時,元字符.匹配不包括換行符\n的任意字符。

多行模式

多行模式影響的是元字符^$。多行模式開啓時,元字符^能夠匹配字符串開頭(字符串的開始位置),也能夠匹配行的開頭(即換行符\n以後的位置),元字符$ 能夠匹配字符串結尾(字符串的結束位置), 也能夠匹配行的結尾(即換行符\n以前的位置)。多行模式關閉時,元字符^$只能匹配字符串的開頭和結尾,不能匹配換行符\n的以前和以後。也就是說若是沒有這兩個元字符,即便正則後加多行模式修飾符m也無心義,多行模式的正則必須有這兩個元字符中的一個或兩個纔有意義。
概念參考mark: ?正則表達式的多行模式與單行模式
附一個JS描述的多行模式:?multiline 屬性(正則表達式)(JavaScript)

注意:JS中正則並不支持單行模式的開啓

JS中的正則

JS中的正則須要注意下正則對象的方法與String對象方法的微妙區別。

RegExp對象

每一個正則對象實例有這幾項屬性global(是否帶有修飾符g的布爾值) ignoreCase(是否帶有修飾符i的布爾值) lastIndex(對象方法匹配開始的位置索引,初始爲0) multiline(是否帶有修飾符m的布爾值) source(正則源碼文本,注意源碼文本不包括修飾符)。

ES6中RegExp對象實例新增的只讀屬性sticky能夠判斷正則後是否帶修飾符y(新增),設置y修飾符的正則每次匹配,存在lastIndex屬性狀況下lastIndex不會自動歸零,而且在正則表達式中隱式地加入了開頭元字符^,這樣就使得正則表達式的匹配錨定在lastIndex的位置。這種用法的詳情可參考MDN文檔的栗子?:?RegExp.prototype.sticky 以及?JavaScript:正則表達式的/y標識

JS中每一個RegExp對象實例有兩個方法,分別是:exec()test()。這兩個方法運行邏輯幾乎等價,可是exec要複雜些,匹配失敗返回null,匹配成功它將返回一個數組, 數組第一個元素是匹配整個正則的內容,以後的元素是正則中捕獲組的匹配(注:使用非捕獲組能夠得到微弱的性能優點)舉個栗子:

//捕獲組()
var pattern1 = /(www)\.(baidu)\.(com)/i  
//非捕獲組(?:)
var pattern2 = /(?:www)\.(?:baidu)\.(?:com)/i

var str  = 'www.baidu.com'

pattern1.exec(str)
//返回數組["www.baidu.com", "www", "baidu", "com"]
pattern2.exec(str)
//返回數組["www.baidu.com"]

注意:雖然exec()和String的match()方法返回的都是數組的實例,可是他們的返回值都有兩個額外的屬性:indexinput分別表示匹配項在字符串中的位置和應用正則的整段字符串。

test()方法更爲簡單,匹配成功返回true,失敗返回false。這裏須要注意下這兩個方法中的lastIndex屬性與正則表達式中的全局修飾符g的關係。
當正則中存在全局修飾符g時,RegExp對象的lastIndex屬性將保存下一次開始的匹配的位置,好比:

var  pattern = /(www)\.(baidu)\.(com)/g 
var  str2 = 'www.baidu.com www.google.com www.baidu.com'
pattern.exec(str2)
pattern.lastIndex  //13

這意味着下次調用exec方法它將從被匹配字符串索引13的位置開始匹配(也能夠手動設置lastIndex的值)。test()方法同理。正由於存在殘留的lastIndex,因此使用正則對象的方法可能會致使一些意外的結果,不過在ES5中,正則表達式直接量的每次計算都會建立一個RegExp對象實例,他們各自擁有lastIndex,這就大大下降了意外的機率。
可是String對象中支持正則的方法卻與正則對象的方法不一樣,String方法忽略lastIndex,可是,若是存在全局修飾符g,string方法將有些許不一樣。

String中的正則方法

String方法中支持正則的有match(),replace(), search(),split()這四個方法。
match()方法接受一個字符串或正則做爲參數,當參數是正則時, 正則是否帶全局修飾符g將影響該方法的返回值,該方法匹配不帶全局修飾符的正則時,與RegExp對象實例方法exec()的返回值同樣, 匹配帶全局修飾符的正則時,match()方法將返回全局中所匹配到的全部內容而再也不將正則中的捕獲組編碼。好比前面的str2若是使用String的match方法的話其返回的數組中將是["www.baidu.com","www.baidu.com"]

replace(searchValue,replaceValue)方法一樣接受字符串或正則做爲第一個參數,當搜索值是字符串的時候,它只會匹配一次,不少時候這並不是咱們要的結果, 通常咱們但願全局匹配,因而可使用帶全局修飾符的正則表達式。replace()的替換值中的美圓符號$有特殊含義,好比$1表示正則表達式中第一個捕獲組的匹配內容。它的意義取自RegExp構造函數的屬性(正則表達式每次的操做都會影響構造函數RegExp的屬性,該構造函數屬性的詳細參考能夠看下《JS高級程序設計》中文第三版108頁中的說明)下面借用《JS權威指南》核心參考中的栗子。

var  name = 'Doe,John'
name.replace(/(\w+)*,(\w+)/,'$2 $1')//$num表明捕獲組的編號
"John Doe"

美圓符號的特殊含義概括:

  • $$ 替換對象$

  • $& 整個匹配的文本

  • $number 分組捕獲的文本(從1開始,不是0哦)

  • $` 匹配以前的文本

  • $' 匹配以後的文本

search()方法與String中indexOf相似,接受一個正則或者字符串爲參數,匹配成功返回第一個匹配的首字符位置, 失敗則返回-1。

split(separator,howmany)方法接受一個必選的分割位置做爲第一個參數,第二個參數設置返回數組的最大長度。第一個參數能夠是字符串或正則表達式,若是該參數是包含捕獲組(帶圓括號帶子表達式)的正則表達式,那麼返回的數組中包括與這些子表達式匹配的字串(但不包括與整個正則表達式匹配的文本)
舉個例子?:

'hello world'.split(/(\w)\s/)
//返回["hell", "o", "world"]

能夠看到捕獲組中的匹配("o")也在返回的數組中,可是整個正則的匹配被做爲分隔位置。並不是全部瀏覽器都支持split()這個特性。
MDN文檔中並未說明該特性更多細節:?String.prototype.split()

JS中不支持的正則特性

這裏也順便轉貼一下《JS高級程序設計》第三版中提到的JS(ES5)正則的侷限性:

  • 匹配字符串開始的結尾的A和Z錨(但支持以^和$來匹配字符串的開始的結尾)

  • 向後查找(但支持向前查找)

  • 並集和交集類

  • 原子組

  • Unicode支持(單個字符除外)

  • 命名的捕獲組(但支持編號的捕獲組)

  • s(single單行)和x(free-spacing無間隔)匹配模式

  • 條件匹配

  • 正則表達式註釋

JS的正則庫

以上所提到的侷限性中,可以使用正則庫或多或少的抵消一些,好比XRegExp 調用該庫以後就可使JS的正則支持可命名空間的捕獲組以及註釋等有用的特性, 關於JS的正則庫XRegExp,我寫了篇大體的介紹:?JavaScript正則庫:XRegExp

相關文章
相關標籤/搜索