正則表達式是對字符串的結(jié)構(gòu)進行的形式化描述,非常簡潔優(yōu)美,而且功能十分強大。很多的語言都不同程度的支持正則表達式,而在很多的文本編輯器如 Emacs,vim,UE 中,都支持正則表達式來進行字符串的搜索替換工作。UNIX 下的很多命令行程序,如 awk,grep,find 更是對正則表達式有良好的支持。
JavaScript 同樣也對正則表達式有很好的支持,RegExp 是 JavaScript 中的內(nèi)置“類”,通過使用RegExp,用戶可以自己定義模式來對字符串進行匹配。而 JavaScrip t中的 String 對象的 replace 方法也支持使用正則表達式對串進行匹配,一旦匹配,還可以通過調(diào)用預(yù)設(shè)的回調(diào)函數(shù)來進行替換。
正則表達式的用途十分廣泛,比如在客戶端的 JavaScript 環(huán)境中的用戶輸入驗證,判斷用戶輸入的身份證號碼是否合法,郵件地址是否合法等。另外,正則表達式可用于查找替換工作,首先應(yīng)該關(guān)注的是正則表達式的基本概念。
關(guān)于正則表達式的完整內(nèi)容完全是另外一個主題了,事實上,已經(jīng)有很多本專著來解釋這個主題,限于篇幅,我們在這里只關(guān)注 JavaScript 中的正則表達式對象。
本節(jié)討論正則表達式中的基本概念,這些基本概念在很多的正則表達式實現(xiàn)中是一致的,當(dāng)然,細節(jié)方面可能會有所不同,畢竟正則表達式是來源于數(shù)學(xué)定義的,而不是程序員。JavaScriipt 的正則表達式對象實現(xiàn)了 perl 正則表達式規(guī)范的一個子集,如果你對 perl 比較熟悉的話,可以跳過這個小節(jié)。腳本語言 perl 的正則表達式規(guī)范是目前廣泛采用的一個規(guī)范,Java 中的 regex 包就是一個很好的例子,另外,如 vim 這樣的應(yīng)用程序中,也采用了該規(guī)范。
元字符,是一些數(shù)學(xué)符號,在正則表達式中有特定的含義,而不僅僅表示其“字面”上的含義,比如星號(*),表示一個集合的零到多次重復(fù),而問號(?)表示零次或一次。如果你需要使用元字符的字面意義,則需要轉(zhuǎn)義。
下面是一張元字符的表:
| 元字符 | 含義 |
|---|---|
| ^ | 串的開始 |
| $ | 串的結(jié)束 |
| * | 零到多次匹配 |
| + | 一到多次匹配 |
| ? | 零或一次匹配 |
| \b | 單詞邊界 |
特殊字符,主要是指注入空格,制表符,其他進制(十進制之外的編碼方式)等,它們的特點是以轉(zhuǎn)義字符()為前導(dǎo)。如果需要引用這些特殊字符的字面意義,同樣需要轉(zhuǎn)義。
下面為轉(zhuǎn)移字符的一張表:
| 字符 | 含義 |
|---|---|
| 字符本身 | 匹配字符本身 |
| \r | 匹配回車 |
| \n | 匹配換行 |
| |t制表符 | |
| \f | 換頁 |
| \x# | 匹配十六進制數(shù) |
| \cX | 匹配控制字符 |
我們經(jīng)常會遇到要描述一個范圍的例子,比如,從 0 到 3 的數(shù)字,所有的英文字母,包含數(shù)字,英文字母以及下劃線等等,正則表達式規(guī)定了如何表示范圍:
| 標(biāo)志符 | 含義 |
|---|---|
| […] | 在集合中的任一個字符 |
| [^…] | 不在集合中的任一個字符 |
| . | 出\n之外的任一個字符 |
| \w | 所有的單字,包括字母,數(shù)字及下劃線 |
| \W | 不包括所有的單字,\w的補集 |
| \s | 所有的空白字符,包括空格,制表符 |
| \S | 所有的非空白字符 |
| \d | 所有的數(shù)字 |
| \D | 所有的非數(shù)字 |
| \b | 退格字符 |
結(jié)合元字符和范圍,我們可以定義出很強大的模式來,比如,一個簡化版的匹配 Email 的正則表達是為:
var emailval = /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/;
emailval.test("kmustlinux@hotmail.com");//true
emailval.test("john.abruzzi@pl.kunming.china");//true
emailval.test("@invalid.com");//false,不合法
[\w-]表示所有的字符,數(shù)字,下劃線及減號,[\w-]+ 表示這個集合最少重復(fù)一次,然后緊接著的這個括號表示一個分組(分組的概念參看下一節(jié)),這個分組的修飾符為星號(*),表示重復(fù)零或多次。這樣就可以匹配任意字母,數(shù)字,下劃線及中劃線的集合,且至少重復(fù)一次。
而@符號之后的部分與前半部分唯一不同的是,后邊的一個分組的修飾符為(+),表示至少重復(fù)一次,那就意味著后半部分至少會有一個點號(.),而且點號之后至少有一個字符。這個修飾主要是用來限制輸入串中必須包含域名。 最后,脫字符(^)和美元符號($)限制,以……開始,且以……結(jié)束。這樣,整個表達式的意義就很明顯了。
再來看一個例子:在 C/Java 中,變量命名的規(guī)則為:以字母或下劃線開頭,變量中可以包含數(shù)字,字母以及下劃線(有可能還會規(guī)定長度,我們在下一節(jié)討論)。這個規(guī)則描述成正則表達式即為下列的定義:
var variable = /[a-zA-Z_][a-zA-Z0-9_]*/;
print(variable.test("hello"));
print(variable.test("world"));
print(variable.test("_main_"));
print(variable.test("0871"));
將會打?。?/p>
true
true
true
false
前三個測試字符均為合法,而最后一個是數(shù)字開頭,因此為非法。應(yīng)該注意的是,test 方法只是測試目標(biāo)串中是否有表達式匹配的部分,而不一定整個串都匹配。比如上例中:
print(variable.test("0871_hello_world"));//true
print(variable.test("@main"));//true
同樣返回 true,這是因為,test 在查找整個串時,發(fā)現(xiàn)了完整匹配 variable 表達式的部分內(nèi)容,同樣也是匹配。為了避免這種情況,我們需要給 variable 做一些修改:
var variable = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
通過加推導(dǎo)(+),星推導(dǎo)(*),以及謂詞,我們可以靈活的對范圍進行重復(fù),但是我們?nèi)匀恍枰环N機制來提供諸如 4 位數(shù)字,最多 10 個字符等這樣的精確的重復(fù)方式。這就需要用到下表中的標(biāo)記:
| 標(biāo)記 | 含義 |
|---|---|
| {n} | 重復(fù)n次 |
| {n,} | 重復(fù)n或更多次 |
| {n,m} | 重復(fù)至少n次,至多m次 |
有了精確的重復(fù)方式,我們就可以來表達如身份證號碼,電話號碼這樣的表達式,而不用擔(dān)心出做,比如:
var pid = /^[\d{15}|\d{18}]$/;//身份證
var mphone = /\d{11}/;//手機號碼
var phone = /\d{3,4}-\d{7,8}/;//電話號碼
mphone.test("13893939392");//true
phone.test("010-99392333");//true
phone.test("0771-3993923");//true
在正則表達式中,括號是一個比較特殊的操作符,它可以有三中作用,這三種都是比較常見的:
第一種情況,括號用來將子表達式標(biāo)記起來,以區(qū)別于其他表達式,比如很多的命令行程序都提供幫助命令,鍵入 h 和鍵入 help 的意義是一樣的,那么就會有這樣的表達式:
h(elp)?//字符h之后的elp可有可無
這里的括號僅僅為了將elp自表達式與整個表達是隔離(因為h是必選的)。
第二種情況,括號用來分組,當(dāng)正則表達式執(zhí)行完成之后,與之匹配的文本將會按照規(guī)則填入各個分組,比如,某個數(shù)據(jù)庫的主鍵是這樣的格式:四個字符表示省份,然后是四個數(shù)字表示區(qū)號,然后是兩位字符表示區(qū)縣,如 yunn0871cg 表示云南省昆明市呈貢縣(當(dāng)然,看起來的確很怪,只是舉個例子),我們關(guān)心的是區(qū)號和區(qū)縣的兩位字符代碼,怎么分離出來呢?
var pattern = /\w{4}(\d{4})(\w{2})/;
var result = pattern.exec("yunn0871cg");
print("city code = "+result[1]+", county code = "+result[2]);
result = pattern.exec("shax0917cc");
print("city code = "+result[1]+", county code = "+result[2]);
正則表達式的 exec 方法會返回一個數(shù)組(如果匹配成功的話),數(shù)組的第一個元素(下標(biāo)為 0)表示整個串,第一個元素為第一個分組,第二個元素為第二個分組,以此類推。因此上例的執(zhí)行結(jié)果即為:
city code = 0871, county code = cg
city code = 0917, county code = cc
第三種情況,括號用來對引用起輔助作用,即在同一個表達式中,后邊的式子可以引用前邊匹配的文本,我們來看一個非常常見的例子:我們在設(shè)計一個新的語言,這個語言中有字符串類型的數(shù)據(jù),與其他的程序設(shè)計語言并無二致,比如:
var str = "hello, world";
var str = 'fair enough';
均為合法字符,我們可能會設(shè)計出這樣的表達式來匹配該聲明:
var pattern = /['"][^'"]*['"]/;
看來沒有什么問題,但是如果用戶輸入:
var str = 'hello, world";
var str = "hello, world';
我們的正則表達式還是可以匹配,注意這兩個字符串兩側(cè)的引號不匹配!我們需要的是,前邊是單引號,則后邊同樣是單引號,反之亦然。因此,我們需要知道前邊匹配的到底是“單”還是“雙”。這里就需要用到引用,JavaScript 中的引用使用斜杠加數(shù)字來表示,如\1表示第一個分組(括號中的規(guī)則匹配的文本),\2表示第二個分組,以此類推。因此我們就設(shè)計出了這樣的表達式:
var pattern = /(['"])[^'"]*\1/;
在我們新設(shè)計的這個語言中,為了某種原因,在單引號中我們不允許出現(xiàn)雙引號,同樣,在雙引號中也不允許出現(xiàn)單引號,我們可以稍作修改即可完成:
var pattern = /(['"])[^\1]*\1/;
這樣,我們的語言中對于字符串的處理就完善了。
創(chuàng)建一個正則表達式有兩種方式,一種是借助 RegExp 對象來創(chuàng)建,另一種方式是使用正則表達式字面量來創(chuàng)建。在 JavaScript 內(nèi)部的其他對象中,也有對正則表達式的支持,比如 String 對象的 replace,match 等。我們可以分別來看:
使用字面量:
var regex = /pattern/;
使用 RegExp 對象:
var regex = new RegExp("pattern", switchs);
而正則表達式的一般形式描述為:
var regex = /pattern/[switchs];
這里的開關(guān)(switchs)有以下三種:
| 修飾符 | 描述 |
|---|---|
| i | 忽略大小寫開關(guān) |
| g | 全局搜索開關(guān) |
| m | 多行搜索開關(guān)(重定義^與$的意義) |
比如,/java/i 就可以匹配 java/Java/JAVA,而 /java/ 則不可。而 g 開關(guān)用來匹配整個串中所有出現(xiàn)的子模式,如 /java/g 匹配”javascript&java”中的兩個”java”。而 m 開關(guān)定義是否多行搜索,比如:
var pattern = /^javascript/;
print(pattern.test("java\njavascript"));//false
pattern = /^javascript/m;
print(pattern.test("java\njavascript"));//true
RegExp 對象的方法:
| 方法名 | 描述 |
|---|---|
| test() | 測試模式是否匹配 |
| exec() | 對串進行匹配 |
| compile() | 編譯正則表達式 |
RegExp 對象的 test 方法用于檢測字符串中是否具有匹配的模式,而不關(guān)心匹配的結(jié)果,通常用于測試,如上邊提到的例子:
var variable = /[a-zA-Z_][a-zA-Z0-9_]*/;
print(variable.test("hello"));//true
print(variable.test("world"));//true
print(variable.test("_main_"));//true
print(variable.test("0871"));//false
而 exec 則通過匹配,返回需要分組的信息,在分組及引用小節(jié)中我們已經(jīng)做過討論,而 compile 方法用來改變表達式的模式,這個過程與重新聲明一個正則表達式對象的作用相同,在此不作深入討論。
除了正則表達式對象及字面量外,String 對象中也有多個方法支持正則表達式操作,我們來通過例子討論這些方法:
| 方法 | 作用 |
|---|---|
| match | 匹配正則表達式,返回匹配數(shù)組 |
| replace | 替換 |
| split | 分割 |
| search | 查找,返回首次發(fā)現(xiàn)的位置 |
var str = "life is very much like a mirror.";
var result = str.match(/is|a/g);
print(result);//返回[“is”, “a”]
這個例子通過 String 的 match 來匹配 str 對象,得到返回值為[“is”, “a”]的一個數(shù)組。
var str = "<span>Welcome, John</span>";
var result = str.replace(/span/g, "div");
print(str);
print(result);
得到結(jié)果:
<span>Welcome, John</span>
<div>Welcome, John</div>
也就是說,replace 方法不會影響原始字符串,而將新的串作為返回值。如果我們在替換過程中,需要對匹配的組進行引用(正如之前的\1,\2方式那樣),需要怎么做呢?還是上邊這個例子,我們要在替換的過程中,將 Welcome 和 John 兩個單詞調(diào)換順序,編程 John, Welcome:
var result = str.replace(/(\w+),\s(\w+)/g, "$2, $1");
print(result);
可以得到這樣的結(jié)果:
<span>John, Welcome</span>
因此,我們可以通過$n來對第 n 個分組進行引用。
var str = "john : tomorrow :remove:file";
var result = str.split(/\s*:\s*/);
print(str);
print(result);
得到結(jié)果:
john : tomorrow :remove:file
john,tomorrow,remove,file
注意此處 split 方法的返回值 result 是一個數(shù)組。其中包含了 4 個元素。
var str = "Tomorrow is another day";
var index = str.search(/another/);
print(index);//12
search 方法會返回查找到的文本在模式中的位置,如果查找不到,返回-1。
本小節(jié)提供一個實例,用以展示在實際應(yīng)用中正則表達式的用途,當(dāng)然,一個例子不可能涵蓋所有的內(nèi)容,只是一個最常見的場景。
考慮這樣一種情況,我們在UI上為用戶提供一種快速搜索的能力,使得隨著用戶的鍵入,結(jié)果集不斷的減少,直到用戶找到自己需要的關(guān)鍵字對應(yīng)的欄目。在這個過程中,用戶可以選擇是否區(qū)分大小寫,是否全詞匹配,以及高亮一個記錄中的所有匹配。
顯然,正則表達式可以滿足這個需求,我們在這個例子中忽略掉諸如高亮,刷新結(jié)果集等部分,來看看正則表達式在實際中的應(yīng)用:
http://wiki.jikexueyuan.com/project/javascript-core/images/js8.png" alt="" />
圖1 在列表中使用 JSFilter(結(jié)果集隨用戶輸入而變化)
來看一個代碼片段:
this.content.each(function(){
var text = $(this).text();
var pattern = new RegExp(keyword, reopts);
if(pattern.test(text)){
var item = text.replace(pattern, function(t){
return "<span class=\""+filterOptions.highlight+"\">"+t+"</span>";
});
$(this).html(item).show();
}else{//clear previous search result
$(this).find("span."+filterOptions.highlight).each(function(){
$(this).replaceWith($(this).text());
});
}
});
其中,content 是結(jié)果集,是一個集合,其中的每一個項目都可能包含用戶輸入的關(guān)鍵字,keyword 是用戶輸入的關(guān)鍵字序列,而 reopts 為正則表達式的選項,可能為(i,g,m),each 是 jQuery 中的遍歷集合的方式,非常方便。程序的流程是這樣的:
遍歷完所有的結(jié)果集,生成了一個新的,高亮標(biāo)注的結(jié)果集,然后將其呈現(xiàn)給用戶。而且可以很好的適應(yīng)用戶的需求,比如是否忽略大小寫檢查,是否高亮所有,是否全詞匹配,如果自行編寫程序進行分析,則需要耗費極大的時間和精力。
http://wiki.jikexueyuan.com/project/javascript-core/images/js9.png" alt="" />
圖2 在表格中使用 JSFilter(不減少結(jié)果集)