javascript
玩转JavaScript正则表达式
Why Regular Expression
我們先來看看,我們干哈要學正則表達式這玩意兒:
- 復雜的字符串搜尋、替換工作,無法用簡單的方式(類似借助標準庫函數)達成。
- 能夠幫助你進行各種字符串驗證。
- 不止應用于編程語言中:JavaScript、JAVA、Perl、PHP、C#...
- 也應用于許多操作系統的主流指令中:Linux/Unix、Mac、Windows PowerScript
在我們常用的開發工具中,如Fiddler Willow、WebStorm、Vim,正則表達式也能幫助我們方便的進行Find&Replace的工作。由于正則表達式的流派很多,這篇文章主要是描述JavaScript中的正則表達式。
介紹點語法
定義
所謂正則表達式,就是一種描述字符串結構模式的形式化表達方法。
這是《精通正則表達式》對于它的定義,反正我看了這句話還是不知道正則表達式是干嘛用的,不過沒關系,下面我們先來看一下JavaScript的正則表達式中一些常用的語法。
創建方式
在JavaScript中,我們可以通過RegExp()構造函數或者RegExp直接量兩種方式去創建正則表達式。
var pattern1 = /s$/; var pattern2 = new RegExp('s$');上面代碼中的pattern1和pattern2是等價的,都是用來匹配所有以字母s結尾的字符串。
多說兩句:
在創建變量時,對于布爾、數值、字符串、null和undefined這個五個原始值類型來說,原始類型優于封裝對象,原因如下。
1、不同于字符串直接量,new出來的String是一個真正的對象,這意味著你不能使用內置操作符來比較兩個截然不同的String對象的內容。
var s = new String('hello'); typeof 'hello'; // string typeof s; // object var s1 = new String('hello'); var s2 = new String('hello'); s1 === s2; // false這是因為每個String對象對是一個單獨的對象,其總是只等于其自身。使用不嚴格相等運算符也是一樣。
s1 == s2; // false2、在ES5規范中,就像[],{}這樣的對象直接量一樣,程序運行時每次碰到RegExp直接量都會創建新對象。比如,如果在循環體中寫var pattern = /s$/,則每次遍歷都會創建一個新的正則表達式對象。然而在ES3規范中一個正則表達式直接量會在執行到它時轉換為一個RegExp對象,同一段代碼的正則表達式直接量的每次運算都返回同一個對象。而ES5做了相反的規定。用下面這段代碼做比較。
function getRE() {var re = /[a-z]/; re.foo = 'bar'; return re; } var reg = getRE(); re2 = getRE(); console.log(reg === re2); // 在ES3中返回true,在ES5中返回false reg.foo = 'baz'; console.log(re2.foo); // 在ES3中返回'baz',在ES5中返回'bar'顯然ES5的規范更符合開發者的期望。
各種表格
直接量字符
| 字母和數字字符 | 自身 |
| \o | NUL字符 |
| \t | 制表符(\u0009) |
| \n | 換行符(\u000A) |
| \v | 垂直制表符(\u000B) |
| \f | 換頁符(\u000C) |
| \r | 回車符(\u000D) |
| \xnn | 由十六進制數nn指定的拉丁字符 |
| \uxxx | 由十六進制數xxxx指定的Unicode字符 |
| \cX | 控制字符^X |
注:
由十六進制數nn指定的拉丁字符,例如:\x0A等價于\n
由十六進制數xxxx指定的Unicode字符:\u0009等價于\t
控制字符^X:\cJ等價于換行符\n
如果不記得哪些標點符號需要反斜桿轉義,可以在每個標點符號前都加上反斜桿。
字符類
| [...] | 方括號內任意字符 |
| [^...] | 不在方括號內的任意字符 |
| . | 除換行符和Unicode行終止符外的任意字符 |
| \w | 任何ASCⅡ字符組成的單詞,等價于[a-zA-Z0-9_] |
| \W | 任何不是ASCⅡ字符組成的單詞,等價于[^a-zA-Z0-9_] |
| \s | 任何Unicode空白符 |
| \S | 任何非Unicode空白符,注意\w和\S的不同 |
| \d | 任何ASCⅡ數字,等價于[0-9] |
| \D | 除了ASCⅡ數字之外的任何字符,等價于[^0-9] |
| [\b] | 退格直接量 |
注:
方括號又叫字符組,注意某些元字符在字符組外和字符組內的意義不同。例如:^在字符組外匹配行的開頭,在字符組內表示排除型字符;-在字符組外匹配普通連字符號,在字符組內(不在開頭)表示一個范圍;問號和點號在字符組外通常是元字符,但在字符組內只是匹配普通字符而已。
重復字符類
| {n,m} | 匹配前一項至少n次,但不能超過m次 |
| {n,} | 匹配前一項n次或多次 |
| {n} | 匹配前一項n次 |
| ? | 匹配前一項0次或1次,也就是說前一項是可選的,等價于{0,1} |
| + | 匹配前一項1次或多次,等價于{1,} |
| * | 匹配前一項0次或多次,等價于{0,} |
注:
javascript默認是貪婪匹配,也就是說匹配重復字符是盡可能多地匹配,而且允許后續的正則表達式繼續匹配。而進行非貪婪匹配,只需要在待匹配的字符后面跟隨一個問號即可:“??”、“+?”、“*?”、“{1,5}?”。比如:/a+/可以匹配一個或多個連續的字母a。當使用“aaa”作為匹配字符串時,/a+/會匹配它的三個字母。但是/a+?/會盡可能少的匹配,只能匹配第一個哦~
選擇、分組和引用字符
| "豎線" | 選擇,匹配的是該符號左邊的子表達式或右邊的子表達式 |
| (...) | 組合,將幾個項組合為一個單元,這個單元可通過“*”、“+”、“?”和"豎線"等符號修飾,而且可以記住和這個相匹配的字符串以供伺候的引用使用 |
| (?:...) | 只組合,把項組合到一個單元,但不記憶與該組相匹配的字符 |
| \n | 和第n個分組第一次匹配的字符相匹配,組是圓括號中的子表達式(也有可能是嵌套),組索引是從左到右的左括號數,“(?:”形式的分組不編碼 |
錨字符
| ^ | 匹配字符串的開頭,在多行檢索中,匹配一行的開頭 |
| $ | 匹配字符串的結尾,在多行檢索中,匹配一行的結尾 |
| \b | 匹配一個單詞的邊界,簡而言之,就是位于字符\w和字符\W之間的位置,或位于字符\w和字符串的開頭或結尾之間的位置(但需要注意的是在字符組內[\b]匹配的是退格符) |
| \B | 匹配非單詞邊界的位置 |
| (?=p) | 零寬正向先行斷言,要求接下來的字符都與p匹配,但不能包括匹配p的那些字符 |
| (?!p) | 零寬負向先行斷言,要求接下來的字符不與p匹配 |
修飾符
| i | 執行不區分大小寫的匹配 |
| g | 執行一個全局匹配,簡而言之,即找到所有的匹配,而不是在找到第一個之后就停止 |
| m | 多行匹配模式,^匹配一行的開頭和字符串的開頭,$匹配行的結束和字符串的結束 |
用于模式匹配的String方法
| String.search() | 參數:一個正則表達式。返回:第一個與參數匹配的子串的起始位置,如果找不到,返回-1。不支持全局搜索,如果參數是字符串,會先通過RegExp構造函數轉換成正則表達式。 |
| String.replace() | 檢索和替換。第一個參數:正則表達式,第二個參數:要進行替換的字符串,也可以是函數。設置了g修飾符,則替換所有匹配的子串,否則只替換第一個子串。通過在替換字符串中使用“$n”,可以使用子表達式相匹配的文本來替換字符。 |
| String.match() | 參數:一個正則表達式。返回:一個由匹配結果組成的數組。設置g則返回所有匹配結果,否則數組的第一個元素是匹配的字符串,剩下的是圓括號中的子表達式,即a[n]中存放的是$n的內容。 |
| String.split() | 參數:正則表達式或字符串。返回:子串組成的數組。 |
RegExp對象
RegExp構造函數
var pattern = new RegExp(arg1, arg2);arg1: 正則表達式中兩條斜桿之間的文本
arg2: 可選,指定修飾符:g,m,i
作用:動態創建正則表達式,例如待檢索的字符串是由用戶輸入的。
RegExp的屬性
| source | 只讀字符串,包含正則表達式的文本。 |
| global | 只讀布爾值,是否帶修飾符g |
| ignoreCase | 只讀布爾值,是否帶修飾符i |
| multiline | 只讀布爾值,是否帶修飾符m |
| lastIndex | 可讀寫整數,如果帶g修飾符,這個屬性儲存在整個字符串中下一次檢索開始的位置,這個屬性會被exec()和test()方法用到。 |
RegExp的方法
| exec() | 參數:字符串。在一個字符串中執行匹配檢索,與String.macth()非全局檢索類似,返回一個數組或null。 |
| test() | 參數:字符串。返回true or false |
| toString() | 轉換成字符串形式 |
關于RegExp對象的屬性和方法多說兩句:
RegExp對象的屬性index包含了發生匹配的字符位置,屬性input引用的是正在檢索的字符串。
當調用exec()或test()的正則表達式具有修飾符g時,它將把當前正則表達式對象的lastIndex屬性設置為緊挨著匹配子串的字符位置。如果沒發現任何匹配結果,lastIndex將重置為0。可以通過此特性反復調用exec()或test()來遍歷字符串。
ES5中,正則表達式直接量的每次計算都會創建一個新的RegExp對象,每個新的RegExp對象具有各自的lastIndex屬性,這勢必會大大減少“殘留”lastIndex屬性對程序造成的意外影響。
一些栗子
?
?
匹配URL
常見的URL:http://hostname/path.html當然,.htm或.shtml的結尾也很常見,或者干脆沒有path部分,還包括http或https的協議頭。
其實hostname的規則比較復雜,但是跟在http(s)://之后的就有可能是主機名,所以這個部分先簡單的用[-a-z0-9_.]來匹配,再加上可能存在的端口號,所以再加上:, 就成了[-a-z0-9_.:]。
path部分變化更多,所以需要使用[-a-z0-9_:@&?=+,.!/~*%$]來匹配。注意,連字符必須放在字符組的開頭,保證它是一個普通字符,而不是用來表示范圍。
綜合起來,我們得到的正則表達式就是:var patternURL = /https?:\/\/[a-z0-9_.:]+\/[-a-z0-9_:@&?=+,.!/~*%$]*(\.(html|htm|shtml))?/
因為我們降低了對匹配的要求,所以'http://.../foo.html' 這種顯然不是合法URL的字符串也能匹配,不過我覺得還好,畢竟我們需要在正則匹配的復雜性和完整性之間取得平衡。
接下來,我們一步步地對URL進行分析。
我們可以將URL分為三個部分:
協議頭:^http://或^https://
主機名:主機名是位于^http://之后和第一個反斜桿(如果有的話)之前的內容。
路徑:除了上面兩者之外的內容。
得到正則表達式:var patternURL = /^https?:\/\/([^/]+)(/.*)?$/
由于URL可能包含端口號,它位于主機名和路徑之間,以冒號開頭:?(:(\d)+)?
得到正則表達式:var patternURL = /^https?:\/\/([^/:]+)(:(\d)+)?(/.*)?$/
匹配合法的主機名:由點號分隔部分組成,每個部分可以包括ASCⅡ字符、數字和連字符,但不能以連字符開頭和結尾。則可以得到:var patternHostname = /[a-z0-9]|[a-z0-9][-a-z0-9]*[a-z0-9]/i
結尾的后綴部分只有有限個可能:(com|edu|gov|int|mil|net|org|biz|info|name|museum|coop|aero|[a-z][a-z])
完善后得到:var patternHostname =/^([a-z0-9]\.|[a-z0-9][-a-z0-9]{0,61}[a-z0-9]\.)(com|edu|gov|int|mil|net|org|biz|info|name|museum|coop|aero|[a-z][a-z])$/i
匹配HTML Tag
匹配HTML標簽嘛,感覺很簡單的樣子,我們的第一反應可能是:var pattern = /<[^>]+>/
不過這樣匹配可能存在的問題是:如果tag中含有>,上面的正則就不能正常匹配了。如:
<input name=123 value=">" >
雖然上面這種HTML的寫法很少(sha)見(bi),但確實合法的。因此,簡單的<[^>]+>就不能用了,需要想個聰明點的辦法。
我們先來看一下HTML Tag中有什么規則:<...>中能夠出現
引用文本:HTML中的引文可以用雙引號,也可以用單引號,但不允許嵌套轉義的引號。
因此我們可以使用/("[^"]*"|'[^']*')/來匹配。
其他文本:除了>和引號之外的任意字符
可以使用/[^'">]/來匹配
現在可以得出匹配HTML Tag的正則表達式最終版!
var pattern = /<("[^"]*"|'[^']*'|[^'">])*>/
給這個正則表達式來點注釋:
< # 開始的尖括號"<"( # 任意數量的..."[^"]*" # 雙引號字符串| # 或者是...'[^']*' # 單引號字符串 | # 或者是... [^'">] # "其他文本" )* # > # 結束的尖括號">"需要注意的是,我們不用"+"來修飾[^'">]的原因是([^'">]+)*可能會帶來災難性的后果。匹配次數呈指數級增長。比如:對于簡單的目標字符串helloworld,是星號會迭代10次,每一次迭代中[^'">]+匹配一個字符?還是星號迭代3次,內部的[^'">]+分別匹配5、2、3個字符?或者2、3、1、4個字符?還是其他情況?這樣會把正則引擎搞瘋掉的啦!
匹配String
其實匹配引號內字符串的最簡單辦法是用這個表達式:/"[^"]*"/。
不過我們要容許其中包含轉義的引號,例如:"we have a \"awesome\" world!"。
下面進行任務分解:
不過由于轉義之后的引號也能夠出現的正文中,所以處理起來比較棘手哈。
我們還是以"we have a \"awesome\" world!"為例子。如果JavaScript中有逆序環視(lookaround)可用,我們可以這樣寫:var pattern = /"([^"]|(?<=\\)")*"/。
但是這個正則表達式無法匹配下面這兩個無聊的例子:"/-|-\\" or "[^-^]"
我本來想匹配"/-|-\\",結果匹配的確是"/-|-\\" or "。
注:
這里的結束分隔符是一個引號,但正文也可能包含轉義之后的引號。匹配開始和結束分隔符很容易,訣竅就在于,匹配正文的時候不要超越結束分隔符。
匹配正文的思路:1、不是引號:由[^"]匹配。2、是一個引號,而它左邊又有一個反斜桿,那么這個引號也屬于正文。使用逆序環視:/"([^"]|(?<=\\)")*"/
鑒于上面的例子,我們需要對var pattern = /"([^"]|(?<=\\)")*"/進行修改!
第一個表達式的問題在于,我們把反斜桿認為只是用來轉義引號的,其實反斜桿在字符串中可以用來轉義任何字符。因此,我們要匹配的文本其實是開始引號和結束引號之間,包括轉義字符和非引號的任何字符。得到:/"(\\.|[^"])*"/
不過!
上面的表達式還是會錯誤的匹配:"You need a new\"world\" haha.?中的"You need a new\"world\"?即使這并不是一個字符串。
因為,這個表達式一開始匹配到了引號之后的文本,如果找不到結束的引號,它就會回溯。而[^"]匹配到了world\里的反斜桿后,之后的那個引號會被表達式認為是一個結束的引號。。。
繼續改改改!
所以我們需要保證,字符串里的反斜桿不能以[^"]方式匹配。要將[^"]改為[^\\"]
上面的正則表達式使用了JavaScript正則表達式并不茲瓷的逆序環視,這里給出JavaScript支持的版本。
/(["'])(((\\.|[^\1\\])*)+)\1/?或者?/^(['"])(((\\['"])?([^\1])*)+)\1/
好了,由于本人筆力有限,關于JavaScript的正則表達式只能介紹到這里,感興趣的同學可以去閱讀犀牛書的第十章以及《精通正則表達式》這本書
原文鏈接:http://ivweb.io/topic/56e804ef1a5f05dc50643106
本文編輯:宋秉金
總結
以上是生活随笔為你收集整理的玩转JavaScript正则表达式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: sql语句中as的用法和作用
- 下一篇: springmvc 后台向页面EasyU