正則
by Jim Hollenhorst 譯 寒帶魚
你是否曾經想過Regex是什麼,怎樣能夠快速得到對它的一個基本的認識?我的目的就是在30分鐘內帶你入門並且對Regex有一個基本的理解。事實是Regex並沒有它看起來那麼複雜。學習它最好的辦法就是開始寫Regex並且不斷實踐。在最初的30分鐘之後,你就應該知道一些基本的結構並且有能力在你的程式或者web頁面中設計和使用Regex了。對那些想要深入研究的人,現在已經有很多非常好的可用資源來讓你更深入的學習。
到底什麼是Regex?
我相信你對模式比對的“電腦萬用字元”字元應該比較熟悉了。例如,如果你想要在一個Windows檔案夾中找到所有Mircosoft Word檔案,你要搜尋“*.doc”,因為你知道星號會被解釋為一個萬用字元,它匹配所有序列的字串。Regex就是這種功能的一個更加細節的擴充。
在寫處理文本的程式或者web頁面時,定位匹配複雜模式的字串是很常見的。Regex就是用來描述這類模式的。這樣,一個Regex就是一個模式的縮減代碼。例如,模式“\w+”是表達“匹配任何包含字母數字字元的非Null 字元串”的精確方法。.NET架構提供了一個功能強大類庫,它使得在你的應用程式中包含Regex更加容易。使用這個庫,你可以輕易地搜尋和替換文本,解碼複雜的標題,解析語言,或者驗證文本。
學習Regex的神秘的文法的一個好辦法是用例子作為開始學習的對象,然後實踐建立自己的Regex。
讓我們開始吧!
一些簡單的例子
搜尋Elvis
假設你要花費你所有的空餘時間來掃描文檔來尋找Elvis仍然活著的證據。你可以使用下面的Regex來搜尋:
1. elvis -- Find elvis
這是搜尋精確字元序列的一個完全合法的Regex。在.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"
僅僅使用幾個特殊字元我們就開始建立功能強大的Regex了,而且它們已經開始變得難以被我們人類理解了。
讓我們看看另一個例子。
確定電話號碼的合法性
假設你的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}”表示“重複前面的字元三次”。
.NETRegex的基礎
讓我們探索一下.NET中Regex的基礎
特殊字元
你應該知道幾個有特殊意義的字元。你已經見過了“\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 |
匹配一個單詞的開始或結尾 |
^ |
匹配字串的開始 |
$ |
匹配字字串的結尾 |
表1 Regex的常用特殊字元
開始階段
特殊字元“^”和“$”被用來搜尋那些必須以一些文本開頭和(或)以一些文本結尾的文本。特別是在驗證輸入時特別有用,在這些驗證中,輸入的整個文本必須要匹配一個模式。例如,要驗證一個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次 |
表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
在設定和不設定“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中的字元 |
表3 怎樣指定你不想要東西
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就好了,但是只用Regex是不能夠辦到的。下一個例子使用模式比對測試了基於第一位元字的多種可選項來保證限制數位取值範圍。這表明一個運算式會變得很笨重,儘管搜尋模式的描述是簡單的。
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
貪婪與懶惰
當一個Regex有一個可以接受一個重複次數範圍的量詞(像“.*”),正常的行為是匹配儘可能多的字元。考慮下面的Regex:
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 懶惰量詞
我們遺漏了什嗎?
我已經描述了很多元素,使用它們來開始建立Regex;但是我還遺漏了一些東西,它們在下面的表中歸納出來。這些中的很多都在專案檔中使用額外的例子說明了。例子編號在這個表的左列中列出。
|
\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) |
Regex選項為子運算式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 我們遺漏的東西。左端的列顯示了專案檔中說明這個結構的例子的序號
結論
我們已經給出了很多例子來說明.NETRegex的關鍵特性,強調使用工具(如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測試,點這裡下載該程式,點這裡察看原文。