一提到Regex,相信好多人都不會陌生,並且很多時候,我們都用過,比如說驗證郵箱或者是手機號碼的正確性等等,在.NET中,提供了強大的Regex輔助類,最主要的還是要數Regex類,利用這個類,可以非常方便的來操作Regex的匹配工作:
| 代碼如下 |
複製代碼 |
string matchText = "this|is|test"; Regex reg = new Regex(@"[a-z]+|"); MatchCollection mc = reg.Matches(matchText); foreach (Match myValue in mc) { MessageBox.Show(myValue.Value); } |
上面就是一個非常簡單的例子,用來匹配英文字元以及豎線,這裡匹配出來的結果就是this|和is|。需要注意的是,這裡我們需要引入命名空間:
using System.Text.RegularExpressions;
下面具體來說明Regex。
首先,對於Regex,我們需要知道三個命令,用英文字母表示,就是BCD,怎麼說呢?
B指三種括符類型:中括弧[],它是用來匹配你需要匹配的字串;大括弧{},它是用來指定匹配的長度;小括弧(),它是用來分組。
C 指脫字元,也即上尖角符號(^),它是用來指定匹配的開始。
D指貨幣符號,也即$,它是用來指定匹配的結束。
下面以一個例子來說明:
首先我這裡有一堆郵件地址的集合:
"使用者一"<491204829@qq.com>,
"使用者二"<12340352@qq.com>,
"使用者三"<962390304@qq.com>,
"使用者四"<xylw2y2011@163.com>,
"使用者五"<443225735@qq.com>,
"使用者六"519733331@qq.com
然後我們只需要提取其後面的實際郵件地址,那麼這個該如何來做呢?
首先我們觀察,後面的郵件地址都是數字或者是字母的集合,然後帶上@符號,然後都是以數字或者字母結合,最後都是以.com結束,知道了大致的規則後,我們來進行匹配。
首先是在<內的數字或者字母的集合,[a-zA-Z0-9]指匹配a到z或者是A到Z或者是0-9的字串,長度一般都是10位左右,那麼就匹配1到10位的:{1,10},然後就是@符號後面,首先在點號之前的也是字母或者數位組合[a-zA-Z0-9],同樣我們限定長度為10位之間{1,10},然後是點號後面的,我們可以利用(com|org)進行匹配,這樣,匹配出來的結果為:
| 代碼如下 |
複製代碼 |
<[a-zA-Z0-9]{1,10}@[a-zA-Z0-9]{1,10}.(com|org)> |
下面通過代碼來說明:
| 代碼如下 |
複製代碼 |
string myMailInfo = this.txtUserMail.Text; Regex reg = new Regex(@"<[a-zA-Z0-9]{1,10}@[a-zA-Z0-9]{1,10}.(com|org)>"); //Regex reg = new Regex(@"<S{1,20}@S{6}>"); MatchCollection mc = reg.Matches(myMailInfo); this.txtUserMail.Text = string.Empty; for (int i = 0; i < mc.Count; i++) { this.txtUserMail.AppendText(mc[i].Value+","); } |
這樣就很容易的將結果提取出來,結果如下:
<491204829@qq.com>,
<12340352@qq.com>,
<962390304@qq.com>,
<xylw2y2011@163.com>,
<443225735@qq.com>,
<519733331@qq.com>,
所以這個需要找出一定的規律,然後進行的話,將會變得比較容易一些。下面是匹配的Mapping關係:
匹配字母或者數字
[a-zA-Z0-9]
匹配長度在1到10之間的字母或數字組合
[a-zA-Z0-9]{1,10}
匹配固定字串com或者是org
(com|org)
當然,其實如果說不匹配任何字元或者數字或者其他字串,該怎麼弄呢?其實,可以使用^來決定,如果不想以字母開頭,可以利用^w來表示,不想以數字開頭,可以利用^d來表示,不想以任何空白字元開頭,可以利用^s來表示,這個其實還有更簡單的寫法,也即:^w 對應於 W ; ^d對應於D;^s對應於S。
下面是一些常見的元字元:
元字元
說明
.
匹配除 n 以外的任何字元(注意元字元是小數點)。
[abcde]
匹配abcde之中的任意一個字元
[a-h]
匹配a到h之間的任意一個字元
[^fgh]
不與fgh之中的任意一個字元匹配
w
匹配大小寫英文字元及數字0到9之間的任意一個及底線,相當於[a-zA-Z0-9_]
W
不匹配大小寫英文字元及數字0到9之間的任意一個,相當於[^a-zA-Z0-9_]
s
匹配任何空白字元,相當於[ fnrtv]
S
匹配任何非空白字元,相當於[^s]
d
匹配任何0到9之間的單個數字,相當於[0-9]
D
不匹配任何0到9之間的單個數字,相當於[^0-9]
[u4e00-u9fa5]
匹配任意單個漢字
下面一部分內容來自周公的部落格,個人認為講解的非常好:
上面的元字元都是針對單個字元匹配的,要想同時匹配多個字元的話,還需要藉助限定符。下面是一些常見的限定符(下表中n和m都是表示整數,並且0<n<m):
限定符 說明
-----------------------------------------------
* 匹配0到多個元字元,相當於{0,}
-----------------------------------------------
? 匹配0到1個元字元,相當於{0,1}
-----------------------------------------------
{n} 匹配n個元字元
-----------------------------------------------
{n,} 匹配至少n個元字元
-----------------------------------------------
{n,m} 匹配n到m個元字元
-----------------------------------------------
+ 匹配至少1個元字元,相當於{1,}
-----------------------------------------------
b 匹配單詞邊界
-----------------------------------------------
^ 字串必須以指定的字元開始
-----------------------------------------------
$ 字串必須以指定的字元結束
-----------------------------------------------
說明:
(1)由於在Regex中“”、“?”、“*”、“^”、“$”、“+”、“(”、“)”、“|”、“{”、“[”等字元已經具有一定特殊意義,如果需要用它們的原始意義,則應該對它進行轉義,例如希望在字串中至少有一個“”,那麼Regex應該這麼寫:\+。
(2)可以將多個元字元或者原義文本字元用括弧括起來形成一個分組,比如^(13)[4-9]d{8}$表示任意以13開頭的移動手機號碼。
(3)另外對於中文字元的匹配是採用其對應的Unicode編碼來匹配的,對於單個Unicode字元,如u4e00表示漢字“一”, u9fa5表示漢字“?”,在Unicode編碼中這分別是所能表示的漢字的第一個和最後一個的Unicode編碼,在Unicode編碼中能表示20901個漢字。
(4)關於b的用法,它代表單詞的開始或者結尾,以字串“123a 345b 456 789d”作為樣本字串,如果Regex是“bd{3}b”,則僅能匹配456。
(5)可以使用“|”來表示或的關係,例如[z|j|q]表示匹配z、j、q之中的任意一個字母。
Regex分組(本部分及以下來源自周公部落格:)
將Regex的一部分用()括起來就可以形成一個分組,也叫一個子匹配或者一個擷取的群組。例如對於“08:14:27”這樣格式的時間,我們可以寫如下的Regex:
((0[1-9])|(1[0-9])|(2[0-3])(:[0-5][1-9]){2}
如果以這個作為運算式,它將從下面的一段IIS訪問日誌中提取出訪問時間(當然分析IIS日誌最好的工具是Log Parser這個微軟提供的工具):
00:41:23 GET /admin_save.asp 202.108.212.39 404 1468 176
01:04:36 GET /userbuding.asp 202.108.212.39 404 1468 176
10:00:59 GET /upfile_flash.asp 202.108.212.39 404 1468 178
12:59:00 GET /cp.php 202.108.212.39 404 1468 168
19:23:04 GET /sqldata.php 202.108.212.39 404 1468 173
23:00:00 GET /Evil-Skwiz.htm 202.108.212.39 404 1468 176
23:59:59 GET /bil.html 202.108.212.39 404 1468 170
如果我們想對上面的IIS日誌進行分析,提取每條日誌中的訪問時間、訪問頁面、用戶端IP及伺服器端響應代碼(對應C#中的HttpStatusCode),我們可以按照分組的方式來擷取。
代碼如下:
private String text= @"00:41:23 GET /admin_save.asp 202.108.212.39 404 1468 176
01:04:36 GET /userbuding.asp 202.108.212.39 404 1468 176
10:00:59 GET /upfile_flash.asp 202.108.212.39 404 1468 178
12:59:00 GET /cp.php 202.108.212.39 404 1468 168
19:23:04 GET /sqldata.php 202.108.212.39 404 1468 173
23:00:00 GET /Evil-Skwiz.htm 202.108.212.39 404 1468 176
23:59:59 GET /bil.html 202.108.212.39 404 1468 170";
/// <summary>
/// 分析IIS日誌,提取用戶端訪問的時間、URL、IP地址及伺服器響應代碼
/// </summary>
public void AnalyzeIISLog()
{
//提取訪問時間、URL、IP地址及伺服器響應代碼的Regex
//大家可以看到關於提取時間部分的子運算式比較複雜,因為做了比較嚴格的時間匹配限制
//注意為了簡化起見,沒有對用戶端IP格式進行嚴格驗證,因為IIS訪問日誌中也不會出現不符合要求的IP地址
| 代碼如下 |
複製代碼 |
Regex regex = new Regex(@ "((0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2})s(GET)s([^s]+)s(d{1,3}(.d{1,3}){3})s(d{3})", RegexOptions.None); MatchCollection matchCollection = regex.Matches(text); for (int i = 0; i < matchCollection.Count; i++) { Match match = matchCollection[i]; Console.WriteLine( "Match[{0}]========================", i); for (int j = 0; j < match.Groups.Count; j++) { Console.WriteLine( "Groups[{0}]={1}", j, match.Groups[j].Value); } } } |
這段代碼的輸出結果如下:
Match[0]========================
Groups[0]=00:41:23 GET /admin_save.asp 202.108.212.39 404
Groups[1]=00:41:23
Groups[2]=00
Groups[3]=:23
Groups[4]=GET
Groups[5]=/admin_save.asp
Groups[6]=202.108.212.39
Groups[7]=.39
Groups[8]=404
Match[1]========================
Groups[0]=01:04:36 GET /userbuding.asp 202.108.212.39 404
Groups[1]=01:04:36
Groups[2]=01
Groups[3]=:36
Groups[4]=GET
Groups[5]=/userbuding.asp
Groups[6]=202.108.212.39
Groups[7]=.39
Groups[8]=404
Match[2]========================
Groups[0]=10:00:59 GET /upfile_flash.asp 202.108.212.39 404
Groups[1]=10:00:59
Groups[2]=10
Groups[3]=:59
Groups[4]=GET
Groups[5]=/upfile_flash.asp
Groups[6]=202.108.212.39
Groups[7]=.39
Groups[8]=404
Match[3]========================
Groups[0]=12:59:00 GET /cp.php 202.108.212.39 404
Groups[1]=12:59:00
Groups[2]=12
Groups[3]=:00
Groups[4]=GET
Groups[5]=/cp.php
Groups[6]=202.108.212.39
Groups[7]=.39
Groups[8]=404
Match[4]========================
Groups[0]=19:23:04 GET /sqldata.php 202.108.212.39 404
Groups[1]=19:23:04
Groups[2]=19
Groups[3]=:04
Groups[4]=GET
Groups[5]=/sqldata.php
Groups[6]=202.108.212.39
Groups[7]=.39
Groups[8]=404
Match[5]========================
Groups[0]=23:00:00 GET /Evil-Skwiz.htm 202.108.212.39 404
Groups[1]=23:00:00
Groups[2]=23
Groups[3]=:00
Groups[4]=GET
Groups[5]=/Evil-Skwiz.htm
Groups[6]=202.108.212.39
Groups[7]=.39
Groups[8]=404
Match[6]========================
Groups[0]=23:59:59 GET /bil.html 202.108.212.39 404
Groups[1]=23:59:59
Groups[2]=23
Groups[3]=:59
Groups[4]=GET
Groups[5]=/bil.html
Groups[6]=202.108.212.39
Groups[7]=.39
Groups[8]=404
從上面的輸出結果中我們可以看出在每一個匹配結果中,第2個分組就是用戶端訪問時間(因為索引是從0開始的,所以索引順序為1,以下同理),第6個分組是訪問的URL(索引順序為6),第7個分組是用戶端IP(索引順序為6),第9個分組是伺服器端響應代碼(索引順序為9)。如果我們要提取這些元素,可以直接按照索引來訪問這些值就可以了,這樣比我們不採用Regex要方便多了。
命名擷取的群組
上面的方法儘管方便,但也有一些不便之處:假如需要提取更多的資訊,對擷取的群組進行了增減,就會導致擷取的群組索引對應的值發生變化,我們就需要重新修改代碼,這也算是一種寫入程式碼吧。有沒有比較好的辦法呢?答案是有的,那就是採用命名擷取的群組。
就像我們使用DataReader訪問資料庫或者訪問DataTable中的資料一樣,可以使用索引的方式(索引同樣也是從0開始),不過如果變化了select語句中的欄位數或者欄位順序,按照這種方式擷取資料就需要重新變動,為了適應這種變化,同樣也允許使用欄位名作為索引來訪問資料,只要資料來源中存在這個欄位而不管順序如何都會取到正確的值。在Regex中命名擷取的群組也可以起到同樣的作用。
普通擷取的群組表示方式:(Regex),如(d{8,11});
命名擷取的群組表示方式:(?<擷取的群組命名>Regex),如(?<phone>d{8,11})
對於普通擷取的群組只能採用索引的方式擷取它對應的值,但對於命名擷取的群組,還可以採用按名稱的方式訪問,例如(?<phone>d{8,11}),在代碼中就可以按照match.Groups["phone"]的方式訪問,這樣代碼更直觀,編碼也更靈活,針對剛才的對IIS日誌的分析,我們採用命名擷取的群組的代碼如下:
private String text= @"00:41:23 GET /admin_save.asp 202.108.212.39 404 1468 176
01:04:36 GET /userbuding.asp 202.108.212.39 404 1468 176
10:00:59 GET /upfile_flash.asp 202.108.212.39 404 1468 178
12:59:00 GET /cp.php 202.108.212.39 404 1468 168
19:23:04 GET /sqldata.php 202.108.212.39 404 1468 173
23:00:00 GET /Evil-Skwiz.htm 202.108.212.39 404 1468 176
23:59:59 GET /bil.html 202.108.212.39 404 1468 170";
/// <summary>
/// 採用命名擷取的群組提取IIS日誌裡的相關資訊
/// </summary>
| 代碼如下 |
複製代碼 |
public void AnalyzeIISLog2() { Regex regex = new Regex(@ "(?<time>(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2})s(GET)s(?<url>[^s]+)s(?<ip>d{1,3}(.d{1,3}){3})s(?<httpCode>d{3})", RegexOptions.None); MatchCollection matchCollection = regex.Matches(text); for (int i = 0; i < matchCollection.Count; i++) { Match match = matchCollection[i]; Console.WriteLine( "Match[{0}]========================", i); Console.WriteLine( "time:{0}", match.Groups["time"]); Console.WriteLine( "url:{0}", match.Groups["url"]); Console.WriteLine( "ip:{0}", match.Groups["ip"]); Console.WriteLine( "httpCode:{0}", match.Groups["httpCode"]); } } |
這段代碼的執行效果如下:
Match[0]========================
time:00:41:23
url:/admin_save.asp
ip:202.108.212.39
httpCode:404
Match[1]========================
time:01:04:36
url:/userbuding.asp
ip:202.108.212.39
httpCode:404
Match[2]========================
time:10:00:59
url:/upfile_flash.asp
ip:202.108.212.39
httpCode:404
Match[3]========================
time:12:59:00
url:/cp.php
ip:202.108.212.39
httpCode:404
Match[4]========================
time:19:23:04
url:/sqldata.php
ip:202.108.212.39
httpCode:404
Match[5]========================
time:23:00:00
url:/Evil-Skwiz.htm
ip:202.108.212.39
httpCode:404
Match[6]========================
time:23:59:59
url:/bil.html
ip:202.108.212.39
httpCode:404
採用命名擷取的群組之後使訪問擷取的群組的值更直觀了,而且只要命名擷取的群組的值不發生變化,其它的變化都不影響原來的代碼。
非擷取的群組
如果經常看別人有關Regex的原始碼,可能會看到形如(?: 子運算式)這樣的運算式,這就是非擷取的群組,對於擷取的群組我們可以理解,就是在後面的代碼中可以通過索引或者名稱(如果是命名擷取的群組)的方式來訪問匹配的值,因為在匹配過程中會將對應的值儲存到記憶體中,如果我們在後面不需要訪問匹配的值那麼就可以告訴程式不用在記憶體中儲存匹配的值,以便提高效率減少記憶體消耗,這種情況下就可以使用非擷取的群組,例如在剛剛分析IIS日誌的時候我們對用戶端提交請求的方式並不在乎,在這裡就可以使用非擷取的群組,如下:
| 代碼如下 |
複製代碼 |
Regex regex = new Regex(@"(?<time>(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2})s(?:GET)s(?<url>[^s]+)s(?<ip>d{1,3}(.d{1,3}){3})s(?<httpCode>d{3})"; |
零寬度斷言
關於零寬度斷言有多種叫法,也有叫環視、也有叫預搜尋的,我這裡採用的是MSDN中的叫法,關於零寬度斷言有以下幾種:
(?= 子運算式):零寬度正預測先行斷言。僅當子運算式在此位置的右側匹配時才繼續匹配。例如,19(?=99) 與跟在99前面的19執行個體匹配。
(?! 子運算式):零寬度負預測先行斷言。僅當子運算式不在此位置的右側匹配時才繼續匹配。例如,(?!99)與不以99結尾的單詞匹配,所以不與1999匹配。
(?<= 子運算式):零寬度正回顧後發斷言。僅當子運算式在此位置的左側匹配時才繼續匹配。例如,(?<=19)99 與跟在 19 後面的 99 的執行個體匹配。此構造不會回溯。
(?<! 子運算式):零寬度負回顧後發斷言。僅當子運算式不在此位置的左側匹配時才繼續匹配。例如(?<=19)與不以19開頭的單詞匹配,所以不與1999匹配。
Regex選項
在使用Regex時除了使用RegexOptions這個枚舉給Regex賦予一些額外的選項之外,還可以在在運算式中使用這些選項,如:
| 代碼如下 |
複製代碼 |
Regex regex = new Regex("(?i)def"); Regex regex = new Regex("(?i)def"); |
它與下面一句是等效的:
| 代碼如下 |
複製代碼 |
Regex regex = new Regex("def", RegexOptions.IgnoreCase); |
Regex regex = new Regex("def", RegexOptions.IgnoreCase);
採用(?i)這種形式的稱之為內聯模式,顧名思義就是在Regex中已經體現了Regex選項,這些內聯字元與RegexOptions的對應如下:
IgnoreCase:內聯字元為i,指定不區分大小寫匹配。
Multiline:內聯字元為m,指定多行模式。更改 ^ 和 $ 的含義,以使它們分別與任何行的開頭和結尾匹配,而不只是與整個字串的開頭和結尾匹配。
ExplicitCapture:內聯字元為n,指定唯一有效捕獲是顯式命名或編號的 (?<name>…) 形式的組。這允許圓括弧充當非擷取的群組,從而避免了由 (?:…) 導致的文法上的笨拙。
Singleline:內聯字元為s,指定單行模式。更改句點字元 (.) 的含義,以使它與每個字元(而不是除 n 之外的所有字元)匹配。
IgnorePatternWhitespace:內聯字元為x,指定從模式中排除非轉義空白並啟用數字記號 (#) 後面的注釋。(有關轉義空白字元的列表,請參見字元轉義。) 請注意,空白永遠不會從字元類中消除。
舉例說明:
| 代碼如下 |
複製代碼 |
RegexOptions option=RegexOptions.IgnoreCase|RegexOptions.Singleline; Regex regex = new Regex("def", option); |
用內聯的形式表示為:
| 代碼如下 |
複製代碼 |
Regex regex = new Regex("(?is)def"); |