by jim hollenhorst 譯 寒帶魚
你是否曾經(jīng)想過正則表達(dá)式是什么,怎樣能夠快速得到對它的一個基本的認(rèn)識?我的目的就是在30分鐘內(nèi)帶你入門并且對正則表達(dá)式有一個基本的理解。事實是正則表達(dá)式并沒有它看起來那么復(fù)雜。學(xué)習(xí)它最好的辦法就是開始寫正則表達(dá)式并且不斷實踐。在最初的30分鐘之后,你就應(yīng)該知道一些基本的結(jié)構(gòu)并且有能力在你的程序或者web頁面中設(shè)計和使用正則表達(dá)式了。對那些想要深入研究的人,現(xiàn)在已經(jīng)有很多非常好的可用資源來讓你更深入的學(xué)習(xí)。
到底什么是正則表達(dá)式?
我相信你對模式匹配的“計算機通配符”字符應(yīng)該比較熟悉了。例如,如果你想要在一個windows文件夾中找到所有mircosoft word文件,你要搜索“*.doc”,因為你知道星號會被解釋為一個通配符,它匹配所有序列的字符串。正則表達(dá)式就是這種功能的一個更加細(xì)節(jié)的擴展。
在寫處理文本的程序或者web頁面時,定位匹配復(fù)雜模式的字符串是很常見的。正則表達(dá)式就是用來描述這類模式的。這樣,一個正則表達(dá)式就是一個模式的縮減代碼。例如,模式“/w+”是表達(dá)“匹配任何包含字母數(shù)字字符的非空字符串”的精確方法。.net框架提供了一個功能強大類庫,它使得在你的應(yīng)用程序中包含正則表達(dá)式更加容易。使用這個庫,你可以輕易地搜索和替換文本,解碼復(fù)雜的標(biāo)題,解析語言,或者驗證文本。
學(xué)習(xí)正則表達(dá)式的神秘的語法的一個好辦法是用例子作為開始學(xué)習(xí)的對象,然后實踐創(chuàng)建自己的正則表達(dá)式。
讓我們開始吧!
一些簡單的例子
搜索elvis
假設(shè)你要花費你所有的空余時間來掃描文檔來尋找elvis仍然活著的證據(jù)。你可以使用下面的正則表達(dá)式來搜索:
1. elvis -- find elvis
這是搜索精確字符序列的一個完全合法的正則表達(dá)式。在.net中,你可以輕松的設(shè)置選項來忽略字符的各種情況,所以這個表達(dá)式將會匹配“elivs”,“elvis”,或者“elvis”。不幸的是,它也將匹配單詞“pelvis”的后五個字母。我們可以改進(jìn)這個表達(dá)式如下:
2. /belvis/b -- find elvis as a whole word
現(xiàn)在事情變得更加有趣了。“/b”是一個特殊代碼,它表示“匹配任何單詞的開頭或結(jié)尾的位置”。這個表達(dá)式將只匹配完整的拼寫為“elvis”的單詞,無論是小寫的還是大寫的情況。
假設(shè)你想要找到所有這樣的行,在其中單詞“elvis”后面都跟著單詞“alive”。句點或者點“.”是一個特殊代碼匹配除了換行符之外的任何字符。星號“*”表示重復(fù)前面的部分有必要的次數(shù)以保證能夠有一個匹配。這樣,“.*”表示“匹配除了換行符之外的任意數(shù)目的字符”。現(xiàn)在建立一個表示“搜索在同一行內(nèi)后面跟著單詞‘a(chǎn)live’的單詞‘elvis’”的表達(dá)式就是一件簡單的事了。
3. /belvis/b.*/balive/b -- find text with "elvis" followed by "alive"
僅僅使用幾個特殊字符我們就開始創(chuàng)建功能強大的正則表達(dá)式了,而且它們已經(jīng)開始變得難以被我們?nèi)祟惱斫饬恕?/p>
讓我們看看另一個例子。
確定電話號碼的合法性
假設(shè)你的web頁面收集顧客的7位電話號碼,而且你希望驗證輸入的電話號碼是正確的格式,“xxx-xxxx”,這里每個“x”是一個數(shù)字。下面的表達(dá)式將搜索整個文本尋找這樣的一個字符串:
4. /b/d/d/d-/d/d/d/d -- find seven-digit phone number
每個“/d”表示“匹配任何單個數(shù)字”。“-”沒有特殊的意義并且按照字面解釋,匹配一個連字符。要避免繁瑣的重復(fù),我們可以使用一個含有相同含義的速記符:
5. /b/d{3}-/d{4} -- find seven-digit phone number a better way
“/d”后面的“{3}”表示“重復(fù)前面的字符三次”。
.net正則表達(dá)式的基礎(chǔ)
讓我們探索一下.net中正則表達(dá)式的基礎(chǔ)
特殊字符
你應(yīng)該知道幾個有特殊意義的字符。你已經(jīng)見過了“/b”,“.”,“*”,和“/d”。要匹配任何空白字符,像空格,制表符和換行符,使用“/s”。相似地,“/w”匹配任何字母數(shù)字字符。
讓我們嘗試更多的例子:
6. /ba/w*/b -- find words that start with the letter a
這個搜索一個單詞的開頭(/b),然后是一個字母“a”,接著是任意次數(shù)重復(fù)的字母數(shù)字字符(/w*),最后是一個單詞的結(jié)尾(/b)。
7. /d+ -- find repeated strings of digits
這里,“+”與“*”是相似的,除了它需要至少一次重復(fù)。
8. /b/w{6}/b -- find six letter words
在expresso中測試這幾個表達(dá)式,然后實踐創(chuàng)建你自己的表達(dá)式。下面是一個說明有特殊含義的字符的表格:
| . | 匹配除換行符外的任何字符 |
| /w | 匹配任何字母數(shù)字字符 |
| /s | 匹配任何空白字符 |
| /d | 匹配任何數(shù)字 |
| /b | 匹配一個單詞的開始或結(jié)尾 |
| ^ | 匹配字符串的開始 |
| $ | 匹配字字符串的結(jié)尾 |
表1 正則表達(dá)式的常用特殊字符
開始階段
特殊字符“^”和“$”被用來搜索那些必須以一些文本開頭和(或)以一些文本結(jié)尾的文本。特別是在驗證輸入時特別有用,在這些驗證中,輸入的整個文本必須要匹配一個模式。例如,要驗證一個7位電話號碼,你可能要用:
9. ^/d{3}-/d{4}$ -- validate a seven-digit phone number
這是和第5個例子一樣的,但是強迫它符合整個文本字符串,匹配文本的頭尾之外沒有其他字符。通過在.net中設(shè)置“multiline”選項,“^”和“$”改變他們的意義為匹配一行文本的起點和結(jié)束,而不是整個正文字符串。expresso的例子使用這個選項。
換碼字符
當(dāng)你想要匹配這些特殊字符中的一個時會產(chǎn)生一個錯誤,像“^”或者“$”。使用反斜線符號來去掉它們的特殊意義。這樣,“/^”,“/.”,和“//”,分別匹配文本字符“^”,“.”,和“/”。
重復(fù)
你已經(jīng)見過了“{3}”和“*”可以指定一個單獨字符的重復(fù)次數(shù)。稍后,你會看到相同的語法怎樣用來重復(fù)整個子表達(dá)式。此外還有其他幾種方法來指定一個重復(fù),如下表所示:
| * | 重復(fù)任意次數(shù) |
| + | 重復(fù)一次或多次 |
| ? | 重復(fù)一次或多次 |
| {n} | 重復(fù)n次 |
| {n,m} | 重復(fù)最少n次,最多m次 |
| {n,} | 重復(fù)最少n次 |
表2 常用量詞
讓我們試試幾個例子:
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
在設(shè)置和不設(shè)置“multiline”選項的時試試最后一個例子,它改變了“^”的含義。
字符集合
搜索字母數(shù)字字符,數(shù)字,和空白字符是容易的,但如果你需要搜索一個字符集合中的任意字符時怎么辦?這可以通過在方括號中列出想要的字符來輕松的解決。這樣,“[aeiou]”就能匹配任意韻母,而“[.?!]”就匹配句子末尾的標(biāo)點。在這個例子中,注意“.”和“?”在方括號中都失去了他們的特殊意義而被解釋為文本含義。我們也可以指定一個范圍的字符,所以“[a-z0-9]”表示“匹配任何小寫字母或者任何數(shù)字”。
讓我們試試一個搜索電話號碼的更加復(fù)雜的表達(dá)式:
14. /(?/d{3}[) ]/s?/d{3}[- ]/d{4} a ten digit phone number
這個表達(dá)式將會搜索幾種格式的電話號碼,像“(800)325-3535”或者“650 555 1212”。“/(?”搜索0個或1個左圓括號,“[)]”搜索一個右圓括號或者一個空格。“/s?”搜索0個或一個空白字符。不幸的是,它也會找到像“650)555-1212”這樣括號沒有去掉的情況。在下面,你會看到怎樣用可選項解決這個問題。
否定
有些時候我們需要搜索一個字符,它不是一個很容易定義的字符集合的成員。下面的表格說明了這種字符怎樣指定:
| /w | 匹配任何非字母數(shù)字字符 |
| /s | 匹配任何非空白字符 |
| /d | 匹配任何非數(shù)字字符 |
| /b | 匹配非單詞開始或結(jié)束的位置 |
| [^x] | 匹配任何非x字符 |
| [^aeiou] | 匹配任何不在aeiou中的字符 |
15. /s+ -- all strings that do not contain whitespace characters
后面,我們會看到怎樣使用“l(fā)ookahead”和“l(fā)ookbehind”來搜索缺少更加復(fù)雜的模式的情況。
可選項
要從幾個可選項中選擇,允許符合任何一個的匹配,使用豎杠“|”來分隔可選項。例如,郵政編碼有兩種,一個是5位的,另一個是9位的加一個連字符。我們可以使用下面的表達(dá)式找到任何一種:
16. /b/d{5}-/d{4}/b|/b/d{5}/b -- five and nine digit zip codes
當(dāng)使用可選項時,順序是很重要的因為匹配算法將試圖先匹配最左面的選擇。如果這個例子中的順序顛倒過來,表達(dá)式將只能找到5位的郵政編碼,而不會找到9位的。我們可以使用可選項來改進(jìn)十位電話號碼的表達(dá)式,允許包含區(qū)碼無論是通過空白字符還是連字符劃分的:
17. (/(/d{3}/)|/d{3})/s?/d{3}[- ]/d{4} -- ten digit phone numbers, a better way
分組
圓括號可以用來劃分一個子表達(dá)式來允許重復(fù)或者其他特殊的處理,例如:
18. (/d{1,3}/.){3}/d{1,3} -- a simple ip address finder
表達(dá)式的第一部分搜索后面跟著一個“/.”的一個一位到三位的數(shù)字。這被放在圓括號中并且通過使用修飾符“{3}”被重復(fù)三次,后面跟著與之前一樣的表達(dá)式而不帶后綴部分。
不幸的是,這個例子允許ip地址中被分隔的部分是任意的一位,兩位,或三位數(shù)字,盡管一個合法的ip地址不能有大于255的數(shù)字。要是能夠算術(shù)比較一個獲取的數(shù)字n使n<256就好了,但是只用正則表達(dá)式是不能夠辦到的。下一個例子使用模式匹配測試了基于第一位數(shù)字的多種可選項來保證限制數(shù)字的取值范圍。這表明一個表達(dá)式會變得很笨重,盡管搜索模式的描述是簡單的。
19. ((2[0-4]/d|25[0-5]|[01]?/d/d?)/.){3}(2[0-4]/d|25[0-5]|[01]?/d/d?) -- ip finder
一個“回引”用來搜索前面被一個分組捕獲的已匹配文本的再現(xiàn)。例如,“/1”表示“匹配分組1中已捕獲到的文本”。下面是一個例子:
20. /b(/w+)/b/s*/1/b -- find repeated words
它的運行過程是先捕獲一個分組1中“(/w+)”表示的至少包含一個字母數(shù)字字符的字符串,但僅當(dāng)它是一個單詞的開始或結(jié)束字符時才行。然后它搜索任意數(shù)量的空白字符“/s*”后跟以被捕獲的文本“/1”結(jié)尾的單詞。
在上面的例子中,想要替換分組“(/w+)”這種寫法,我們可以把它寫成“(?<word>/w+)”來給這個分組命名為“word”。一個對這個分組的回引可以寫成“/k<word>”。試試下面的例子:
21. /b(?<word>/w+)/b/s*/k<word>/b -- capture repeated word in a named group
通過使用圓括號,有很多可用的特殊用途的語法元素。一些最常用的歸納如下面這張表格:
| 捕獲 | |
| (exp) | 匹配exp并且在一個自動計數(shù)的分組中捕獲它 |
| (?<name>exp) | 匹配exp并且在一個命名的分組中捕獲它 |
| (?:exp) | 匹配exp并且不捕獲它 |
| 察看 | |
| (?=exp) | 匹配任何后綴exp之前的位置 |
| (?<=exp) | 匹配任何前綴exp之后的位置 |
| (?!exp) | 匹配任何未找到的后綴exp之后的位置 |
| (?<!exp) | 匹配任何未找到的前綴exp之前的位置 |
| 評論 | |
| (?#comment) | 評論 |
表4 常用分組結(jié)構(gòu)
前兩個我們已經(jīng)說過了。第三個“(?:exp)”不會改變匹配行為,它只是不像前兩個那樣捕獲已命名的或者計數(shù)的分組。
確定察看(positive lookaround)
下面四個是所謂的前向或后向斷言。它們從當(dāng)前的匹配向前或向后尋找需要的東西而不在匹配中包含它們。這些表達(dá)式匹配一個類似于“^”或“/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"
下面這個例子可以用來重復(fù)向三位數(shù)為一組的數(shù)字中插入逗號的例子:
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)
之前,我說明了怎樣搜索一個不是特定字符或一個字符集合的成員的字符。那么如果我們想要簡單的驗證一個字符沒有出現(xiàn),但是不想匹配任何東西怎么辦?例如,如果我們想要搜索其中“q”不是后跟著“u”的單詞怎么辦?我們可以嘗試:
26. /b/w*q[^u]/w*/b -- words with "q" followed by not "u"
運行例子你就會看到如果“q”是一個單詞的最后一個字母就不會匹配,比如“iraq”。這是因為“[^q]”總是匹配一個字符。如果“q”是單詞的最后一個字符,它會匹配后面跟著的空白字符,所以這個例子中表達(dá)式結(jié)束時匹配兩個完整的單詞。否定察看可以解決這個問題,因為它匹配一個位置而不消耗任何文本。與確定察看一樣,它也可以用來匹配一個任意復(fù)雜的子表達(dá)式的位置,而不僅僅是一個字符。我們現(xiàn)在可以做得更好:
27. /b/w*q(?!u)/w*/b -- search for words with "q" not followed by "u"
我們使用“零寬度否定前向斷言”,“(?!exp)”,只有當(dāng)后綴“exp”沒有出現(xiàn)時它才成功。下面是另一個例子:
28. /d{3}(?!/d) -- three digits not followed by another digit
相似地,我們可以使用“(?<!exp)”,“零寬度否定后向斷言”,來搜索文本中的一個位置,這里前綴“exp”沒有出現(xiàn):
29. (?<![a-z ])/w{7} -- strings of 7 alphanumerics not preceded by a letter or space
這里是另一個使用后向的例子:
30. (?<=<(/w+)>).*(?=<///1>) -- text between html tags
這個使用后向搜索一個html標(biāo)記,而使用前向搜索對應(yīng)的結(jié)束標(biāo)記,這樣,就能獲得中間的文本而不包括兩個標(biāo)記。
評論
標(biāo)點的另一個用法是使用“(?#comment)”語法包含評論。一個更好的辦法是設(shè)置“ignore pattern whitespace”選項,它允許空白字符插入表達(dá)式然后當(dāng)使用表達(dá)式時忽略它。設(shè)置了這個選項之后,任何文本每行末尾在數(shù)字符號“#”后面的東西都被忽略。例如,我們可以格式化先前的例子如下:
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
貪婪與懶惰
當(dāng)一個正則表達(dá)式有一個可以接受一個重復(fù)次數(shù)范圍的量詞(像“.*”),正常的行為是匹配盡可能多的字符。考慮下面的正則表達(dá)式:
32. a.*b -- the longest string starting with a and ending with b
如果這被用來搜索字符串“aabab”,它會匹配整個字符串“aabab”。這被稱為“貪婪”匹配。有些時候,我們更喜歡“懶惰”匹配,其中一個匹配使用發(fā)現(xiàn)的最小數(shù)目的重復(fù)。表2中所有的量詞可以增加一個問號“?”來轉(zhuǎn)換到“懶惰”量詞。這樣,“*?”的意思就是“匹配任何數(shù)目的匹配,但是使用達(dá)到一個成功匹配的最小數(shù)目的重復(fù)”。現(xiàn)在讓我們試試懶惰版本的例子(32):
33. a.*?b -- the shortest string starting with a and ending with b
如果我們把這個應(yīng)用到相同的字符串“aabab”,它會先匹配“aab”然后匹配“ab”。
| *? | 重復(fù)任意次數(shù),但盡可能少 |
| +? | 匹配一次或多次,但盡可能少 |
| ?? | 重復(fù)零次或多次,但盡可能少 |
| {n,m}? | 重復(fù)最少n次,但不多于m次,但盡可能少 |
| {n,}? | 重復(fù)最少n次,但盡可能少 |
表5 懶惰量詞
我們遺漏了什么?
我已經(jīng)描述了很多元素,使用它們來開始創(chuàng)建正則表達(dá)式;但是我還遺漏了一些東西,它們在下面的表中歸納出來。這些中的很多都在項目文件中使用額外的例子說明了。例子編號在這個表的左列中列出。
| /a | 報警字符 | |
| /b | 通常是單詞邊界,但是在一個字符集合中它表示退格鍵 | |
| /t | 制表符 | |
| 34 | /r | 回車 |
| /v | 垂直制表符 | |
| /f | 分頁符 | |
| 35 | /n | 換行符 |
| /e | esc | |
| 36 | /nnn | ascii碼八進(jìn)制數(shù)為nnn的字符 |
| 37 | /xnn | 十六進(jìn)制數(shù)為nn的字符 |
| 38 | /unnnn | unicode碼為nnnn的字符 |
| 39 | /cn | control n字符,例如回車(ctrl-m)就是/cm |
| 40 | /a | 字符串的開始(像^但是不依賴于多行選項) |
| 41 | /z | 字符串的結(jié)尾或者/n之前的字符串結(jié)尾(忽略多行) |
| /z | 字符串結(jié)尾(忽略多行) | |
| 42 | /g | 當(dāng)前搜索的開始階段 |
| 43 | /p{name} | 命名為name的unicode類中的任何字符,例如/p{isgreek} |
| (?>exp) | 貪婪子表達(dá)式,也被稱為非回溯子表達(dá)式。它只匹配一次然后就不再參與回溯。 | |
| 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) | 正則表達(dá)式選項為子表達(dá)式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 我們遺漏的東西。左端的列顯示了項目文件中說明這個結(jié)構(gòu)的例子的序號
結(jié)論
我們已經(jīng)給出了很多例子來說明.net正則表達(dá)式的關(guān)鍵特性,強調(diào)使用工具(如expresso)來測試,實踐,然后是用例子來學(xué)習(xí)。如果你想要深入的研究,網(wǎng)上也有很多在線資源會幫助你更深入的學(xué)習(xí)。你可以從訪問ultrapico網(wǎng)站開始。如果你想讀一本相關(guān)書籍,我建議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網(wǎng)站下載expresso測試,點這里下載該程序,點這里察看原文。
新聞熱點
疑難解答
圖片精選