本文同步自個人博客園:http://www.cnblogs.com/hustskyking/php
關於正則表達式,網上能夠搜到一大片文章,我以前也蒐集了一些資料,並作了排版整理,能夠看這篇文章http://www.cnblogs.com/hustskyking/archive/2013/06/04/RegExp.html,做爲基礎入門講解,這篇文章說的十分到位。css
記得最開始學習正則,是使用 php 作一個爬蟲程序。爲了獲取指定的信息,必須用必定的方式把有規律的數據匹配出來,而正則是首選。下面是當時寫的爬蟲程序的一個代碼片斷:html
$regdata = "/<font size=\"3\">((?<bf>[^<]*)<br \/>){0,1}⊙(?<bs>.{12})\S*\s/"; //獲取頁面 $html = file_get_contents('http://www.qnwz.cn/html/daodu/201107/282277.html'); $html = iconv("GBK", "UTF-8", $html); if ($html == '') { die("<hr />出錯:【錯】沒法打開《青年文摘》頁面<hr />"); } //匹配頁面信息 preg_match_all($regdata, $html, $mdata); print_r($mdata);
當時寫代碼還真是歡樂多,什麼都不懂,什麼都是新知識,學起來津津有味。我以爲學習知識必定要把握最基本的原理,先把一個知識的大概輪廓搞清楚,而後學習怎麼去使用他,完了就是深刻學習,瞭解底層基礎實現。不少人解決問題都是靠經驗,這個固然很重要,但若是咱們弄懂了一項技術最底層的實現,徹底能夠靠本身的推斷分析出問題的根源。我對一些公司的招聘要求特別不滿,說什麼要三年五年Javascript編程經驗云云,經驗固然和時間成正相關,可是對於那些沒有三年五年工做經驗卻照樣可以解決實際的人呢?算是小小的吐槽吧,下面進入正題。正則表達式
畫了一個草圖,簡單的說明了下正則表達式的工做原理。編程
+--------+ | 編譯 | +--------+ | ↓ +----------------+ | 設置開始位置 |←---------+ +----------------+ ↑ | | ↓ 其 | +----------------+ 他 | | 匹配 & 回溯 | 路 | +----------------+ 徑 | | | ↓ | +----------------+ | | 成功 or 失敗 |---------→+ +----------------+
你寫的任何一個正則直接量或者 RegExp 都會被瀏覽器編譯爲一個原生代碼程序,第一次匹配是從頭個字符開始,匹配成功時,他會查看是否還有其餘的路徑沒有匹配到,若是有的話,回退到上一次成功匹配的位置,而後重複第二步操做,不過此時開始匹配的位置(lastIndex)是上次成功位置加 1.這樣說有點難以理解,下面寫了一個 demo,這個 demo 就是實現一個正則表達式的解析引擎,由於邏輯和效果的表現都太複雜了,因此只作了一個簡單的演示:瀏覽器
http://qianduannotes.duapp.com/demo/regexp/index.htmlapp
若是要深刻了解正則表達式的內部原理,必須先理解匹配過程的一個基礎環節——回溯,他是驅動正則的一個基本動力,也是性能消耗、計算消耗的根源。性能
正則表達式中出現最多的是分支和量詞,上面的 demo 中能夠很清楚的看到 hi 和 hello 這兩個分支,當匹配到第一個字符 h 以後,進入 (i|ello) 的分支選擇,首先是進入 i 分支,當 i 分支匹配完了以後,再回到分支選擇的位置,從新選擇分支。簡單點說,分支就是 |
操做符帶來的多項選擇問題,而量詞指的是諸如 *, +?, {m,n}
之類的符號,正則表達式必須決定什麼時候嘗試匹配更多的字符。下面結合回溯詳細說說分支和量詞。學習
繼續分析上面那個案例。"Lalala. Hi, barret. Hello, John".match(/H(i|ello), barret/g)
,首先會查找 H 字符,在第九位找到 H 以後,正則子表達式提供了兩個選擇 (i|ello),程序會先拿到最左邊的那個分支,進入分支後,在第十位匹配到了 i,接着匹配下一個字符,下一個字符是逗號,接着剛纔的位置又匹配到了這個逗號,而後再匹配下一個,依次類推,直到完整匹配到整個正則的內容,此時程序會在Hi, barret
後面作一個標記,表示在這裏進行了一次成功的匹配。但程序到此並無結束,由於後面加了一個全局參數,依然使用這個分支日後匹配,很顯然,到了 Hello 的時候,Hi 分支匹配不了了,因而程序會回溯到剛纔咱們作標記的位置,並進入第二個分支,從作標記的位置從新開始匹配,依次循環。測試
只要正則表達式沒有嘗試完全部的可選項,他就會回溯到最近的決策點(也就是上次匹配成功的位置)。
量詞這個概念特別簡單,只是在匹配過程當中有貪婪匹配和懶惰匹配兩種模式,結合回溯的概念理解稍微複雜。仍是用幾個例子來講明。
1) 貪婪
str = "AB1111BA111BA"; reg = /AB[\s\S]+BA/; console.log(str.match(reg));
首先是匹配AB,遇到了 [\s\S]+
,這是貪婪模式的匹配,他會一口吞掉後面全部的字符,也就是若是 reg 的內容爲 AB[\s\S]+,那後面的就不用看了,直接所有匹配,而日後看,正則後面還有B字符,因此他會先回溯到倒數第一個字符,匹配看是否爲 B,顯然倒數第一個字符不是B,因而他又接着回溯,找到了B字母,找到以後就不繼續回溯了,而是日後繼續匹配,此刻匹配的是字符A,程序發現緊跟B後的字母確實是A,那此時匹配就結束了。若是沒有看明白,能夠再讀讀下面這個圖:
REG: /AB[\s\S]+BA/ MATCH: A 匹配第一個字符 AB 匹配第二個字符 AB1111BA111BA [\s\S]+ 貪婪吞併全部字符 AB1111BA111BA 回溯,匹配字符B AB1111BA111B 找到字符B,繼續匹配A AB1111BA111BA 找到字符A,匹配完成,中止匹配
2) 懶惰(非貪婪)
str = "AB1111BA111BA"; reg = /AB[\s\S]+?BA/; console.log(str.match(reg));
與上面不一樣的是,reg 中多了一個 ? 號,此時的匹配模式爲懶惰模式,也叫作非貪婪匹配。此時的匹配流程是,先匹配AB,遇到[\s\S]+?,程序嘗試跳過並開始匹配後面的字符B,日後查看的時候,發現是數字1,不是要匹配的內容,繼續日後匹配,知道遇到字符B,而後匹配A,發現緊接着B後面就有一個A,因而宣佈匹配完成,中止程序。
REG: /AB[\s\S]+BA/ MATCH: A 匹配第一個字符 AB 匹配第二個字符 AB [\s\S]+? 非貪婪跳過並開始匹配B AB1 不是B,回溯,繼續匹配 AB11 不是B,回溯,繼續匹配 AB111 不是B,回溯,繼續匹配 AB1111 不是B,回溯,繼續匹配 AB1111B 找到字符B,繼續匹配A AB1111BA 找到字符A,匹配完成,中止匹配
若是匹配的內容是 AB1111BA,那貪婪和非貪婪方式的正則是等價的,可是內部的匹配原理仍是有區別的。爲了高效運用正則,必須搞清楚使用正則時會遇到那些性能消耗問題。
//去測試下這句代碼 "TTTTTTTT".match(/(T+T+)+K/); //而後把前面的T重複次數改爲30 //P.S:當心風扇狂轉,CPU暴漲
咱們來分析下上面這段代碼,上面使用的都是貪婪模式,那麼他會這樣作:
REG: (T+T+)+K MATCH: ①第一個T+匹配前7個T,第二個T+匹配最後一個T,沒找到K,宣佈失敗,回溯到最開始位置 ②第一個T+匹配前6個T,第二個T+匹配最後兩個T,沒找到K,宣佈失敗,回溯到最開始位置 ③... ... 接着還會考慮(T+T+)+後面的 + 號,接着另外一輪的嘗試。 ⑦... ...
這段程序並不會智能的去檢測字符串中是否存在 K,若是匹配失敗,他會選擇其餘的匹配方式(路徑)去匹配,從而形成瘋狂的回溯和從新匹配,結果可想而知。這是回溯失控的典型例子。
前瞻有兩種,一種是負向前瞻,JS中使用 (?!xxx)
來表示,他的做用是對後面要匹配的內容作一個預判斷,若是後面的內容是xxx,則此段內容匹配失敗,跳過去從新開始匹配。另外一種是正向前瞻,(?=xxx)
,匹配方式和上面相反,還有一個長的相似的是 (?:xxx)
,這個是匹配xxx,他是非捕獲性分組匹配,即匹配的內容不會建立反向引用。具體內容能夠去文章開頭提到的文檔中查看。
反向引用,這個在 replace 中用的比較多,在 replace 中:
字符 | 替換文本 |
---|---|
$一、$二、...、$99 | 與 regexp 中的第 1 到第 99 個子表達式相匹配的文本。 |
$& | 與 regexp 相匹配的子串。 |
$` | 位於匹配子串左側的文本。 |
$' | 位於匹配子串右側的文本。 |
$$ | 直接量符號。 |
而在正則表達中,主要就是 \1, \2 之類的數字引用。前瞻和反向引用使用恰當能夠大大的減小正則對資源的消耗。舉個例子來簡單說明下這幾個東西:
問題:使用正則匹配過濾後綴名爲 .css 和 .js 的文件。 如:test.wow.js test.wow.css test.js.js等等。
有人會立馬想到使用負向前瞻,即:
//過濾js文件 /(?!.+\.js$).*/.exec("test.wow.js") //過濾js和css文件 /(?!.+\.js$|.+\.css$).*/.exec("test.wow.js") /(?!.+\.js$|.+\.css$).*/.exec("test.wow.html")
可是你本身去測試下,拿到的結果是什麼。匹配非js和非css文件能夠拿到正確的文件名,可是咱們指望這個表達式對js和css文件的匹配結果是null,上面的表達式卻作不到。問題是什麼,由於(?!xxx)和(?=xxx)都會消耗字符,在作預判斷的時候把 .js 和 .css 給消耗了,因此這裏咱們必須使用非捕獲模式。
/(?:(?!.+\.js$|.+\.css$).)*/.exec("test.wow.html"); /(?:(?!.+\.js$|.+\.css$).)*/.exec("test.wow.js");
咱們來分析下這個正則:
(?:(?!.+\.js$|.+\.css$).)* --- ---------------- - | | | +----------------------+ ↓ | 非捕獲,內部只有一個佔位字符 | ↓ 負向前瞻以.js和.css結尾的字符串
最後一個星號是貪婪匹配,直接吞掉所有字符。
這裏講的算是有點複雜了,不過在稍複雜的正則中,這些都是很基礎的東西了,想在這方面提升的童鞋能夠多研究下。
JavaScript的正則算是比較弱的,他沒有分組命名、遞歸、原子組等功能特別強的匹配模式,不過咱們能夠利用一些組合方式達到本身的目的。上面的例子中,咱們實際上用正則實現了一個或和與的功能,上面的例子體現的還不是特別明顯,再寫個例子來展現下:
str1 = "我(wo)叫(jiao)李(li)靖(jing)"; str2 = "李(li)靖(jing)我(wo)叫(jiao)"; reg = /(?=.*?我)(?=.*?叫)(?=.*?李)(?=.*?靖)/; console.log(reg.test(str1)); //true console.log(reg.test(str2)); //true
無論怎麼打亂順序,只要string中包含「我」,「是」,「李」,「靖」這四個字,結果都是true。
相似(?=xxx)\1,就至關於一個原子組,原子組的做用就是消除回溯,只要是這種模式匹配過的地方,回溯時都不會到這裏和他以前的地方。上面的程序"TTTTTTTT".match(/(T+T+)+K/);
能夠經過原子組的方式處理:
"TTTTTTTT".match(/(?=(T+T+))\2+K/);
如此便能完全消除回溯失控問題。
關於正則的學習,重點是要多練習多實踐,而且多嘗試用不一樣的方案去解決一個正則問題,一個很典型的例子,去除字符串首尾的空白,嘗試用5-10種不一樣的正則去測試,並思考哪些方式的效率最高,爲何?經過這一連串的思考能夠帶動你學習的興趣,也會讓你成長的比較快~