天天看點

C#正規表達式程式設計(四):正規表達式

正規表達式提供了功能強大、靈活而又高效的方法來處理文本。正規表達式的全面模式比對表示法使您可以快速分析大量文本以找到特定的字元模式;提取、編輯、替換或删除文本子字元串;或将提取的字元串添加到集合以生成報告。對于處理字元串(例如 HTML 處理、日志檔案分析和 HTTP 标頭分析)的許多應用程式而言,正規表達式是不可缺少的工具。正規表達式是一個非常有用的技術,有人曾稱之為能讓程式員不至于丢掉飯碗的十大技術之一,可見它的重要性。

熟悉DOS或者指令行的朋友或許已經用過類似的功能,比如我們要查找D盤下所有的低于Word2007版本的Word檔案(因為低于Word2007版本的Word檔案的檔案字尾是.doc,而Word2007版本的Word檔案的檔案字尾是.docx),我們可以在指令行下執行這個命名:

dir D:\*doc

當然如果想查找D盤下任意級子目錄下的所有此類檔案,就應該執行dir /s D:\*doc了。

注意正規表達式并不是在C#中獨有的東東,實際上在其它語言中早就實作了,比如Perl(可能很多人沒有聽說過這個程式設計語言,十年前大學期間我曾經學過一點皮毛),其它的程式設計語言Java、PHP及JavaScript等也支援正規表達式,正規表達式差不多像SQL語言一樣成為标準了,同樣和SQL類似,在不同的資料庫廠商那裡對SQL标準支援的程度并不完全一樣,正規表達式也是如此,大部分内的正規表達式可以跨語言使用,但是在各語言中也會有細微的差別,這一點是需要我們注意的。

正規表達式元字元

