Regex提供了功能強大、靈活而又高效的方法來處理文本。Regex的全面模式比對標記法使您可以快速分析大量文本以找到特定的字元模式;提取、編輯、替換或刪除文本子字串;或將提取的字串添加到集合以產生報告。對於處理字串(例如 HTML 處理、記錄檔分析和 HTTP 標題分析)的許多應用程式而言,Regex是不可缺少的工具。Regex是一個非常有用的技術,有人曾稱之為能讓程式員不至於丟掉飯碗的十大技術之一,可見它的重要性。
熟悉 DOS 或者命令列的朋友或許已經用過類似的功能,比如我們要尋找 D 盤下所有的低於 Word2007 版本的 Word 檔案(因為低於 Word2007 版本的 Word 檔案的檔案尾碼是 .doc ,而 Word2007 版本的 Word 檔案的檔案尾碼是 .docx ),我們可以在命令列下執行這個命名:
dir D:/*doc
當然如果想尋找 D 盤下任意級子目錄下的所有此類檔案,就應該執行 dir /s D:/*doc 了。
注意Regex並不是在 C# 中專屬的東東,實際上在其它語言中早就實現了,比如 Perl (可能很多人沒有聽說過這個程式設計語言,十年前大學期間我曾經學過一點皮毛),其它的程式設計語言 Java 、 PHP 及 JavaScript 等也支援Regex,Regex差不多像 SQL 語言一樣成為標準了,同樣和 SQL 類似,在不同的資料庫廠商那裡對 SQL 標準支援的程度並不完全一樣,Regex也是如此,大部分內的Regex可以跨語言使用,但是在各語言中也會有細微的區別,這一點是需要我們注意的。
Regex元字元
Regex語言由兩種基底字元類型組成:原義(正常)文本字元和元字元。元字元使Regex具有處理能力。元字元既可以是放在 [] 中的任意單個字元(如 [a] 表示匹配單個小寫字元 a ),也可以是字元序列(如 [a-d] 表示匹配 a 、 b 、 c 、 d 之間的任意一個字元,而 /w 表示任意英文字母和數字及底線),下面是一些常見的元字元:
元字元 |
說明 |
. |
匹配除 /n 以外的任何字元(注意元字元是小數點)。 |
[abcde] |
匹配 abcde 之中的任意一個字元 |
[a-h] |
匹配 a 到 h 之間的任意一個字元 |
[^fgh] |
不與 fgh 之中的任意一個字元匹配 |
/w |
匹配大小寫英文字元及數字 0 到 9 之間的任意一個及底線,相當於 [a-zA-Z0-9_] |
/W |
不匹配大小寫英文字元及數字 0 到 9 之間的任意一個,相當於 [^a-zA-Z0-9_] |
/s |
匹配任何空白字元,相當於 [ /f/n/r/t/v] |
/S |
匹配任何非空白字元,相當於 [^/s] |
/d |
匹配任何 0 到 9 之間的單個數字,相當於 [0-9] |
/D |
不匹配任何 0 到 9 之間的單個數字,相當於 [^0-9] |
[/u4e00-/u9fa5] |
匹配任意單個漢字(這裡用的是 Unicode 編碼錶示漢字的 ) |
Regex限定符
上面的元字元都是針對單個字元匹配的,要想同時匹配多個字元的話,還需要藉助限定符。下面是一些常見的限定符 ( 下表中 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是“ /b/d{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 ),我們可以按照分組的方式來擷取。
代碼如下:
view plaincopy to clipboardprint?
- 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);
- }
- }
- }
private String text= @"00:41:23 GET /admin_save.asp 202.108.212.39 404 1468 17601:04:36 GET /userbuding.asp 202.108.212.39 404 1468 17610:00:59 GET /upfile_flash.asp 202.108.212.39 404 1468 17812:59:00 GET /cp.php 202.108.212.39 404 1468 16819:23:04 GET /sqldata.php 202.108.212.39 404 1468 17323:00:00 GET /Evil-Skwiz.htm 202.108.212.39 404 1468 17623: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 日誌的分析,我們採用命名擷取的群組的代碼如下:
view plaincopy to clipboardprint?
- 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"]);
- }
- }
private String text= @"00:41:23 GET /admin_save.asp 202.108.212.39 404 1468 17601:04:36 GET /userbuding.asp 202.108.212.39 404 1468 17610:00:59 GET /upfile_flash.asp 202.108.212.39 404 1468 17812:59:00 GET /cp.php 202.108.212.39 404 1468 16819:23:04 GET /sqldata.php 202.108.212.39 404 1468 17323:00:00 GET /Evil-Skwiz.htm 202.108.212.39 404 1468 17623: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 日誌的時候我們對用戶端提交請求的方式並不在乎,在這裡就可以使用非擷取的群組,如下:
view plaincopy to clipboardprint?
- 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})";
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賦予一些額外的選項之外,還可以在在運算式中使用這些選項,如:
view plaincopy to clipboardprint?
- Regex regex = new Regex("(?i)def");
Regex regex = new Regex("(?i)def");
它與下面一句是等效的:
view plaincopy to clipboardprint?
- 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 ,指定從模式中排除非轉義空白並啟用數字記號 (#) 後面的注釋。(有關轉義空白字元的列表,請參見字元轉義。) 請注意,空白永遠不會從字元類中消除。
舉例說明:
view plaincopy to clipboardprint?
- RegexOptions option=RegexOptions.IgnoreCase|RegexOptions.Singleline;
- Regex regex = new Regex("def", option);
RegexOptions option=RegexOptions.IgnoreCase|RegexOptions.Singleline;Regex regex = new Regex("def", option);
用內聯的形式表示為:
view plaincopy to clipboardprint?
- Regex regex = new Regex("(?is)def");
Regex regex = new Regex("(?is)def");
說明,其實關於Regex還有比較多的內容可以講,比如反向引用、匹配順序及幾種匹配模式的區別和聯絡等,不過這些在日常開發中使用不是太多(如果做文本分析處理還是會用到的),所以暫時不會繼續講了。儘管本系列四篇文章篇幅都不是太長(本人不敢太熬夜了,因為每天 5 點多就要起床),不過通過這些基礎的學習仍是可以掌握Regex的精華之處的,至於在開發中怎麼樣去用,就要靠我們自己靈活去結合實際情況用了。我個人經驗是如果是用於驗證是否滿足要求,那麼寫Regex時寫得嚴格一點,如果是從標準格式的文本中提取資料,則可以寫得寬鬆一點,比如驗證時間,則必須寫成
(?<time>(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2}) 這種形式,這樣別人輸入 26:99:99 就不能通過驗證,但是如果是像從上面提到的 IIS 日誌中提取時間,用 (?<time>/d{2}(:/d{2}){2}) 這種方式也是可以,當然如果寫比較嚴格的驗證比較麻煩時也可以寫比較寬鬆的格式,然後藉助其它手段來驗證,在網上有一個驗證日期的Regex,編寫者充分考慮到各個月份天數的不同、甚至平年和閏年 2 月份天數的不同的情況寫了一個相當複雜的Regex來驗證,個人覺得可以結合將文本值轉換成日期的方式來共同驗證,這樣更好理解和接受些。
到此,關於Regex的文章就暫時寫到這裡了,其它還有一些知識用得不是太多,以後有時間再總結了,接下來我可能要比較一下 ADO.NET 與 ORM 。