學習了半年的正則表達式,也不能說一直學習吧,就是和它一直在打交道,如何用正則表達式解決本身的問題,而且還要考慮如何在匹配大量的文本時去優化它。慢慢的以爲正則已經成爲本身的一項技能,逐漸的從一個正則表達式小白變成一個僞精通者。javascript
那麼,我不打算詳細介紹正則表達式的使用,或者說這篇文章並非入門教程,因此若是你對正則表達式一無所知或者處於入門階段,建議你仍是先去看看下面這些正則表達式入門的文章。html
阮一峯老師的正則教程
MDN 正則介紹
鬍子哥正則表達式 30 分鐘入門
阮一峯 ES6 正則表達式擴展
百度百科 正則表達式 很詳細,能夠看成手冊參考java
固然正則的教程不少,不限於此,若是你對正則已經瞭解了,那麼能夠開始下面的內容了,文章中可能還會涉及一些效率的問題。es6
若是寫過 Python 的同窗,都必定會知道 Python 中能夠在字符串前面加個小寫的 r ,來表示防止轉義。防止轉義的意思就是說:str = r"\t' 等價於 str = '\\t',加了 r 會防止 \ 被轉義。正則表達式
爲何要介紹這個,由於這就是 new RegExp
和 //
的區別,由於咱們知道在正則表達式中會頻繁的使用轉義字符 \w\s\d 等,可是它們在內存中的是以 \\w\\s\\d 存儲的,看個例子:數組
//推薦寫法 var regex1 = /\w+/g; regex1 // /\w+/g //RegExp 寫法 var regex2 = new RegExp('\\w+','g'); regex2 // /\w+/g //錯誤寫法 var regex3 = new RegExp('\w+','g'); regex3 // /w+/g
你也看出來了,錯誤寫法只能匹配 wwwww
這樣的字符串,曾經我就見過有人把他們弄混了,還說第一個第三個沒有區別。第二種方法的輸出,仍是 /\w+/g,中間仍是要轉換,因此推薦第一種寫法。ide
固然,還有比較奇葩的:函數
var regex4 = new RegExp(/\w+/g); regex4 // /\w+/g
MSDN 上關於 RegExp 的介紹。性能
那麼,如何能像 Python 的 r''
那樣,實現一個防轉義的功能呢?我這裏有一種很蹩腳的方法(僅供娛樂!):學習
var str1 = '\d\w\s'; str1; // "dws" var str2 = /\d\w\s/; str2.source; // "\d\w\s"
沒錯,就是 srouce
,不知道 source 的同窗快去面壁吧。(這方法確實很摳腳!)
這幾個修飾符只是針對 JS 來講的,像 Python 中還有 re.S
表示 . 能夠匹配換行符。
對於 i 表示忽略字母大小寫,不是很經常使用,由於它有不少替代品,好比:/[a-zA-Z]/
能夠用來替代 /[a-z]/i
,至於二者處理長文本的時間效率,我本身沒有研究過,不下定論。
使用 i 須要注意的地方,就是 i 會對正則表達式的每個字母都忽略大小寫,當咱們須要部分單詞的時候,能夠考慮一下/(?:t|T)he boy/
。
g 表示全局匹配,在印象中,可能不少人會以爲全局匹配就是當使用 match 的時候,把全部符合正則表達式的文本所有匹配出來,這個用途確實很普遍,不過 g 還有其餘更有意思的用途,那就是 lastIndex
參數。
var str = '1a2b3c4d5e6f', reg = /\d\w\d/g; str.match(reg); //["1a2", "3c4", "5e6"]
爲何不包括2b3,4d5
,由於正則表達式匹配的時候,會用 lastIndex
來標記上次匹配的位置,正常狀況下,已經匹配過的內容是不會參與到下次匹配中的。帶有 g 修飾符時,能夠經過正則對象的 lastIndex 屬性指定開始搜索的位置,固然這僅僅侷限於函數 exec 和 test(replace 沒研究過,沒據說過能夠控制 lastIndex,match 返回的是數組,沒法控制 lastIndex),針對這個題目修改以下:
var str = '1a2b3c4d5e6f', reg = /\d\w\d/g; var a; var arr = []; while(a = reg.exec(str)){ arr.push(a[0]); reg.lastIndex -= 1; } arr //["1a2", "2b3", "3c4", "4d5", "5e6"]
m 表示多行匹配,我發現不少人介紹 m 都只是一行略過,其實關於 m 仍是頗有意思的。首先,來了解一下單行模式,咱們知道 JavaScript 正則表達式中的 .
是沒法匹配 \r\n (換行,各個系統使用不同) 的,像 Python 提供 re.S 表示 .
能夠匹配任意字符,包括 \r\n,在 JS 中若是想要表示匹配任意字符,只能用[\s\S] 這種蹩腳的方式了(還有更蹩腳的 [\d\D],[.\s])。這種模式叫作開啓或關閉單行模式,惋惜 JS 中沒法來控制。
多行模式跟 ^ $
兩兄弟有關,若是你的正則表達式沒有 ^$,即時你開啓多行模式也是沒用的。正常的理解/^123$/
只能匹配字符串123
,而開啓多行模式/^123$/g
能匹配['123','\n123','123\n','\n123\n'],相對於 ^$ 能夠匹配 \r\n 了。
var str = '\na'; /^a/.test(str); //false /^a/m.test(str); //true
有人說,m 沒用。其實在某些特殊的格式下,你知道你要匹配的內容會緊接着 \r\n 或以 \r\n 結尾,這個時候 m 就很是有用,好比 HTTP 協議中的請求和響應,都是以 \r\n 劃分每一行的,響應頭和響應體之間以 \r\n\r\n 來劃分,咱們須要匹配的內容就在開頭,經過多行匹配,能夠很明顯的提升匹配效率。
原理性的東西,咱們仍是要知道的,萬一之後會用到。
在正則表達式中,括號不能亂用,由於括號就表明分組,在最終的匹配結果中,會被算入字匹配中,而 (?:) 就是來解決這個問題的,它的別名叫作非捕獲分組。
var str = 'Hello world!'; var regex = /Hello (\w+)/; regex.exec(str); //["Hello world", "world"] var regex2 = /Hello (?:\w+)/; regex2.exec(str); //["Hello world"] //replace 也同樣 var regex3 = /(?:ab)(cd)/ 'abcd'.replace(regex3,'$1') //"cd"
能夠看到 (?:) 並不會把括號裏的內容計入到子分組中。
關於 (?=),新手理解起來可能比較困難,尤爲是一些很牛逼的預查正則表達式。其實還有個 (?!),不過它和 (?=) 是屬於一類的,叫作正向確定(否認)預查,它還有不少別名好比零寬度正預測先行斷言。但我以爲最重要的只要記住這兩點,預查和非捕獲。
預查的意思就是在以前匹配成功的基礎上,在向後預查,看看是否符合預查的內容。正由於是預查,lastIndex 不會改變,且不會被捕獲到總分組,更不會被捕獲到子分組。
var str = 'Hello world!'; var regex = /Hello (?=\w+)/; regex.exec(str); //["Hello "]
和 (?:) 區別是:我習慣的會把匹配的總結果叫作總分組,match 函數返回數組每一項都是總分組,exec 函數的返回數組的第一項是總分組。(?:) 會把括號裏的內容計入總分組,(?=) 不會把括號裏的內容計入總分組。
說白了,仍是強大的 lastIndex 在起做用。(?:) 和 (?=) 差異是有的,使用的時候要合適的取捨。
說了這麼多關於 (?=) 的內容,下面來點進階吧!如今的需求是一串數字表示錢 "10000000",可是在國際化的表示方法中,應該是隔三位有個逗號 "10,000,000",給你一串沒有逗號的,替換成有逗號的。
var str = "10000000"; var regex = /\d(?=(\d{3})+$)/g; str.replace(regex, '$&,'); //"10,000,000"
咱們分析一下 regex,/\d(?=(\d{3})+$)/g 它是全局 g,實際上它匹配的內容只有一個 \d,(?=(\d{3})+$) 是預判的內容,以前說過,預判的內容不計入匹配結果,lastIndex 仍是停留在 \d 的位置。(?=(\d{3})+$) 到結尾有至少一組 3 個在一塊兒的數字,纔算預判成功。
\d = 1 的時候,不知足預判,向後移一位,\d = 0,知足預判,replace。
(?=) 和 (?!) 叫作正向預查,但每每是正向這個詞把咱們的思惟給束縛住了。正向給人的感受是隻能在正則表達式後面來預判,那麼預判爲何不能放在前面呢。下面這個例子也很是有意思。
一個簡單密碼的驗證,要保證至少包含大寫字母、小寫字母、數字中的兩種,且長度 8~20。
若是能夠寫多個正則,這個題目很簡單,思路就是:/^[a-zA-Z\d]{8,20}$/ && !(/[a-z]+/) && !(/[A-Z]+/) && !(/\d+/),看着眼都花了,好長一串。
下面用 (?!) 前瞻判斷來實現:
var regex = /^(?![a-z]+$)(?![A-Z]+$)(?!\d+$)[a-zA-Z\d]{8,12}$/; regex.test('12345678'); //false regex.test('1234567a'); //true
分析一下,由於像 (?!) 預判不消耗 lastIndex,徹底能夠放到前面進行前瞻。(?![a-z]+$)
的意思就是從當前 lastIndex (就是^)開始一直到 $,不能全是小寫字母,(?![A-Z]+$)
不能全是大寫字母,(?!\d+$) 不能全是數字,[a-zA-Z\d]{8,12}$ 這個是主體,判斷到這裏的時候,lastIndex 的位置仍然是 0,這就是 (?!) 前瞻帶來的效率。
咱們都知道,JS 中的正則表達式是不支持正回顧後發斷言的 (?<=)
,固然也不支持 (?<!)
。有時候會以爲這種正回顧後發斷言確實頗有幫助,它可讓咱們的思惟更清晰,哪些是真正匹配的正則,哪些是斷言的正則。在 Python 中咱們就能夠輕鬆的使用 (?<=),可是在 JS 中不行。
緣由多是採用的正則引擎不同致使,既然不支持,那咱們也只能經過現有的條件來改進咱們所寫的正則,下面就說一說個人理解。
對於一個非全局匹配的正則表達式,徹底能夠經過 (?:)
來實現。好比對於 /(?<=Hello) (.*)$/
(這個在 JS 中是不支持的),可使用 /(?:Hello) (.*)$/
做爲一個簡單的替代,這兩個正則的差異就在於最終的匹配分組上面,總分組略有不一樣,但總有辦法能夠解決。但要注意,這是非全局匹配,反正只匹配一次。
那若是是全局匹配呢?又該如何實現 (?<=)
?
var str = 'a1b2c3d'; //var regex = /(?<=\w)\d\w/g //str.match(regex) => ['1b','2c','3d'] var regex2 = /(?:\w)\d\w/g str.match(regex2); //["a1b", "c3d"]
很明顯,只經過 (?:)
就顯得有點力不從心了,咱們想要的結果是 ['1b','2c','3d']
,卻返回其中的第一和第三個,少了第二個。
這時候,又要拿出強大的 lastIndex
:
var str = 'a1b2c3d'; var regex = /(?:\w)(\d\w)/g; var m,arr = []; while(m = regex.exec(str)){ arr.push(m[1]); regex.lastIndex --; } arr; //["1b", "2c", "3d"]
和前面的例子很相似,經過重寫 lastIndex 的值,達到模仿 (?<=)
的做用。
貪婪出如今 + * {1,}
這種不肯定數量的匹配中,所謂的貪婪,表示正則表達式在匹配的時候,儘量多的匹配符合條件的內容。好比 /hello.*world/
匹配'hello world,nice world'
會匹配到第二個 world 結束。
鑑於上面的狀況,可使用 ? 來實現非貪婪匹配。? 在正則表達式中用途不少,正常狀況下,它表示前面那個字符匹配 0 或 1 次,就是簡化版的 {0,1}
,若是在一些不肯定次數的限制符後面出現,表示非貪婪匹配。/hello.*?world/
匹配'hello world,nice world'
的結果是 hello world
。
我剛開始寫正則的時候,寫出來的正則都是貪婪模式的,每每獲得的結果和預想的有些誤差,就是由於少了 ? 的緣由。
我初入正則的時候,非貪婪模式還給我一種錯覺。仍是前面的那個例子,被匹配的內容換一下,用/hello.*?world/
匹配'hello word,nice world'
,由於 word 不等於 world,在第一次嘗試匹配失敗以後,應該返回失敗,但結果倒是成功的,返回的是 'hello word,nice world'
。
一開始我對於這種狀況是不理解的,但仔細想一想也對,這原本就應該返回成功。至於如何在第一次嘗試匹配失敗以後,後面就再也不繼續匹配,只能經過優化 .*
。若是咱們把 .*?end
這樣子來看,.*
會把全部字符都吞進去,慢慢吐出最後幾個字符,和 end 比較,若是是貪婪,吐到第一個知足條件的就中止,若是是非貪婪,一直吐到不能吐爲止,把離本身最近的結果返回。
因此,貪婪是返回最近的一次成功匹配,而不是第一次嘗試。
回溯能夠殺死一個正則表達式,這一點都不假。關於正則表達式回溯也很好理解,就是正則引擎發現有兩條路能夠走時,它會選擇其中的一條,把另外一條路保存以便回溯時候用。
好比正則 /ab?c/
在成功匹配到 a 以後,後面能夠有 b,也能夠沒有 b,這時候要提供兩種選擇。還有其餘類型的回溯,好比 /to(night|do)/
。固然影響性能的回溯就要和 .* .+ .{m}
有關。
所謂的回溯失控,就是可供選擇的路徑太多,看一個常見回溯失控的例子,正則 /(A+A+)+B/
,若是匹配成功,會很快返回,那麼匹配失敗,很是可怕。好比來匹配 10 個 A AAAAAAAAAA
,假設第一個 A+ 吞了 9 個 A,整個正則吐出最後一個字符發現不是 B,這一輪吐完,還不能返回 false,由於還有其餘路能夠選擇;第一個 A+ 吞 8 個 A,....一直這樣回溯下去,回溯次數的複雜度大概是 2 的 n 次方吧。
固然你可能會說,本身不會寫這樣傻的正則表達式。真的嗎?咱們來看一個匹配 html 標籤的正則表達式,/<html>[\s\S]*?<head>[\s\S]*?</head>[\s\S]*?<body>[\s\S]*?</body>[\s\S]*?</html> (感受這樣寫也很傻)。若是一切都 OK,匹配一個正常的 HTML 頁面,工做良好。可是若是不是以 </html>
結尾,每個 [\s\S]*? 就會擴大其範圍,一次一次回溯查找知足的一個字符串,這個時候可怕的回溯就來了。
在說到回溯的同時,有時候仍是要考慮一下 . * {}
查詢集合的問題,反正個人建議是儘可能避免使用匹配任何字符的 [\s\S],這真的是有點太暴力了。由於咱們寫正則的時候,都是以正確匹配的思路去寫的,同時還須要考慮若是匹配不成功,該如何儘快的讓 [a-zA-Z]*
集合儘快中止。好比一般在匹配 HTML 標籤的時候正則若是這樣寫 /<([^>]+)>[sS]*?<\/\1>/ (匹配一個不帶 class 等屬性的標籤),匹配成功時,一切都好說,若是匹配失敗,或者匹配的文本中剛好只有左半個 < ,因爲範圍 [^>] 範圍太大,根本停不下來,相比來講 /<(\w+)>[\s\S]*?<\/\1>/` 要好一些。又好比 [^\r\n]* 在匹配單行時效果不錯,即時匹配失敗也能夠快速中止。
感受這篇文章寫的很亂,東扯西扯的,大概把我這幾個月以來所學到的正則表達式知識都寫在了這裏,固然這並不包括一些基礎的知識。我以爲學習正則最主要的仍是去練習,只有在實際項目中總結出來的正則經驗,纔算本身正在掌握的,若是隻是簡單的掃一眼,時間久了,終究會忘記。共勉!
如何找出文件名爲 ".js" 的文件,但要過濾掉 ".min.js" 的文件。
代碼以下:
歡迎來個人博客參考代碼。