1、Regex簡介
Regex提供了功能強大、靈活而又高效的方法來處理文本。Regex的全面模式比對標記法可以快速地分析大量的文本以找到特定的字元模式;提取、編輯、替換或刪除文本子字串;或將提取的字串添加到集合以產生報告。對於處理字串(例如 HTML 處理、記錄檔分析和 HTTP 標題分析)的許多應用程式而言,Regex是不可缺少的工具。
.NET 架構Regex併入了其他Regex實現的最常見功能,被設計為與 Perl 5 Regex相容,.NET 架構Regex還包括一些在其他實現中尚未提供的功能,.NET 架構Regex類是基底類別庫的一部分,並且可以和面向公用語言運行庫的任何語言或工具一起使用。
2、字串搜尋
Regex語言由兩種基底字元類型組成:原義(正常)文本字元和元字元。正是元字元組為Regex提供了處理能力。當前,所有的文字編輯器都有一些搜尋功能,通常可以開啟一個對話方塊,在其中的一個文字框中鍵入要定位的字串,如果還要同時進行替換操作,可以鍵入一個替換字串,比如在Windows作業系統中的記事本、Office系列中的文檔編輯器都有這種功能。這種搜尋最簡單的方式,這類問題很容易用String類的String.Replace()方法來解決,但如果需要在文檔中識別某個重複的,該怎麼辦?編寫一個常式,從一個String類中選擇重複的字是比較複雜的,此時使用語言就很適合。
一般運算式語言是一種可以編寫搜尋運算式的語言。在該語言中,可以把文檔中要搜尋的文本、逸出序列和特定含義的其他字元組合在一起,例如序列\b表示一個字的開頭和結尾(子的邊界),如果要表示正在尋找的以字元th開頭的字,就可以編寫一般運算式\bth(即序列字元界是-t-h)。如果要搜尋所有以th結尾的字,就可以編寫th\b(序列t-h-字邊界)。但是,一般運算式要比這複雜得多,例如,可以在搜尋操作中找到儲存部分文本的工具性程式(facility)。
3、.NET 架構的Regex類
下面通過介紹 .NET 架構的Regex類,熟悉一下.NET架構下的Regex的使用方法。
3.1 Regex 類表示唯讀Regex
Regex 類包含各種靜態方法,允許在不顯式執行個體化其他類的對象的情況下使用其他Regex類。以下程式碼範例建立了 Regex 類的執行個體並在初始化對象時定義一個簡單的Regex。請注意,使用了附加的反斜線作為逸出字元,它將 \s 匹配字元類中的反斜線指定為原義字元。
Regex r; // 聲明一個 Regex類的變數 r = new Regex("\\s2000"); // 定義運算式 |
3.2 Match 類表示Regex匹配操作的結果
以下樣本使用 Regex 類的 Match 方法返回 Match 類型的對象,以便找到輸入字串中第一個匹配。此樣本使用 Match 類的 Match.Success 屬性來指示是否已找到匹配。
Regex r = new Regex("abc"); // 定義一個Regex對象執行個體 Match m = r.Match("123abc456"); // 在字串中匹配 if (m.Success) { Console.WriteLine("Found match at position " + m.Index); //輸入匹配字元的位置 } |
3.3 MatchCollection 類表示非重疊匹配的序列
該集合為唯讀,並且沒有公用建構函式。MatchCollection 的執行個體是由 Regex.Matches 屬性返回的。使用 Regex 類的 Matches 方法,通過在輸入字串中找到的所有匹配填充 MatchCollection。下面程式碼範例示範了如何將集合複製到一個字串數組(保留每一匹配)和一個整數數組(指示每一匹配的位置)中。
MatchCollection mc; String[] results = new String[20]; int[] matchposition = new int[20]; Regex r = new Regex("abc"); //定義一個Regex對象執行個體 mc = r.Matches("123abc4abcd"); for (int i = 0; i < mc.Count; i++) //在輸入字串中找到所有匹配 { results[i] = mc[i].Value; //將匹配的字串添在字串數組中 matchposition[i] = mc[i].Index; //記錄匹配字元的位置 } |
3.4 GroupCollection 類表示捕獲的組的集合
該集合為唯讀,並且沒有公用建構函式。GroupCollection 的執行個體在 Match.Groups 屬性返回的集合中返回。下面的控制台應用程式尋找並輸出由Regex捕獲的組的數目。
using System; using System.Text.RegularExpressions; public class RegexTest { public static void RunTest() { Regex r = new Regex("(a(b))c"); //定義組 Match m = r.Match("abdabc"); Console.WriteLine("Number of groups found = " + m.Groups.Count); } public static void Main() { RunTest(); } } |
該樣本產生下面的輸出:
Number of groups found = 3 |
3.5 CaptureCollection 類表示捕獲的子字串的序列
由於限定符,擷取的群組可以在單個匹配中捕獲多個字串。Captures屬性(CaptureCollection 類的對象)是作為 Match 和 group 類的成員提供的,以便於對捕獲的子字串的集合的訪問。例如,如果使用Regex ((a(b))c)+(其中 + 限定符指定一個或多個匹配)從字串"abcabcabc"中捕獲匹配,則子字串的每一匹配的 Group 的 CaptureCollection 將包含三個成員。
下面的程式使用Regex (Abc)+來尋找字串"XYZAbcAbcAbcXYZAbcAb"中的一個或多個匹配,闡釋了使用 Captures 屬性來返回多組捕獲的子字串。
using System; using System.Text.RegularExpressions; public class RegexTest { public static void RunTest() { int counter; Match m; CaptureCollection cc; GroupCollection gc; Regex r = new Regex("(Abc)+"); //尋找"Abc" m = r.Match("XYZAbcAbcAbcXYZAbcAb"); //設定要尋找的字串 gc = m.Groups; //輸出尋找組的數目 Console.WriteLine("Captured groups = " + gc.Count.ToString()); // Loop through each group. for (int i=0; i < gc.Count; i++) //尋找每一個組 { cc = gc[i].Captures; counter = cc.Count; Console.WriteLine("Captures count = " + counter.ToString()); for (int ii = 0; ii < counter; ii++) { // Print capture and position. Console.WriteLine(cc[ii] + " Starts at character " + cc[ii].Index); //輸入捕獲位置 } } } public static void Main() { RunTest(); } } |
此例返回下面的輸出結果:
Captured groups = 2 Captures count = 1 AbcAbcAbc Starts at character 3 Captures count = 3 Abc Starts at character 3 Abc Starts at character 6 Abc Starts at character 9 |
3.6 Capture 類包含來自單個子運算式捕獲的結果
在 Group 集合中迴圈,從 Group 的每一成員中提取 Capture 集合,並且將變數 posn 和 length 分別分配給找到每一字串的初始字串中的字元位置,以及每一字串的長度。
Regex r; Match m; CaptureCollection cc; int posn, length; r = new Regex("(abc)*"); m = r.Match("bcabcabc"); for (int i=0; m.Groups[i].Value != ""; i++) { cc = m.Groups[i].Captures; for (int j = 0; j < cc.Count; j++) { posn = cc[j].Index; //捕獲對象位置 length = cc[j].Length; //捕獲對象長度 } } |
把組合字元組合起來後,每次都會返回一個組對象,就可能並不是我們希望的結果。如果希望把組合字元作為搜尋模式的一部分,就會有相當大的系統開銷。對於單個的組,可以用以字元序列"?:"開頭的組禁止這麼做,就像URI範例那樣。而對於所有的組,可以在RegEx.Matches()方法上指定RegExOptions.ExplicitCapture標誌。
4、利用Regex實現字串搜尋
4.1 在C#中使用.NET一般運算式引擎
下面將通過一個範例的開發,執行並顯示一些搜尋的結果,說明一般運算式的一些特性,以及如何在C#中使用.NET一般運算式引擎。說明使用字串時應在前面加上符號@。
String Text=@"I can not find my position in Beijing"; |
把這個文本稱為輸入字串,為了說明一般運算式.NET類,本文先進行一次純文字的搜尋,這次搜尋不帶任何逸出序列或一般運算式命令。假定要尋找所有字串ion,把這個搜尋字串稱為模式。使用一般運算式和上面聲明的變數Text,編寫出下面的代碼:
String Pattern = "ion"; MatchCollection Matches = Regex.Matches(Text,Pattern,RegexOptions); foreach(Match NextMatch in Matches) { Console.WriteLine(NextMatch.Index); } |
在這段代碼中,使用了System.Text.RegularExpressions名稱空間中Regex類的靜態方法Match()。這個方法的參數是一些輸入文本、一個模式和RegexOptions每句中的一組可選標誌。Matches()返回MatchCollection,每個匹配都用一個Match對象來表示。在上面的代碼中,只是在集合中迭代,使用Match類的Index屬性,返回輸入文本中匹配所在的索引。運行這段代碼,將得到1個匹配項。
一般集合的功能主要取決於模式字串。原因是模式字串不僅僅包含純文字。如前所述。還包含元字元和逸出序列,元字元是給出命令的特殊字元,而逸出序列的工作方式與C#的逸出序列相同,它們都是以反斜線\開頭的字元,具有特殊的含義。例如,假定要尋找以n開頭的字,就可以使用逸出序列\b,它表示一個字的邊界(字的邊界是以某個字母數字標的字元開頭,或者後面是一個空白字元或標點符號),下面編寫如下代碼:
String Pattern = @"\bn"; MatchCollection Matches = Regex.Matches(Text,Pattern,RegexOptions.IgnoreCase| RegexOptions.ExplicitCapture); |
要在運行時把\b傳遞給.NET一般運算式引擎,反斜線\不應被C#編譯器解釋為逸出序列。如果要尋找以序列ion結尾的字,可以使用下面的代碼:
String Pattern = @"ion\b"; |
如果要尋找以字母n開頭,以序列ion結尾的所有字,需要一個以\bn開頭,以ion\b結尾的模式,中間內容怎麼辦?需要告訴電腦n和ion中間的內容可以是任意長度的字元,只要字元不是空白即可,正確的模式如下所示:
String Pattern = @"\bn\S*ion\b"; |
4.2 特定字元或逸出序列
大多數重要的Regex語言運算子都是非轉義的單個字元。轉義符 \(單個反斜線)通知Regex分析器反斜線後面的字元不是運算子。例如,分析器將星號 (*) 視為重複限定符,而將後跟星號的反斜線 (\*) 視為 Unicode 字元 002A。
使用一般運算式要習慣的一點是,查看像這樣怪異的字元序列,但這個序列的工作是非常邏輯化的。逸出序列\S表示任何不適空白的字元。*稱為數量詞,其含義是前面的字元可以重複任意次,包括0次。序列\S*表示任何不適空白的字元。因此,上面的模式比對於以n開頭,以ion結尾的任何單個字。下表中列出的字元轉義在Regex和替換模式中都會被識別。
表1:特定字元或逸出序列
特定字元或逸出序列 |
含義 |
範例 |
匹配的範例 |
^ |
輸入文本的開頭 |
^B |
B,但只能是文本中的第一個字元 |
$ |
輸入文本的結尾 |
X$ |
X,但只能是文本中的最後一個字元 |
. |
除了換行字元(\n)以外的所有單個字元 |
i.ation |
isation、ization |
* |
可以重複0次或多次的前置字元 |
ra*t |
rat、raat等 |
+ |
可以重複1次或多次的前置字元 |
ra+t |
rt、rat、raat等 |
? |
可以重複0次或1次的前置字元 |
ra?t |
只有rt和rat匹配 |
\s |
任何空白字元 |
\sa |
[space]a,\ta,\na(\t和\n與C#的\t和\n含義相同) |
\S |
任何不是空白的字元 |
\SF |
aF,rF,cF,但不能是\tf |
\b |
字邊界 |
ion\b |
以ion結尾的任何字 |
\B |
不是字邊界的位置 |
\BX\B |
字中間的任何X |
如果要搜尋一個元字元,也可以通過帶有反斜線的逸出字元來表示。例如,.表示除了換行字元以外的任何字元,而\.表示一個點。
可以把可替換的字元放在方括弧中,請求匹配包含這些字元。例如,[1|c]表示字元可以是1或者是c。如果要搜尋map或者man,可以使用序列"ma[n|p]"(僅指引號內字元,下面雷同)。在方括弧中,也可以制定一個範圍,例如"[a-z]"表示所有的小寫字母(使用連字號 (-) 允許指定連續字元範圍),"[B-F]"表示B到F之間的所有大寫字母,"[0-9]"表示一個數字,如果要搜尋一個整數(該序列只包含0到9的字元),就可以編寫"[0-9]+"(注意,使用+字元表示至少要有這樣一個數字,但可以有多個數字,所以9、83和3443等都是匹配的。)
下面看看一般運算式的結果,編寫一個執行個體RegularExpressionsZzy。建立幾個一般運算式,顯示其結果,讓使用者瞭解一下運算式是如何工作的。
該執行個體的核心是一個方法WriteMatches(),它把MatchCollection中的所有匹配以比較詳細的方式顯示出來。對於每個匹配,它都會顯示該匹配在輸入字串中所在的索引,匹配的字串和一個略長的字串,其中包含輸入文本中至多8個外圍字元,其中至少有5個字元放在匹配的前面,至多5個字元放在匹配的後面(如果匹配的位置在輸入文本的開頭或結尾5個字元內,則結果中匹配前後的字元就會少於4個)。換言之,靠近輸入文本末尾的匹配應是"and messaging ofd",匹配的前後各有5個字元,但位於輸入文本的最後一個字上的匹配就應是"g of data",匹配的字後只有一個字元。因為在該字元的後面是字串的結尾。這個長字串可以更清楚地表明一般運算式是在什麼地方尋找到匹配的:
static void WriteMatches(string text, MatchCollection matches) { Console.WriteLine("Original text was: \n\n" + text + "\n"); Console.WriteLine("No. of matches: " + matches.Count); foreach (Match nextMatch in matches) { int Index = nextMatch.Index; string result = nextMatch.ToString(); int charsBefore = (Index < 5) ? Index : 5; int fromEnd = text.Length - Index - result.Length; int charsAfter = (fromEnd < 5) ? fromEnd : 5; int charsToDisplay = charsBefore + charsAfter + result.Length; Console.WriteLine("Index: {0}, \tString: {1}, \t{2}",Index, result, text.Substring(Index - charsBefore, charsToDisplay)); } } |
在這個方法中,處理過程是確定在較長的字串中有多少個字元可以顯示,而無需超限輸入文本的開頭或結尾。注意在Match對象上使用了另一個屬性Value,它包含標識該匹配的字串,而且,RegularExpressionsZzy只包含名為Find_po,Find_n等的方法,這些方法根據本文執行某些搜尋操作。
4.3 Regex選項
可以使用影響匹配行為的選項修改Regex模式。可以通過兩種基本方法設定Regex選項:其一是可以在 Regex(pattern, options) 建構函式中的 options 參數中指定,其中 options 是 RegexOptions 枚舉值的按位"或"組合;其二是使用內聯 (?imnsx-imnsx:) 分組構造或 (?imnsx-imnsx) 其他構造在Regex模式內設定它們。
在內聯選項構造中,一個選項或一組選項前面的減號 (-) 用於關閉這些選項。例如,內聯構造 (?ix-ms) 將開啟 IgnoreCase 和 IgnorePatternWhiteSpace 選項而關閉 Multiline 和 Singleline 選項。
表2:RegexOptions 枚舉的成員以及等效的內聯選項字元
RegexOption 成員 |
內聯字元 |
說明 |
None |
無 |
指定不設定任何選項。 |
IgnoreCase |
i |
指定不區分大小寫匹配。 |
Multiline |
m |
指定多行模式。更改 ^ 和 $ 的含義,以使它們分別與任何行的開頭和結尾匹配,而不只是與整個字串的開頭和結尾匹配。 |
ExplicitCapture |
n |
指定唯一有效捕獲是顯式命名或編號的 (?<name>...) 形式的組。這允許圓括弧充當非擷取的群組,從而避免了由 (?:...) 導致的文法上的笨拙。 |
Compiled |
無 |
指定Regex將被編譯為程式集。產生該Regex的 Microsoft 中繼語言 (MSIL) 代碼;以較長的啟動時間為代價,得到更快的執行速度。 |
Singleline |
s |
指定單行模式。更改句點字元 (.) 的含義,以使它與每個字元(而不是除 \n 外的所有字元)匹配。 |
IgnorePatternWhitespace |
x |
指定從模式中排除非轉義空白並啟用數字記號 (#) 後面的注釋。請注意,空白永遠不會從字元類中消除。 |
RightToLeft |
無 |
指定搜尋是從右向左而不是從左向右進行的。具有此選項的Regex將移動到起始位置的左邊而不是右邊。(因此,起始位置應指定為字串的結尾而不是開頭。)為了避免構造具有無限迴圈的Regex的可能性,此選項不能在中流指定。但是,(?<) 回顧後發構造提供了可用作子運算式的類似替代物。 |
ECMAScript |
無 |
指定已為運算式啟用了符合 ECMAScript 的行為。此選項僅可與 IgnoreCase 和 Multiline 標誌一起使用。將 ECMAScript 同任何其他標誌一起使用將導致異常。 |
例如,Find_po在字開頭處尋找以"po"開頭的字串:
static void Find_po() { string text = @" I can not find my position in Beijing "; string pattern = @"\bpo\S*ion\b"; MatchCollection matches = Regex.Matches(text, pattern, RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.ExplicitCapture); WriteMatches(text, matches); } |
這段代碼還使用了名稱空間RegularExpressions:
using System; using System.Text.RegularExpressions; |
4.4 匹配、組和捕獲
一般運算式的一個很好的特性是可以把字元組合起來,方式與C#中的複合陳述式一樣。在C#中,可以通過把任意數量的語句放在花括弧中的方式把它們組合在一起。其結果就像一個複合陳述式那樣。在一般運算式模式中,也可以把任何字元組合起來(包括元字元和逸出序列),像處理一個字元那樣處理它們。唯一的區別是要使用圓括弧,而不是花括弧,得到的序列成為一個組。
例如,模式"(an)+"定位序列an的任以重複。量詞+只應用於它前面的一個字元,但因為我們把字元組合起來了,所以它現在把重複的an作為一個單元來對待。"(an)."應用到輸入文本"bananas came to Europe late in the annals of history"上,會從bananas中選擇出anan。另一方面,如果使用an+,則將從annals中選擇ann,從bananas中選擇出兩個an。為什麼(an)+選擇的是anan,而沒有把單個的an作為一個匹配。匹配規則是不能重複的,如果有可能重複,在預設情況下就選擇較長的匹配。
但是,組的功能要比這強大得多。在預設情況下,把模式的一部分組合為一個組時,就要求一般運算式引擎記住可以按照這個組來匹配,也可以按照整個模式來匹配。換言之,可以把組當作一個要匹配的模式,如果要把字串分解為各個部分,這種模式就是非常有效。
例如,URI的格式是"<protocol>://<address>:<port>",其中連接埠是可選的。它的一個範例是http://www.comprg.com.cn:8080。假定要從一個URI中提取協議、地址和連接埠,而且緊鄰URI的後面可能有空白(但沒有標點符號),就可以使用下面的運算式:"\b(\S+)://(\S+)(?::(\S+))?\b"
該運算式的工作方式如下:首先,前置和尾部的\b序列確保只需要考慮完全是字的文本部分,在這個文本部分中,第一組"(\S+)://"會選擇一個或多個不適空白的字元,其後是"://"。在HTTPURI的開頭會選擇出http://。花括弧表示把http儲存為一個組。後面的"(\S+)"則在上述URI中選擇www. comprg.com.cn,這個組在遇到詞的結尾時或標記另一個組的冒號"(:)"時結束。
下一個組選擇連接埠(本例是:8080)。後面的?表示這個組在匹配中是可選的,如果沒有:xxxx,也不會妨礙匹配的標記。
這是非常重要的,因為連接埠在URI中一般不指定,實際上,在大多數情況下,URI是沒有連接埠號碼的。但是,事情會比較複雜。如果要求冒號可以出現,也可以不出現,但不希望把這個冒號也儲存在組中。為此,可以嵌套兩個組:內部的"(\S+)"組選擇冒號後面的內容(本例中是8080),外面的組包含內部的組,後面是一個冒號,該冒號又在序列"?:"的後面。這個序列表示該組不應儲存(只需要儲存"8080",不需要儲存":8080")。不要把這兩個冒號混淆了,第一個冒號是序列"?:"的一部分,表示不儲存這個組,第二個冒號是要搜尋的文本。
在這個字串上運行該模式:I always visit http://www. comprg.com.cn 得到的匹配是http://www. comprg.com.cn。在這個匹配中,僅提到了三個組,還有第四個組表示匹配本身。理論上,每個組都可以選擇0次、1次或者多次匹配。單個的匹配就稱為捕獲。在第一個組"(\S+)",有一個捕獲http。第二個組也有一個捕獲www. comprg.com.cn,但第三個組沒有捕獲,因為在這個URI中沒有連接埠號碼。注意該字串在其本身上包含第二個http://。雖然它匹配於第一個組,但不會被搜尋出來,因為整個搜尋運算式不匹配於這部分文本。
再比如下面這個例子,以下程式碼範例使用 Match.Result 來從 URL提取協議和連接埠號碼。例如,"http://www.yahoo.com.cn:8080/index.html"將返回"http:8080"。
String Extension(String url) { Regex r = new Regex(@"^(?<proto>\w+)://[^/]+?(?<port>:\d+)?/", RegexOptions.Compiled); return r.Match(url).Result("${proto}${port}"); } |
5、小結
.NET 架構Regex類是基底類別庫的一部分,並且可以和面向公用語言運行庫的任何語言或工具(包括 ASP.NET 和 Visual Studio .NET)一起使用。本文給出了在C#下利用Regex實現字串搜尋功能的方法,通過對.NET架構下的Regex的研究及執行個體分析,總結了Regex的規則、選項等,方便以後朋友們的應用。