指尖上的正則表達式–入門篇
1) 歷史和起源
正則表達式的“鼻祖”或許可一直追溯到科學家對人類神經系統工作原理的早期研究。美國新澤西州的Warren McCulloch和出生在美國底特律的Walter Pitts這兩位神經生理方面的科學家,研究出了一種用數學方式來描述神經網絡的新方法,他們創新地將神經系統中的神經元描述成了小而簡單的自動控制元,從而作出了一項偉大的工作革新。
在1956年,出生在被馬克·吐溫(Mark Twain)稱為“美國最美麗的城市之一的”哈特福德市的一位名叫Stephen Kleene的數學科學家,他在Warren McCulloch和Walter Pitts早期工作的基礎之上,發表了一篇題目是《神經網事件的表示法》的論文,利用稱之為正則集合的數學符號來描述此模型,引入了正則表達式的概念。正則表達式被作為用來描述其稱之為“正則集的代數”的一種表達式,因而采用了“正則表達式”這個術語。
之后一段時間,人們發現可以將這一工作成果應用于其他方面。Ken Thompson就把這一成果應用于計算搜索算法的一些早期研究,Ken Thompson是Unix的主要發明人,也就是大名鼎鼎的Unix之父。Unix之父將此符號系統引入編輯器QED,然后是Unix上的編輯器ed,并最終引入grep。Jeffrey Friedl在其著作“Mastering Regular Expressions (2nd edition)”中對此作了進一步闡述講解,如果你希望更多了解正則表達式理論和歷史,推薦你看看這本書。
自此以后,正則表達式被廣泛地應用到各種UNIX或類似于UNIX的工具中,如大家熟知的Perl。Perl的正則表達式源自于Henry Spencer編寫的regex,之后已演化成了pcre(Perl兼容正則表達式Perl Compatible Regular Expressions),pcre是一個由Philip Hazel開發的、為很多現代工具所使用的庫。正則表達式的第一個實用應用程序即為Unix中的qed編輯器。
然后,正則表達式在各種計算機語言或各種應用領域得到了廣大的應用和發展,演變成為目前計算機技術森林中的一只形神美麗且聲音動聽的百靈鳥。
以上是關于正則表達式的起源和發展的歷史描述,到目前正則表達式在基于文本的編輯器和搜索工具中依然占據這一個非常重要的地位。
在最近的六十年中,正則表達式逐漸從模糊而深奧的數學概念,發展成為在計算機各類工具和軟件包應用中的主要功能。不僅僅眾多UNIX工具支持正則表達式,近二十年來,在 Windows 的陣營下,正則表達式的思想和應用在大部分 Windows 開發者工具包中得到支持和嵌入應用!從正則式在 Microsoft Visual Basic 6 或 Microsoft VBScript 到 .NET Framework 中的探索和發展,Windows系列產品對正則表達式的支持發展到無與倫比的高度,目前幾乎所有 Microsoft 開發者和所有.NET語言都可以使用正則表達式。如果你是一位接觸計算機語言的工作者,那么你會在主流操作系統(*nix[Linux, Unix等]、Windows、HP、BeOS等)、目前主流的開發語言(PHP、C#、Java、C++、VB、Javascript、Ruby以及Python等)、數以億萬計的各種應用軟件中,都可以看到正則表達式優美的舞姿。(摘自《百度百科–正則表達式》)
2) 正則表達式的定義
正則表達式是對字符串操作的一種邏輯公式,就是用事先定義好的一些特定字符、及這些特定字符的組合,組成一個“規則字符串”,這個“規則字符串”用來表達對字符串的一種過濾邏輯。
給定一個正則表達式和另一個字符串,我們可以達到如下的目的:1. 給定的字符串是否符合正則表達式的過濾邏輯(稱作“匹配”);2. 可以通過正則表達式,從字符串中獲取我們想要的特定部分。
正則表達式的特點是:1. 靈活性、邏輯性和功能性非常的強;2. 可以迅速地用極簡單的方式達到字符串的復雜控制。3. 對于剛接觸的人來說,比較晦澀難懂。
由于正則表達式主要應用對象是文本,因此它在各種文本編輯器場合都有應用,小到著名編輯器EditPlus,大到Microsoft Word、Visual Studio等大型編輯器,都可以使用正則表達式來處理文本內容。
3) 正則表達式的語法
正則表達式由一些普通字符和一些元字符組成,普通字符就是我們平時常見的字符串、數字之類的,當然也包括一些常見的符號,等等。而元字符則可以理解為正則表達式引擎的保留字符,就像很多計算機語言中的保留字符一樣,他們在正則引擎中有特殊的意義,下面將介紹JavaScript中和正則相關的元字符以及API。(不同的計算機語言,對正則引擎的實現不是完全一致的,所以,有些元字符和組合方式在JavaScript中不存在)
我們將按照下面的分類將元字符一一列出:
3. 1 字面量字符( Literal Characters )
字符 | 描述 | 描述 |
---|---|---|
f | 換頁符 | (u000C) |
n | 換行符 | (u000A) |
r | 回車 | (u000D) |
o | NUL字符 | (u0000) |
t | 制表符 | (u0009) |
v | 垂直制表符 | (u000B) |
xnn | 由十六進制數nn指定的拉丁字符 | x0A等價于n |
uxxxx | 由十六進行xxxx指定的Unicode字符 | u0009等價與t |
cX | 控制字符(X的值必須是A-Z或a-z) | cJ等價于換行符n |
3. 2 字符類( Character Classes )
字符 | 描述 | 示例 |
---|---|---|
[xyz] | 匹配位于括號內的任意字符 | [abc]匹配’plain’中的a |
[^xyz] | 匹配不在括號之中的任意字符 | [^abc]匹配’plain’中的p |
w | 等價于[a-zA-Z0-9_] | w匹配’sina’中的s |
W | 等價于[^a-zA-Z0-9_] | w不能匹配’sina’ |
s | 任何Unicode空白符 | [ fnrtv] |
S | 任何非空白字符 | [^ fnrtv] |
d | 等價于[0-9] | d匹配’sina123′中的1 |
D | 等價于[^0-9] | D不能匹配’sina1′中的1 |
[b] | 退格直接量(特例) | |
除換行符和其它Unicode換行符之外的任意字符,[sS]匹配任意字符 a.c 匹配 “abc”, “a1c”, and “a-c”. |
3. 3 重復( Repetition )
字符 | 描述 | 示例 |
{n,m} | 匹配至少n次,但不超過m次,n和m必須是非負整數,且n<=m
? 等價于 {0,1} |
[abc]匹配’plain’中的a |
{n,} | 匹配至少n次 | o{2,} 不匹配’Bob’中的’o',但匹配’food’中的 o. |
{n} | 恰好匹配n次 | o{2} 不匹配’Bob’中的’o',但匹配’food’中的o. |
? | 匹配0次或1次,等價于{0,1} | zo? 匹配 "z" and "zo", 但不匹配"zoo". |
+ | 匹配1次或多次,等價于{1,} | zo+ 匹配 "zo" and "zoo", 但不匹配 "z". |
* | 匹配0次或多次,等價于{0,} | zo* 匹配 "z" 和 "zoo". |
3. 4 非貪婪的重復(non-greedy)
字符 | 描述 | 示例 | |
*?
+? ?? {n}? {n,}? {n,m}? |
在重復字符后加上問號,匹配模式就是非貪婪的匹配. 這種模式會盡可能少的對目標字符串進行匹配 |
o+? 匹配"oooo"中的一個"o", o+ 則匹配所有的 "o".
/a+?b/.exec(‘aaab’) |
3. 5 選擇、分組和引用
字符 | 描述 | 示例 |
---|---|---|
(選擇)匹配該符號左邊或右邊的子表達式 |
(zf)ood匹配zood或food | |
(pattern) |
(分組)將幾個項目組合成一個單元,這個單元可由*,+,?和等符號使用,而且還可記住和這個組合匹配的字符以供后面的引用使用 |
(AB) [1-9] 匹配 "A5", 字母A被保存,可通過n或RegExp的$1-$9引用該值 |
(?:pattern) | 只組合,不記憶 |
ai(?:rR) 等價于airaiR |
n | 和第n個分組第一次匹配的字符串匹配 |
3. 6 指定匹配位置
字符 | 描述 | 示例 |
^ |
匹配字符串的開頭,在多行檢索中,匹配一行的開頭 |
^food&匹配以f開頭,d結尾的food,而不會匹配’eatfood222′中的’food’ |
$ |
匹配字符串的結尾,在多行檢索中,匹配一行的結尾 |
同上 |
b | 匹配一個單詞的邊界 |
erb 匹配’never’中的’er’,但不匹配’verb’中的’er’. |
B | 匹配非單詞邊界 | |
(?=pattern) | 正向肯定預查,在任何匹配pattern的字符串開始處匹配查找字符串。這是一個非獲取匹配,也就是說,該匹配不需要獲取供以后使用 | “Windows(?=9598NT2000)”能匹配“Windows2000”中的“Windows”,但不能匹配“Windows3.1”中的“Windows”。預查不消耗字符,也就是說,在一個匹配發生后,在最后一次匹配之后立即開始下一次匹配的搜索,而不是從包含預查的字符之后開始 |
(?!pattern) | 正向否定預查,在任何不匹配pattern的字符串開始處匹配查找字符串。這是一個非獲取匹配,也就是說,該匹配不需要獲取供以后使用 | “Windows(?!9598NT2000)”能匹配“Windows3.1”中的“Windows”,但不能匹配“Windows2000”中的“Windows”。預查不消耗字符,也就是說,在一個匹配發生后,在最后一次匹配之后立即開始下一次匹配的搜索,而不是從包含預查的字符之后開始 |
3. 7 標志
字符 | 描述 |
i | 忽略大小寫 |
g | 執行全局匹配,即找到所有的匹配,而不是在找到第一個匹配后停止 |
m |
多行模式,^匹配一行的開頭和字符串的開頭,$匹配一行的結尾或字符串的結尾 |
4) JavaScript中的正則API
上一部分,我們了解了正則表達式的語法,那么,如何在JavaScript中構造一個正則表達式的對象呢?有以下兩種方法:
1.字面量表示法,例如:var reg = /d+/i; 就表示一個正則表達式的實例,這個寫法需要注意的地方是:正則表達式的主體部分,也就是示例中的 d+ ,必須位于“ / / ”之間,而標志位則需要跟在結束 “/” 的后面。
2.實例化RegExp對象,例如: var reg = new RegExp( “d+”, “igm” ); 第一個參數是表達式的主體,而第二個參數則是標志位,此參數可選。
上面兩種方法可以得到正則表達式的實例,通常,我們都使用第一種方式,即字面量的方式,這種方式比較直觀,第二種方式通常用來組合表達式,比如,通過用戶輸入的某個值,來構造表達式,例如:var input = ‘this is a test’; var reg = new RegExp( “dw” + input,); 通過這種方式,我們可以組合成新的表達式,來達到我們的目的。在使用這種方式時,需要注意對特殊字符的轉義和過濾,以免遭受惡意攻擊。
JavaScript中,除了正則表達式對象外,字符串也和正則表達式息息相關。可以說,RegExp和String是密不可分的,缺一不可。所以,和正則相關的API就是RegExp對象和String對象的一些API。
首先是RegExp對象,該對象的實例中,有三個方法和正則表達式相關,分別是test( ),exec( )和compile( )。而String對象中和正則相關的方法則包括:match( ), replace( ), search( ), split( ), 接下來會對這些方法做詳細介紹。
4. 1 exec( )
exec( ) 方法檢索字符串中的指定值。返回值是被找到的值。如果沒有發現匹配,則返回 null。
例子1:
var patt1 = new RegExp("e"); // var patt1 = /e/; document.write( patt1.exec("The best things in life are free") ); //output : e
從上面的例子中可以看出,exec( )方法接收一個參數,類型為字符串,可以簡單的理解為:我是一個正則表達式實例,請給我一個字符串來測測吧,于是,我們滿足了它的愿望,把目標字符串丟給它。
如果匹配成功,該方法的返回結果是一個數組[array],該數組的第一個元素則是匹配的字符串,后面的元素則是表達式分組所捕獲到的值,分組為1,則是該數組中的第一個元素,依次類推。若沒有分組,該數組只包含一個元素。當然,該數組還有另外3個屬性,input,index和lastIndex。若沒有匹配成功,則返回null。
input屬性表示目標字符串。
index屬性表示匹配到的位置。
lastIndex屬性表示上一次匹配后的位置。
另外,需要注意的是,若表達式實例中包含了全局(g)標志位,則會從 lastIndex 所標記的位置開始查找,而不會從字符串的開始位置進行。所以,我們可以多次調用exec( )方法,比如:
function RegExpTest() { var src = "The quick brown fox jumps over the lazy dog."; // Create regular expression pattern with a global flag. var re = /w+/g; // Get the next word, starting at the position of lastindex. var arr; while ((arr = re.exec(src)) != null) { // New line: document.write ("<br />"); document.write (arr.index + "-" + arr.lastIndex + " "); document.write (arr[0]); } }
詳細描述請參考:
http://msdn.microsoft.com/en-us/library/z908hy33(v=vs.94).aspx
4. 2 test( )
test( ) 方法比 exec( ) 方法簡單,該方法只會返回一個Boolean值,如果匹配成功,則返回true,否則,返回false。可參考下面的示例:
function TestDemo(re, teststring) { // Test string for existence of regular expression. var found = re.test(teststring) // Format the output. var s = ""; s += "'" + teststring + "'" if (found) s += " contains "; else s += " does not contain "; s += "'" + re.source + "'" return(s); }
詳細描述請參考:
http://msdn.microsoft.com/en-us/library/a55e5s6b(v=vs.94).aspx
4. 3 match( )
需要注意的是,該方法的參數,也就是正則表達式添加了全局( g )標志位時的變化。如果沒有添加全局標志位,返回結果和exec( )方法的返回值一樣,但不包含所有的屬性。
詳細描述請參考:
http://msdn.microsoft.com/en-us/library/7df7sf4x(v=vs.94).aspx
4. 4 replace( )
該方法是字符串中非常強有力的方法,它接收兩個參數,第一個參數為表達式,也可以直接傳入字符串,該方法在內部會自動將字符串轉換為正則表達式;第二個參數是想要替換成的字符串,該參數可以為一個函數,但是該函數的返回值必須為字符串。具體用法請參考
http://msdn.microsoft.com/en-us/library/t0kbytzc(v=vs.94).aspx
4. 5 split( )
該方法可以按照傳遞的參數對字符串進行分割,會返回一個數組,里面包含了分割后的元素。一般用法是直接傳入字符串進行分割,當然,也可以傳入正則表達式,按表達式分割,詳細信息請參考:
http://msdn.microsoft.com/en-us/library/t5az126b(v=vs.94).aspx
4. 6其它
compile( ),search( )等方法,在日常使用中較少使用,大致了解一下即可。
5) 實例解析
大家可以參考【正則表達式經典實例】,請參考附件。
小結
以上主要講解了正則表達式的基本語法和常用API,對于如何構造復雜的表達式和表達式的匹配原理,本文并未涉及。說實話,正則表達式本身確實晦澀難懂,比較拗口,大家需在平時,多多溫習這些基礎的語法和API。
另外,可以用一些工具來輔助我們寫出嚴謹的正則表達式,比如:RegexBuddy或者一些在線的應用。當然,還有瀏覽器的控制臺,可以在里面很方便的進行測試。
在日常的開發中,千萬不要在正則上鉆牛角尖,因為我們可以利用其它API,達到同樣的目的。而且,太過復雜的表達式,效率可能較低,而且,對于以后的維護人員來說,也是非常不方便維護的。
但千萬不要淺嘗輒止,應該繼續深挖正則中的思想,正是這些基礎的思想,才能幫我我們在編程上更上層樓。
參考信息
http://msdn.microsoft.com/en-us/library/z908hy33(v=vs.94).aspx
exec()
http://msdn.microsoft.com/en-us/library/a55e5s6b(v=vs.94).aspx
test()
http://msdn.microsoft.com/en-us/library/7df7sf4x(v=vs.94).aspx
match()
http://msdn.microsoft.com/en-us/library/t0kbytzc(v=vs.94).aspx
replace()