by Jim Hollenhorst 譯 寒帶魚
你是否曾經想過正則表達式是什么,怎樣能夠快速得到對它的一個基本的認識?我的目的就是在30分鐘內帶你入門并且對正則表達式有一個基本的理解。事實是正則表達式并沒有它看起來那么復雜。學習它最好的辦法就是開始寫正則表達式并且不斷實踐。在最初的30分鐘之后,你就應該知道一些基本的結構并且有能力在你的程序或者web頁面中設計和使用正則表達式了。對那些想要深入研究的人,現在已經有很多非常好的可用資源來讓你更深入的學習。
到底什么是正則表達式?
我相信你對模式匹配的“計算機通配符”字符應該比較熟悉了。例如,如果你想要在一個Windows文件夾中找到所有Mircosoft Word文件,你要搜索“*.doc”,因為你知道星號會被解釋為一個通配符,它匹配所有序列的字符串。正則表達式就是這種功能的一個更加細節的擴展。
在寫處理文本的程序或者web頁面時,定位匹配復雜模式的字符串是很常見的。正則表達式就是用來描述這類模式的。這樣,一個正則表達式就是一個模式的縮減代碼。例如,模式“/w+”是表達“匹配任何包含字母數字字符的非空字符串”的精確方法。.NET框架提供了一個功能強大類庫,它使得在你的應用程序中包含正則表達式更加容易。使用這個庫,你可以輕易地搜索和替換文本,解碼復雜的標題,解析語言,或者驗證文本。
學習正則表達式的神秘的語法的一個好辦法是用例子作為開始學習的對象,然后實踐創建自己的正則表達式。
讓我們開始吧!
一些簡單的例子
搜索Elvis
假設你要花費你所有的空余時間來掃描文檔來尋找Elvis仍然活著的證據。你可以使用下面的正則表達式來搜索:
1. elvis -- Find elvis
這是搜索精確字符序列的一個完全合法的正則表達式。在.NET中,你可以輕松的設置選項來忽略字符的各種情況,所以這個表達式將會匹配“Elivs”,“ELVIS”,或者“eLvIs”。不幸的是,它也將匹配單詞“pelvis”的后五個字母。我們可以改進這個表達式如下:
2. /belvis/b -- Find elvis as a whole word
現在事情變得更加有趣了。“/b”是一個特殊代碼,它表示“匹配任何單詞的開頭或結尾的位置”。這個表達式將只匹配完整的拼寫為“elvis”的單詞,無論是小寫的還是大寫的情況。
假設你想要找到所有這樣的行,在其中單詞“elvis”后面都跟著單詞“alive”。句點或者點“.”是一個特殊代碼匹配除了換行符之外的任何字符。星號“*”表示重復前面的部分有必要的次數以保證能夠有一個匹配。這樣,“.*”表示“匹配除了換行符之外的任意數目的字符”。現在建立一個表示“搜索在同一行內后面跟著單詞‘alive’的單詞‘elvis’”的表達式就是一件簡單的事了。
3. /belvis/b.*/balive/b -- Find text with "elvis" followed by "alive"
僅僅使用幾個特殊字符我們就開始創建功能強大的正則表達式了,而且它們已經開始變得難以被我們人類理解了。
讓我們看看另一個例子。
確定電話號碼的合法性
假設你的web頁面收集顧客的7位電話號碼,而且你希望驗證輸入的電話號碼是正確的格式,“xxx-xxxx”,這里每個“x”是一個數字。下面的表達式將搜索整個文本尋找這樣的一個字符串:
4. /b/d/d/d-/d/d/d/d -- Find seven-digit phone number
每個“/d”表示“匹配任何單個數字”。“-”沒有特殊的意義并且按照字面解釋,匹配一個連字符。要避免繁瑣的重復,我們可以使用一個含有相同含義的速記符:
5. /b/d{3}-/d{4} -- Find seven-digit phone number a better way
“/d”后面的“{3}”表示“重復前面的字符三次”。
.NET正則表達式的基礎
讓我們探索一下.NET中正則表達式的基礎
特殊字符
你應該知道幾個有特殊意義的字符。你已經見過了“/b”,“.”,“*”,和“/d”。要匹配任何空白字符,像空格,制表符和換行符,使用“/s”。相似地,“/w”匹配任何字母數字字符。
讓我們嘗試更多的例子:
6. /ba/w*/b -- Find words that start with the letter a
這個搜索一個單詞的開頭(/b),然后是一個字母“a”,接著是任意次數重復的字母數字字符(/w*),最后是一個單詞的結尾(/b)。
7. /d+ -- Find repeated strings of digits
這里,“+”與“*”是相似的,除了它需要至少一次重復。
8. /b/w{6}/b -- Find six letter words
在ExPResso中測試這幾個表達式,然后實踐創建你自己的表達式。下面是一個說明有特殊含義的字符的表格:
| . | 匹配除換行符外的任何字符 |
| /w | 匹配任何字母數字字符 |
| /s | 匹配任何空白字符 |
| /d | 匹配任何數字 |
| /b | 匹配一個單詞的開始或結尾 |
| ^ | 匹配字符串的開始 |
| $ | 匹配字字符串的結尾 |
開始階段
特殊字符“^”和“$”被用來搜索那些必須以一些文本開頭和(或)以一些文本結尾的文本。特別是在驗證輸入時特別有用,在這些驗證中,輸入的整個文本必須要匹配一個模式。例如,要驗證一個7位電話號碼,你可能要用:
9. ^/d{3}-/d{4}$ -- Validate a seven-digit phone number
這是和第5個例子一樣的,但是強迫它符合整個文本字符串,匹配文本的頭尾之外沒有其他字符。通過在.NET中設置“Multiline”選項,“^”和“$”改變他們的意義為匹配一行文本的起點和結束,而不是整個正文字符串。Expresso的例子使用這個選項。
換碼字符
當你想要匹配這些特殊字符中的一個時會產生一個錯誤,像“^”或者“$”。使用反斜線符號來去掉它們的特殊意義。這樣,“/^”,“/.”,和“//”,分別匹配文本字符“^”,“.”,和“/”。
重復
你已經見過了“{3}”和“*”可以指定一個單獨字符的重復次數。稍后,你會看到相同的語法怎樣用來重復整個子表達式。此外還有其他幾種方法來指定一個重復,如下表所示:
| * | 重復任意次數 |
| + | 重復一次或多次 |
| ? | 重復一次或多次 |
| {n} | 重復n次 |
| {n,m} | 重復最少n次,最多m次 |
| {n,} | 重復最少n次 |
讓我們試試幾個例子:
10. /b/w{5,6}/b -- Find all five and six letter words
11. /b/d{3}/s/d{3}-/d{4} -- Find ten digit phone numbers
12. /d{3}-/d{2}-/d{4} -- Social security number
13. ^/w* -- The first word in the line or in the text
在設置和不設置“Multiline”選項的時試試最后一個例子,它改變了“^”的含義。
字符集合
搜索字母數字字符,數字,和空白字符是容易的,但如果你需要搜索一個字符集合中的任意字符時怎么辦?這可以通過在方括號中列出想要的字符來輕松的解決。這樣,“[aeiou]”就能匹配任意韻母,而“[.?!]”就匹配句子末尾的標點。在這個例子中,注意“.”和“?”在方括號中都失去了他們的特殊意義而被解釋為文本含義。我們也可以指定一個范圍的字符,所以“[a-z0-9]”表示“匹配任何小寫字母或者任何數字”。
讓我們試試一個搜索電話號碼的更加復雜的表達式:
14. /(?/d{3}[) ]/s?/d{3}[- ]/d{4} A ten digit phone number
這個表達式將會搜索幾種格式的電話號碼,像“(800)325-3535”或者“650 555 1212”。“/(?”搜索0個或1個左圓括號,“[)]”搜索一個右圓括號或者一個空格。“/s?”搜索0個或一個空白字符。不幸的是,它也會找到像“650)555-1212”這樣括號沒有去掉的情況。在下面,你會看到怎樣用可選項解決這個問題。
否定
有些時候我們需要搜索一個字符,它不是一個很容易定義的字符集合的成員。下面的表格說明了這種字符怎樣指定:
| /W | 匹配任何非字母數字字符 |
| /S | 匹配任何非空白字符 |
| /D | 匹配任何非數字字符 |
| /B | 匹配非單詞開始或結束的位置 |
| [^x] | 匹配任何非x字符 |
| [^aeiou] | 匹配任何不在aeiou中的字符 |
15. /S+ -- All strings that do not contain whitespace characters
后面,我們會看到怎樣使用“lookahead”和“lookbehind”來搜索缺少更加復雜的模式的情況。
可選項
要從幾個可選項中選擇,允許符合任何一個的匹配,使用豎杠“|”來分隔可選項。例如,郵政編碼有兩種,一個是5位的,另一個是9位的加一個連字符。我們可以使用下面的表達式找到任何一種:
16. /b/d{5}-/d{4}/b|/b/d{5}/b -- Five and nine digit Zip Codes
當使用可選項時,順序是很重要的因為匹配算法將試圖先匹配最左面的選擇。如果這個例子中的順序顛倒過來,表達式將只能找到5位的郵政編碼,而不會找到9位的。我們可以使用可選項來改進十位電話號碼的表達式,允許包含區碼無論是通過空白字符還是連字符劃分的:
17. (/(/d{3}/)|/d{3})/s?/d{3}[- ]/d{4} -- Ten digit phone numbers, a better way
分組
圓括號可以用來劃分一個子表達式來允許重復或者其他特殊的處理,例如:
18. (/d{1,3}/.){3}/d{1,3} -- A simple IP address finder
表達式的第一部分搜索后面跟著一個“/.”的一個一位到三位的數字。這被放在圓括號中并且通過使用修飾符“{3}”被重復三次,后面跟著與之前一樣的表達式而不帶后綴部分。
不幸的是,這個例子允許IP地址中被分隔的部分是任意的一位,兩位,或三位數字,盡管一個合法的IP地址不能有大于255的數字。要是能夠算術比較一個獲取的數字N使N<256就好了,但是只用正則表達式是不能夠辦到的。下一個例子使用模式匹配測試了基于第一位數字的多種可選項來保證限制數字的取值范圍。這表明一個表達式會變得很笨重,盡管搜索模式的描述是簡單的。
19. ((2[0-4]/d|25[0-5]|[01]?/d/d?)/.){3}(2[0-4]/d|25[0-5]|[01]?/d/d?) -- IP finder
一個“回引”用來搜索前面被一個分組捕獲的已匹配文本的再現。例如,“/1”表示“匹配分組1中已捕獲到的文本”。下面是一個例子:
20. /b(/w+)/b/s*/1/b -- Find repeated words
它的運行過程是先捕獲一個分組1中“(/w+)”表示的至少包含一個字母數字字符的字符串,但僅當它是一個單詞的開始或結束字符時才行。然后它搜索任意數量的空白字符“/s*”后跟以被捕獲的文本“/1”結尾的單詞。
在上面的例子中,想要替換分組“(/w+)”這種寫法,我們可以把它寫成“(?<Word>/w+)”來給這個分組命名為“Word”。一個對這個分組的回引可以寫成“/k<Word>”。試試下面的例子:
21. /b(?<Word>/w+)/b/s*/k<Word>/b -- Capture repeated word in a named group
通過使用圓括號,有很多可用的特殊用途的語法元素。一些最常用的歸納如下面這張表格:
| 捕獲 | |
| (exp) | 匹配exp并且在一個自動計數的分組中捕獲它 |
| (?<name>exp) | 匹配exp并且在一個命名的分組中捕獲它 |
| (?:exp) | 匹配exp并且不捕獲它 |
| 察看 | |
| (?=exp) | 匹配任何后綴exp之前的位置 |
| (?<=exp) | 匹配任何前綴exp之后的位置 |
| (?!exp) | 匹配任何未找到的后綴exp之后的位置 |
| (?<!exp) | 匹配任何未找到的前綴exp之前的位置 |
| 評論 | |
| (?#comment) | 評論 |
表4 常用分組結構
前兩個我們已經說過了。第三個“(?:exp)”不會改變匹配行為,它只是不像前兩個那樣捕獲已命名的或者計數的分組。
確定察看(Positive Lookaround)
下面四個是所謂的前向或后向斷言。它們從當前的匹配向前或向后尋找需要的東西而不在匹配中包含它們。這些表達式匹配一個類似于“^”或“/b”的位置而不匹配任何文本,理解這個是很重要的。由于這個原因,他們也被稱為“零寬度斷言”。最好用例子來解釋它們:
“(?=exp)”是“零寬度確定前向斷言”。它匹配一個文本中在給定后綴之前的位置,但不在匹配中包含這個后綴:
22. /b/w+(?=ing/b) -- The beginning of words ending with "ing"
“(?<=exp)”是“零寬度確定后向斷言”。它匹配在給定前綴后面的位置,但不在匹配中包含這個前綴:
23. (?<=/bre)/w+/b -- The end of words starting with "re"
下面這個例子可以用來重復向三位數為一組的數字中插入逗號的例子:
24. (?<=/d)/d{3}/b -- Three digits at the end of a word, preceded by a digit
下面是一個同時搜索前綴和后綴的例子:
25. (?<=/s)/w+(?=/s) -- Alphanumeric strings bounded by whitespace
否定察看(Negative Lookaround)
之前,我說明了怎樣搜索一個不是特定字符或一個字符集合的成員的字符。那么如果我們想要簡單的驗證一個字符沒有出現,但是不想匹配任何東西怎么辦?例如,如果我們想要搜索其中“q”不是后跟著“u”的單詞怎么辦?我們可以嘗試:
26. /b/w*q[^u]/w*/b -- Words with "q" followed by NOT "u"
運行例子你就會看到如果“q”是一個單詞的最后一個字母就不會匹配,比如“Iraq”。這是因為“[^q]”總是匹配一個字符。如果“q”是單詞的最后一個字符,它會匹配后面跟著的空白字符,所以這個例子中表達式結束時匹配兩個完整的單詞。否定察看可以解決這個問題,因為它匹配一個位置而不消耗任何文本。與確定察看一樣,它也可以用來匹配一個任意復雜的子表達式的位置,而不僅僅是一個字符。我們現在可以做得更好:
27. /b/w*q(?!u)/w*/b -- Search for words with "q" not followed by "u"
我們使用“零寬度否定前向斷言”,“(?!exp)”,只有當后綴“exp”沒有出現時它才成功。下面是另一個例子:
28. /d{3}(?!/d) -- Three digits not followed by another digit
相似地,我們可以使用“(?<!exp)”,“零寬度否定后向斷言”,來搜索文本中的一個位置,這里前綴“exp”沒有出現:
29. (?<![a-z ])/w{7} -- Strings of 7 alphanumerics not preceded by a letter or space
這里是另一個使用后向的例子:
30. (?<=<(/w+)>).*(?=<///1>) -- Text between HTML tags
這個使用后向搜索一個HTML標記,而使用前向搜索對應的結束標記,這樣,就能獲得中間的文本而不包括兩個標記。
評論
標點的另一個用法是使用“(?#comment)”語法包含評論。一個更好的辦法是設置“Ignore Pattern Whitespace”選項,它允許空白字符插入表達式然后當使用表達式時忽略它。設置了這個選項之后,任何文本每行末尾在數字符號“#”后面的東西都被忽略。例如,我們可以格式化先前的例子如下:
31. Text between HTML tags, with comments
(?<= # Search for a prefix, but exclude it
<(/w+)> # Match a tag of alphanumerics within angle brackets
) # End the prefix
.* # Match any text
(?= # Search for a suffix, but exclude it
<///1> # Match the previously captured tag preceded by "/"
) # End the suffix
貪婪與懶惰
當一個正則表達式有一個可以接受一個重復次數范圍的量詞(像“.*”),正常的行為是匹配盡可能多的字符。考慮下面的正則表達式:
32. a.*b -- The longest string starting with a and ending with b
如果這被用來搜索字符串“aabab”,它會匹配整個字符串“aabab”。這被稱為“貪婪”匹配。有些時候,我們更喜歡“懶惰”匹配,其中一個匹配使用發現的最小數目的重復。表2中所有的量詞可以增加一個問號“?”來轉換到“懶惰”量詞。這樣,“*?”的意思就是“匹配任何數目的匹配,但是使用達到一個成功匹配的最小數目的重復”。現在讓我們試試懶惰版本的例子(32):
33. a.*?b -- The shortest string starting with a and ending with b
如果我們把這個應用到相同的字符串“aabab”,它會先匹配“aab”然后匹配“ab”。
| *? | 重復任意次數,但盡可能少 |
| +? | 匹配一次或多次,但盡可能少 |
| ?? | 重復零次或多次,但盡可能少 |
| {n,m}? | 重復最少n次,但不多于m次,但盡可能少 |
| {n,}? | 重復最少n次,但盡可能少 |
表5 懶惰量詞
我們遺漏了什么?
我已經描述了很多元素,使用它們來開始創建正則表達式;但是我還遺漏了一些東西,它們在下面的表中歸納出來。這些中的很多都在項目文件中使用額外的例子說明了。例子編號在這個表的左列中列出。
| /a | 報警字符 | |
| /b | 通常是單詞邊界,但是在一個字符集合中它表示退格鍵 | |
| /t | 制表符 | |
| 34 | /r | 回車 |
| /v | 垂直制表符 | |
| /f | 分頁符 | |
| 35 | /n | 換行符 |
| /e | ESC | |
| 36 | /nnn | ASCII碼八進制數為nnn的字符 |
| 37 | /xnn | 十六進制數為nn的字符 |
| 38 | /unnnn | Unicode碼為nnnn的字符 |
| 39 | /cN | Control N字符,例如回車(Ctrl-M)就是/cM |
| 40 | /A | 字符串的開始(像^但是不依賴于多行選項) |
| 41 | /Z | 字符串的結尾或者/n之前的字符串結尾(忽略多行) |
| /z | 字符串結尾(忽略多行) | |
| 42 | /G | 當前搜索的開始階段 |
| 43 | /p{name} | 命名為name的Unicode類中的任何字符,例如/p{IsGreek} |
| (?>exp) | 貪婪子表達式,也被稱為非回溯子表達式。它只匹配一次然后就不再參與回溯。 | |
| 44 | (?<x>-<y>exp) or (?-<y>exp) | Balancing group. This is complicated but powerful. It allows named capture groups to be manipulated on a push down/pop up stack and can be used, for example, to search for matching parentheses, which is otherwise not possible with regular expressions. See the example in the project file. |
| 45 | (?im-nsx:exp) | 正則表達式選項為子表達式exp |
| 46 | (?im-nsx) | Change the regular expression options for the rest of the enclosing group |
| (?(exp)yes|no) | The subexpression exp is treated as a zero-width positive lookahead. If it matches at this point, the subexpression yes becomes the next match, otherwise no is used. | |
| (?(exp)yes) | Same as above but with an empty no expression | |
| (?(name)yes|no) | This is the same syntax as the preceding case. If name is a valid group name, the yes expression is matched if the named group had a successful match, otherwise the no expression is matched. | |
| 47 | (?(name)yes) | Same as above but with an empty no expression |
表6 我們遺漏的東西。左端的列顯示了項目文件中說明這個結構的例子的序號
結論
我們已經給出了很多例子來說明.NET正則表達式的關鍵特性,強調使用工具(如Expresso)來測試,實踐,然后是用例子來學習。如果你想要深入的研究,網上也有很多在線資源會幫助你更深入的學習。你可以從訪問Ultrapico網站開始。如果你想讀一本相關書籍,我建議Jeffrey Friedl寫的最新版的《Mastering Regular Expressions》。
Code Project中還有很多不錯的文章,其中包含下面的教程:
·An Introduction to Regular Expressions by Uwe Keim
·Microsoft Visual C# .NET Developer's Cookbook: Chapter on Strings and Regular Expressions
注:本文例子可以從Ultrapico網站下載Expresso測試,點這里下載該程序,點這里察看原文。
新聞熱點
疑難解答