正規表達式語言由兩種基本字元類型組成:原義(正常)文本字元和元字元。元字元使正規表達式具有處理能力。元字元既可以是放在[]中的任意單個字元(如[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編碼表示漢字的)

正規表達式限定符

上面的元字元都是針對單個字元比對的,要想同時比對多個字元的話,還需要借助限定符。下面是一些常見的限定符(下表中n和m都是表示整數,并且0<n<m):

限定浮    說明

*    比對0到多個元字元,相當于{0,}

?    比對0到1個元字元,相當于{0,1}

{n}    比對n個元字元

{n,}    比對至少n個元字元

{n,m}    比對n到m個元字元

+    比對至少1個元字元,相當于{1,}

\b    比對單詞邊界

^    字元串必須以指定的字元開始

$    字元串必須以指定的字元結束

說明:

(1)由于在正規表達式中“\”、“?”、“*”、“^”、“$”、“+”、“(”、“)”、“|”、“{”、“[”等字元已經具有一定特殊意義,如果需要用它們的原始意義,則應該對它進行轉義,例如希望在字元串中至少有一個“\”,那麼正規表達式應該這麼寫:\\+。

(2)可以将多個元字元或者原義文本字元用括号括起來形成一個分組,比如^(13)[4-9]\d{8}$表示任意以13開頭的移動手機号碼。

(3)另外對于中文字元的比對是采用其對應的Unicode編碼來比對的,對于單個Unicode字元,如\u4e00表示漢字“一”, \u9fa5表示漢字“龥”,在Unicode編碼中這分别是所能表示的漢字的第一個和最後一個的Unicode編碼,在Unicode編碼中能表示20901個漢字。

(4)關于\b的用法,它代表單詞的開始或者結尾,以字元串“123a 345b 456 789d”作為示例字元串,如果正規表達式是“\b\d{3}\b”,則僅能比對456。

(5)可以使用“|”來表示或的關系,例如[z|j|q]表示比對z、j、q之中的任意一個字母。

正規表達式分組

将正規表達式的一部分用()括起來就可以形成一個分組,也叫一個子比對或者一個捕獲組。例如對于“08:14:27”這樣格式的時間,我們可以寫如下的正規表達式:

((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

23:59:59 GET /bil.html 202.108.212.39 404 1468 170";

/// <summary>

/// 分析IIS日志,提取用戶端通路的時間、URL、IP位址及伺服器響應代碼

/// </summary>

public void AnalyzeIISLog()

{

        //提取通路時間、URL、IP位址及伺服器響應代碼的正規表達式

        //大家可以看到關于提取時間部分的子表達式比較複雜,因為做了比較嚴格的時間比對限制

        //注意為了簡化起見,沒有對用戶端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[5]=/userbuding.asp

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[5]=/upfile_flash.asp

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[5]=/cp.php

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[5]=/sqldata.php

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[5]=/Evil-Skwiz.htm

Match[6]========================

Groups[0]=23:59:59 GET /bil.html 202.108.212.39 404

Groups[1]=23:59:59

Groups[5]=/bil.html

從上面的輸出結果中我們可以看出在每一個比對結果中,第2個分組就是用戶端通路時間(因為索引是從0開始的,是以索引順序為1,以下同理),第6個分組是通路的URL(索引順序為6),第7個分組是用戶端IP(索引順序為6),第9個分組是伺服器端響應代碼(索引順序為9)。如果我們要提取這些元素,可以直接按照索引來通路這些值就可以了,這樣比我們不采用正規表達式要友善多了。

命名捕獲組

上面的方法盡管友善,但也有一些不便之處:假如需要提取更多的資訊,對捕獲組進行了增減,就會導緻捕獲組索引對應的值發生變化,我們就需要重新修改代碼,這也算是一種寫死吧。有沒有比較好的辦法呢?答案是有的,那就是采用命名捕獲組。

就像我們使用DataReader通路資料庫或者通路DataTable中的資料一樣,可以使用索引的方式(索引同樣也是從0開始),不過如果變化了select語句中的字段數或者字段順序,按照這種方式擷取資料就需要重新變動,為了适應這種變化,同樣也允許使用字段名作為索引來通路資料,隻要資料源中存在這個字段而不管順序如何都會取到正确的值。在正規表達式中命名捕獲組也可以起到同樣的作用。

普通捕獲組表示方式:(正規表達式),如(\d{8,11});

命名捕獲組表示方式:(?<捕獲組命名>正規表達式),如(?<phone>\d{8,11})

對于普通捕獲組隻能采用索引的方式擷取它對應的值,但對于命名捕獲組,還可以采用按名稱的方式通路,例如(?<phone>\d{8,11}),在代碼中就可以按照match.Groups["phone"]的方式通路,這樣代碼更直覺,編碼也更靈活,針對剛才的對IIS日志的分析,我們采用命名捕獲組的代碼如下:

/// 采用命名捕獲組提取IIS日志裡的相關資訊

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);

                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"]);

這段代碼的執行效果如下:

time:00:41:23

url:/admin_save.asp

ip:202.108.212.39

httpCode:404

time:01:04:36

url:/userbuding.asp

time:10:00:59

url:/upfile_flash.asp

time:12:59:00

url:/cp.php

time:19:23:04

url:/sqldata.php

time:23:00:00

url:/Evil-Skwiz.htm

time:23:59:59

url:/bil.html

采用命名捕獲組之後使通路捕獲組的值更直覺了,而且隻要命名捕獲組的值不發生變化,其它的變化都不影響原來的代碼。

非捕獲組

如果經常看别人有關正規表達式的源代碼,可能會看到形如(?: 子表達式)這樣的表達式,這就是非捕獲組,對于捕獲組我們可以了解,就是在後面的代碼中可以通過索引或者名稱(如果是命名捕獲組)的方式來通路比對的值,因為在比對過程中會将對應的值儲存到記憶體中,如果我們在後面不需要通路比對的值那麼就可以告訴程式不用在記憶體中儲存比對的值,以便提高效率減少記憶體消耗,這種情況下就可以使用非捕獲組,例如在剛剛分析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比對。

正規表達式選項

在使用正規表達式時除了使用RegexOptions這個枚舉給正規表達式賦予一些額外的選項之外,還可以在在表達式中使用這些選項,如:

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)這種形式的稱之為内聯模式,顧名思義就是在正規表達式中已經展現了正規表達式選項,這些内聯字元與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");

說明,其實關于正規表達式還有比較多的内容可以講,比如反向引用、比對順序及幾種比對模式的差別和聯系等,不過這些在日常開發中使用不是太多(如果做文本分析處理還是會用到的),是以暫時不會繼續講了。盡管本系列四篇文章篇幅都不是太長(本人不敢太熬夜了,因為每天5點多就要起床),不過通過這些基礎的學習仍是可以掌握正規表達式的精華之處的,至于在開發中怎麼樣去用,就要靠我們自己靈活去結合實際情況用了。我個人經驗是如果是用于驗證是否滿足要求,那麼寫正規表達式時寫得嚴格一點,如果是從規範格式的文本中提取資料,則可以寫得寬松一點,比如驗證時間,則必須寫成(?<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})這種方式也是可以,當然如果寫比較嚴格的驗證比較麻煩時也可以寫比較寬松的格式,然後借助其它手段來驗證,在網上有一個驗證日期的正規表達式,編寫者充分考慮到各個月份天數的不同、甚至平年和閏年2月份天數的不同的情況寫了一個相當複雜的正規表達式來驗證,個人覺得可以結合将文本值轉換成日期的方式來共同驗證,這樣更好了解和接受些。

到此,關于正規表達式的文章就暫時寫到這裡了,其它還有一些知識用得不是太多,以後有時間再總結了,接下來我可能要比較一下ADO.NET與ORM。

本文轉自周金橋51CTO部落格,原文連結: http://blog.51cto.com/zhoufoxcn/283021,如需轉載請自行聯系原作者