正則表達式(Regular Expression)也叫匹配模式(Pattern),用來檢驗字符串是否滿足特定規則,或從字符串中捕獲滿足特定規則的子串。正則表達式的命名空間是System.Text.RegularExpressions,Regex類即正則表達式。
1.字符匹配
最簡單的正則表達式由“普通字符”和“通配符”組成。比如“Room\d\d\d”就這樣的正則表達式。
這些特殊字符在正則表達式中稱為元字符,因為符號“.”在正則表達式里已有特殊用處,所以要想表達“.”本身,需使用它的轉移字符“\.”,要表達符號“\”本身,同樣,需使用它的轉移字符“\\”。 .NET 提供了一批與正則表達式相關的類,它們都位於System.Text.RegularExpressions命名空間,現在我們來學習最主要的Regex 類,並用它來匹配正則表達式。Regex 類的部分方法如表所示:
通過 Regex.Matches()方法,可以從給定字符串中分解出所有與正則表達式匹配的子串,這些子串被保存在一個MatchCollection 型集合中,每個子串都被看作Match 類的對象。
現在假設某份電子文件里包含着 Kitty 的房間號碼(格式為RoomXXX),檔案很長,人工查閱很耗時間,那么如何通過計算機幫我們找到房間號碼呢?
string text = "The Colony is a beautiful town...Kitty lives in Room415...";
//正則表達式
Regex expression = new Regex(@"Room\d\d\d");
//找出所有匹配的字符串,放入集合中。
MatchCollection matches = expression.Matches(text);
//輸出匹配的字符串
foreach (Match match in matches)
{ Console.WriteLine("She lives in {0}", match); }
@前綴和轉義字符,前面學習過控制文本格式的轉義字符,如“\n”、“\"”、“\t”、“\\”等;現在我們又學習了正則表達式的轉義字符,如“\.”、“\w”、“\d” 、“\s” 、“\\”等。在正則表達式中它們兩者是有區別的。類似Regex expression = new Regex("\d");這樣的語句是錯誤的,因為反斜杠“\”本身就是一個很特殊的字符,要想表示反斜杠本身,需要使用它的轉移字符“\\”,所以需要寫成下面的形式:Regex expression = new Regex("\\d");但這種形式會降低可讀性,所以我們通常使用添加@前綴的方式。Regex expression = new Regex(@"\d");這時會忽略控制文本格式的轉義字符,但不忽略正則表達式的轉義字符。添加前綴@后,如果字符串里需要雙引號本身,可以用兩個連續的雙引號表示。Regex expression = new Regex(@"Say ""Hello"" to Heaven");。
2.可選的字符集
通符符限定了某個位置上匹配的某一類型字符,但有時還想縮小匹配的范圍,限定到某一類型的某一范圍,這里可以把某個位置上允許出現的字符寫在方括號[]內,組成可選字符集,比如:
[abc]表示該位置可以出現字母a、b、c
[A-D]表示該位置可以出現字母A、B、C、D
[A-DM-P]表示該位置可以出現字母A 到D 或M 到P
[A-Da-d]表示該位置可以出現字母A 到D 或a 到d
[12] 表示該位置可以出現數字1 或數字2
[1-5]表示該位置可以出現數字1 到5
[0-57-9]表示該位置可以出現除6 外的所有數字
[\s\S]表示該位置可以出現任何字符,包括任何可見字符和不可見字符(如空格、制表符、換行等)
注意,不管中括號看起來有多長,它都只代表一個字符。表達式中的[VR]、[a-z]、[89]、[0-9]都是一個字符,不能看作多個字符。例如:
string text = "Vitor-1970 Verne-1982 Regan-1998 Robin2008";
//正則表達式
Regex expression = new Regex(@"[VR][a-z][a-z][a-z][a-z]-19[89][0-9]");
//獲取並輸出匹配的字符串
foreach(Match match in expression.Matches(text))
{ Console.WriteLine(match); }
因為在中括號表達式中,“[”標志着中括號表達式的開始,而“]”標志着中括號表達式的結束,所以要想在中括號表達式外表示它本身,需使用它的轉義字符“\[”、 “\]”,同樣“-”表示連字符,要想在中括號表達式外表示它本身,需使用它們的轉義字符“\-”。對於“-”還有不用轉義字符的方式,也能使中括號包含“-”,例如把連字符放在中括號列表的開始或結尾,比如[-1-9]或[abc-];
反向字符集,在中括號表達式用符號“^”表示非,例如[^x]表示匹配除x的所有字符,[^abc] 匹配除a、b、c 以外的所有字符。當然,當需要匹配它本身時,要在中括號表達式中使用它的轉義字符“\^”。
3.或匹配|
在正則表達式中用符號“|”表示“或”,其左右供部分與|一起應放到()中。例如:
"x|y" 匹配 "x"或"y"
"good|ok" 匹配 "good"或"ok"
"(tr|b)ee" 匹配 "tree"或"bee"
"th(i|a)nk" 匹配 "think"或"thank"
"Book One|Two" 匹配 "Book One"或"Two"
"Book (One|Two)" 匹配 "Book One"或"Book Two"
因為符號“|”、“(”和“)”在正則表達式中也有特殊用處,所以當需要匹配它們本身時,也要使用它們的轉義字符“\|”、“\(”“\)”。
4.數量限定符
數量限定符有多種,它們之間有細微的區別。
數量限定符“*”將前面的字符重復0 次或多次,而數量限定符“+”將前面的字符重復1 次或多次,數量限定符“?”將前面的字符重復0 次或1 次,如果需要明確指定重復次數,可使用數量限定符{n},它表示把前面的字符重復n 遍,而數量限定符{n,}表示把它前面的字符至少重復n遍, 數量限定符{n,m}把前面的字符重復n至m遍。例如:
string words = "lg log loog looog loooog looooog";
//正則表達式
Regex expression = new Regex("lo*g");
//獲取並輸出匹配的字符串
foreach (Match match in expression.Matches(words))
{ Console.WriteLine(match); }
Regex expression1 = new Regex("lo+g");
//獲取並輸出匹配的字符串
foreach (Match match in expression1.Matches(words))
{ Console.WriteLine(match); }
Regex expression2 = new Regex("lo?g");
//獲取並輸出匹配的字符串
foreach (Match match in expression2.Matches(words))
{ Console.WriteLine(match); }
//正則表達式
Regex expression = new Regex("lo{3}g");
//獲取並輸出匹配的字符串
foreach (Match match in expression.Matches(words))
{ Console.WriteLine(match);}
//正則表達式
Regex expression = new Regex("lo{3,}g");
//獲取並輸出匹配的字符串
foreach (Match match in expression.Matches(words))
{ Console.WriteLine(match);}
//正則表達式
Regex expression = new Regex("lo{2,4}g");
//獲取並輸出匹配的字符串
foreach (Match match in expression.Matches(words))
{ Console.WriteLine(match);}
以上4個輸出結果分別為:“lg loog looog loooog looooog”、 “log loog looog loooog looooog”、“lg log”、“looog”、“looog loooog looooog”、“loog looog loooog”。
用數量限定符還可以重復多個字符,例如:
string words = "lg leog leoeog leoeoeog leoeoeoeog leoeoeoeoeog";
//正則表達式
Regex expression = new Regex("l(eo)+g");
//獲取並輸出匹配的字符串
foreach (Match match in expression.Matches(words))
{ Console.WriteLine(match); }
正則表達式“l(eo)+g”表示字符串的第一個字符為l,最后一個字符為g,中間有一個或多個eo。結果為:"leog leoeog leoeoeog leoeoeoeog leoeoeoeoeog"。
最奇妙的是這些數量限定符還可以與通配符組合。比如通配符“\w”匹配任意單詞字符,限定符“*”表示把前面的字符重復0 次或多次,所以正則表達式“\w*”匹配由任意單字符組成的任意長度的單詞。例如:
string girls = @"Van is 16; Vicky is 18; Vivien is 19; Vinvcent is 22";
Regex expression = new Regex(@"V\w* is \d\d");
foreach (Match match in expression.Matches(girls))
{ Console.WriteLine(match); }
結果為:Van is 16 、Vicky is 18、Vivien is 19、Vinvcent is 22
貪婪和懶惰
以上限定符都是“貪婪的”(Greedy),它們會匹配盡可能多的文本。如果在限定符后加上?號,它就會變成“懶惰的”(Lazy),會匹配盡可能少的文本。
string words = "ab<H1>Hello World</H1>c";
//貪婪的限定符
Regex expression1 = new Regex("<.*>");
MatchCollection matchs1 = expression1.Matches(words);
Console.WriteLine("There is {0} match with greedy quantifier:",matchs1.Count);
foreach (Match match in matchs1)
{Console.WriteLine(match); }
//懶惰的限定符
Regex expression2 = new Regex("<.*?>");
MatchCollection matchs2 = expression2.Matches(words);
Console.WriteLine("There are {0} matchs with lazy quantifier:",matchs2.Count);
foreach (Match match in matchs2)
{Console.WriteLine(match);}
使用貪婪的限定符有一條長子串與之匹配,使用懶惰的限定符有兩條短子串與之匹配。所以結果分別為:<H1>Hello World</H1>、<H1> </H1>。。一個是“{n}?”,它實際上與“{n}”是等價的,因為不管是貪婪還是懶惰,都已經限定死了,只能重復n次。“?”是把“?”前面字母重復0 到1 次。因為“?”是貪婪的,能重復1 次就不重復0 次(實在沒有也能匹配),“??”是懶惰的,重復次數要盡量少,所以它選擇重復0 次。
符號“*”、“+”、“?”“{”和“}”在正則表達式中也有特殊用處,所以當需要
匹配它們本身時,也要使用它們的轉義字符“\*”、“\+”、“\?”“\{”和“\}”。
5.定位符
通過定位符可以在指定位置尋找匹配的子串。若正則表達式中使用了定位符“^”,則在整個字符串的頭部尋找匹配的子串。例如string words = "year1998 year2008 year2018"; Regex expression = new Regex(@"^year\d\d\d\d");“^”匹配字符串的開頭位置,所以“^year\d\d\d\d”表示要從字符串的開頭位置開始尋找,這里匹配結果為year1998。若正則表達式中使用了定位符“$”,則在整個字符串的尾部尋找匹配的子串。string words = "year1998 year2008 year2018"; Regex expression = new Regex(@"year\d\d\d\d$");匹配結果為year2018。
若正則表達式中使用了定位符“\b”,則在字符串中每個“單詞”的邊界尋找匹配的子串。(單詞通常以空格、段首、段尾、逗號、句號等符號作為邊界,分隔符“-”也可以作為邊界。但要注意“\b”只表示單詞與邊界符號之間的“位置”,不表示邊界符號本身。)
string words = "formfordfork";
Console.Write("None anchors:");
Regex expression = new Regex(@"for\w");
foreach (Match match in expression.Matches(words))
{ Console.Write(match + "\t"); }
Console.Write("\nAnchor start:");
expression = new Regex(@"\bfor\w");
foreach (Match match in expression.Matches(words))
{ Console.Write(match + "\t"); }
Console.Write("\n Anchor end:");
expression = new Regex(@"for\w\b");
foreach (Match match in expression.Matches(words))
{ Console.Write(match + "\t");}
結果為:form ford fork 、form、fork。
可以看出當沒有定位符時,匹配單詞中所有符合要求的子串;當定位符\b 在前面時,在單詞的頭部尋找匹配的子串;當定位符\b 在后面時,在單詞的結尾尋找匹配的子串。常用正則表達式“\b\w+\b”找出一句話里的所有單詞。
string words = "are you ok?";
Regex expression = new Regex(@"\b\w+\b");
foreach (Match match in expression.Matches(words))
{Console.WriteLine(match);}
結果為:are you ok。
顯然,“^”和“$”在正則表達式中也有特殊用處,所以當要匹配它們本身時也需使用它們的轉義字符“\^”和“\$”。
6.分組和向后引用
括號表達式並不簡單的起着確定范圍的作用,它同時會創建子表達式,每個子表達式形成一個分組,並把捕獲到的與子表達式匹配的子串保存在分組中,以供將來使用。電子郵件地址通常由用戶名、二級域名、一級域名三部分構成,通常用"(\w+)@(\w+)\.(\w+)"匹配,這里有三個括號,形成三個子表達式,就會出現三個分組。默認情況下,每個分組會自動擁有一個組號,規則是:從左向右,按分組左括號的出現順序進行編號,第一個分組的組號為1,第二個為2,以此類推。在正則表達式里引用分組的語法為“\number”,比如“\1”代表與分組1 匹配的子串,“\2”代表與分組2 匹配的字串,等等。存儲起來的分組有什么作用呢?下面我們通過一個例子來說明。很多句子里會有重復的單詞,下面我們就通過正則表達式找出這些重復的單詞。
string text = "you are very very good";
foreach (Match match in Regex.Matches(text, @"\b(\w+)\b \1\b"))
{ Console.WriteLine(match);}
結果為:very very。
前面已經知道“\b(\w+)\b”匹配一個單詞,因為這里面有一個括號,所以會捕獲一個以1 為組號的分組,后面的“\1”就代表這個分組,表示這個位置上出現和分組1 一樣的內容。所以兩者連起來就表示兩個重復的單詞。這種在后面的表達式中引用前面的分組的方式就叫做后向引用。除了匹配重復的單詞,后向引用還有一個較為常見的應用,那就是匹配有效的HTML標簽。例如:
string words = "<h0>not valid</h1> <h2>valid</h2> <h3>not valid</h4>";
foreach (Match match in Regex.Matches(words, @"<(.*?)>.*?</\1>"))
{Console.WriteLine(match);}
結果為:<h2>valid</h2>。
7.替換文本
正則表達式除了查找文本外,另一項重要功能就是替換文本。通過Regex 類的Replace()方法就能以特定字符串替換原字符串中與正則表達式匹配的子串。下面通過一個例子說明如何替換文本以及如何在替換中引用分組。電話號碼格式(010)88665987、(0769)23658945 ,可以用正則表達式“\((\d{3,4})\)(\d{7,8})”匹配,該表達式中有兩個分組,第一個分組對應區號,第二個分組對應號碼,現在來把電話本中所有的電話都改為形如010-88665987 的格式。
string phonebook = "(010)88665987 (020)23658945 (021)88965222";
string pattern = @"\((\d{3,4})\)(\d{7,8})";
string result = Regex.Replace(phonebook, pattern, "$1-$2");
Console.WriteLine(result);
結果:010-88665987、020-23658945、021-88965222。
在大部分語言的正則表達式中,查找時,使用后向引用的語法為“\number”;而在替換時,其語法為“$number”。 例子中的Regex.Replace ()方法把與正則表達式匹配的子串替換為“$1-$2”,其中$1 對應與分組1(即區號),$2 對應分組2(即電話號碼)。
在.NET 中使用正則表達式進行替換時,分組的命名方式為:(?<name> subexpression)(即可以對分組顯式命名),后向引用的語法是:${name}。所以上例也可以寫為:
string phonebook = "(010)88665987 (020)23658945 (021)88965222";
string pattern = @"\((?<areacode>\d{3})\)(?<number>\d{8})";
string result = Regex.Replace(phonebook, pattern, "${areacode}-${number}");
Console.WriteLine(result);
8. 非捕獲分組和預查
(1)非捕獲后組?:
很多時候,我們添加一個分組,並不是為了在后向引用中使用它,而是出於表達上的需要。比如正則表達式“(tr|b)ee”中括號,僅僅是為了確定范圍,使之正確匹配“tree”或“bee”。 存儲分組會花費一定的時間,如果我們不需要存儲分組,可以在分組內部添加元字符“?:”,把它標志成一個非捕獲分組。"(?:tr|b)ee"。這樣就不會存儲分組了。
(2)正向預查?=和負正向預查?!
string text = @"Jack boy 29 010-88127631 Microsoft, Sally girl 22 010-88127632 Microsoft,Ben boy 26 020-65423541 Google,Merry girl 23 020-65423542 Google,Alan boy 27 021-23456851 Apple,Kitty girl 18 021-23456852 Apple.";
Console.WriteLine("The Telephone numbers of Microsoft are:");
foreach (Match match in Regex.Matches(text, @"\d{3}-\d{8}(?= Microsoft)"))
{Console.WriteLine(match);}
結果為:010-88127631、010-88127632。
如果僅僅使用正則表達式“\d{3}-\d{8}”,它就會匹配文本里所有的電話號碼,那么如何讓它只匹配微軟的電話號碼呢?這時就需要在它后面的一個分組中添加元字符“(?= Microsoft)”作為限制條件,其格式如下所示。正向預查表達式用來篩選與查詢目標表達式匹配的文本,其結果不光要和查詢目標表達式匹配,它后面跟隨的文本還要和正向預查表達式匹配。需要強調的是正向預查表達式只是用來篩選查詢目標,並不屬於查詢目標本身,不包含在最終結果中。
負正向預查與正向預查相反,查詢目標后面不能跟指定的字符串,其語法為在分組中添加元字符“?!”。 正則表達式“\d{3}-\d{8}(?! Microsoft)”匹配那些后面不跟“ Microsoft”的電話號碼。
(3)反向預查?<=和負反向預查?<!
正向預查?=篩選查尋目標,限制條件在查詢目標的后面,反向預查?<=篩選查尋目標時,限制條件則在查詢目標的前面,這時就需要在它前面的一個分組中添加元字符“(?= Microsoft)”作為限制條件。其使用方法例如:
string text = @"Jack boy 29 010-88127631 Microsoft, Sally girl 22 010-88127632 Microsoft,Ben boy 26 020-65423541 Google,Merry girl 23 020-65423542 Google,Alan boy 27 021-23456851 Apple,Kitty girl 18 021-23456852 Apple.";
Console.WriteLine("The Telephone numbers of girls are:");
foreach (Match match in Regex.Matches(text, @"(?<=girl \d\d )\d{3}-\d{8}"))
{Console.WriteLine(match);}
負反向預查與反向預查相反,查詢目標前面不能跟指定字符串,其語法是在分組中添加元字符“?<!”。正則表達式“(?<!girl \d\d )\d{3}-\d{8}”匹配這樣的電話號碼,它們前面的文本不能與“girl \d\d ”匹配。
總之,預查表達式的作用就是給查詢目標添加限制條件,可以在前面,也可以在后面,可以正面限制,也可以反面限制。
9.正則表達式的類
和正則表達式相關的類都在命名空間System.Text.RegularExpressions 中,上圖展示了部分常用正則表達式類。
string s = "aaa@163.com bbb@263.net ccc@363.cn";
MatchCollection matchs = Regex.Matches(s, @"(\w+)@(\w+)\.(\w+)");
//輸出MatchCollection中所有的Match
Console.WriteLine("所有Match為:");
for (int i = 0; i < matchs.Count; i++)
{ Match match = matchs[i];
Console.WriteLine(match.Value); }
Macth 的Value 屬性就是匹配的字符串,每個 Match 中都有一個Groups 屬性,里面包含了該Match 的所有分組。其中第0 個分組是Match 本身
10.正則表達式的選項
通過Regex 類的Options 屬性可以設置正則表達式的選項,下表是.Net 中常用的正則表達式選項。
例如:
string pattern = @"Room\d{3}";
Regex expression = new Regex(pattern, RegexOptions.IgnoreCase);
如果想同時設置多個選項,可以把它們用按位或“|”連接起來。
new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Multiline);
11.注釋
遇到復雜的、比較難理解的正則表達式時,我們可以通過添加注釋的方式進行解釋說明。在正則表達式中添加注釋的語法為:(?#注釋內容)。比如:
@"\((\d{3,4})(?#該分組匹配區號)\)(\d{7,8})(?#該分組匹配電話號碼)"。