1 概述
捕獲組捕獲到的內容,不僅可以在正則表達式外部通過程序進行引用,也可以在正則表達式內部進行引用,這種引用方式就是反向引用。要了解反向引用,首先要了解捕獲組,關於捕獲組,參考 正則基礎之——捕獲組(capture group)。
反向引用的作用通常是用來查找或限定重復、查找或限定指定標識配對出現等等。
對於普通捕獲組和命名捕獲組的引用,語法如下:
普通捕獲組反向引用:\k<number>,通常簡寫為\number
命名捕獲組反向引用:\k<name>或者\k'name'
普通捕獲組反向引用中number是十進制的數字,即捕獲組的編號;命名捕獲組反向引用中的name為命名捕獲組的組名。
2 反向引用匹配原理
捕獲組(Expression)在匹配成功時,會將子表達式匹配到的內容,保存到內存中一個以數字編號的組里,可以簡單的認為是對一個局部變量進行了賦值,這時就可以通過反向引用方式,引用這個局部變量的值。一個捕獲組(Expression)在匹配成功之前,它的內容可以是不確定的,一旦匹配成功,它的內容就確定了,反向引用的內容也就是確定的了。
反向引用必然要與捕獲組一同使用的,如果沒有捕獲組,而使用了反向引用的語法,不同語言的處理方式不一致,有的語言會拋異常,有的語言會當作普通的轉義處理。
2.1 從一個簡單例子說起
源字符串:abcdebbcde
正則表達式:([ab])\1
對於正則表達式“([ab])\1”,捕獲組中的子表達式“[ab]”雖然可以匹配“a”或者“b”,但是捕獲組一旦匹配成功,反向引用的內容也就確定了。如果捕獲組匹配到“a”,那么反向引用也就只能匹配“a”,同理,如果捕獲組匹配到的是“b”,那么反向引用也就只能匹配“b”。由於后面反向引用“\1”的限制,要求必須是兩個相同的字符,在這里也就是“aa”或者“bb”才能匹配成功。
考察一下這個正則表達式的匹配過程,在位置0處,由“([ab])”匹配“a”成功,將捕獲的內容保存在編號為1的組中,然后把控制權交給“\1”,由於此時捕獲組已記錄了捕獲內容為“a”,“\1”也就確定只有匹配到“a”才能匹配成功,這里顯然不滿足,“\1”匹配失敗,由於沒有可供回溯的狀態,整個表達式在位置0處匹配失敗。
正則引擎向前傳動,在位置5之前,“([ab])”一直匹配失敗。傳動到位置5處時,,“([ab])”匹配到“b”,匹配成功,將捕獲的內容保存在編號為1的組中,然后把控制權交給“\1”,由於此時捕獲組已記錄了捕獲內容為“b”,“\1”也就確定只有匹配到“b”才能匹配成功,滿足條件,“\1”匹配成功,整個表達式匹配成功,匹配結果為“bb”,匹配開始位置為5,結束位置為7。
擴展一下,正則表達式“([a-z])\1{2}”也就表達連續三個相同的小寫字母。
2.2 一個復雜例子的分析
詳細的分析討論參考:正則表達式正向預搜索的問題。
源字符串:aaa bbbb ffffff 999999999
正則表達式:(\w)((?=\1\1\1)(\1))+
測試代碼:
string test = "aaa bbbb ffffff 999999999";
Regex reg = new Regex(@"(\w)((?=\1\1\1)(\1))+");
MatchCollection mc = reg.Matches(test);
foreach (Match m in mc)
{
richTextBox2.Text += "匹配結果:" + m.Value.PadRight(12, ' ') + "匹配開始位置:" + m.Index + "\n";
}
//輸出
匹配結果:bb 匹配開始位置:4
匹配結果:ffff 匹配開始位置:9
匹配結果:9999999 匹配開始位置:16
匹配結果分析:
正則表達式(\w)((?=\1\1\1)(\1))+從匹配結果上分析,其實就等價於 (\w)(\1)*(?=\1\1\1)(\1) ,這個會相對好理解一些,下面討論下分析過程。
因為“+”等價於“{1,}”,表示至少匹配1次,下面把子表達式“((?=\1\1\1)(\1))+”展開來看下規律,下表中的“次數”表示子表達式“((?=\1\1\1)(\1))+”匹配成功的次數 。
次數 |
等價表達式 |
1 |
(\w)((?=\1\1\1)(\1)) |
2 |
(\w)((?=\1\1\1)(\1))((?=\1\1\1)(\1)) |
3 |
(\w)((?=\1\1\1)(\1))((?=\1\1\1)(\1))((?=\1\1\1)(\1)) |
… |
… |
如果最后一個“((?=\1\1\1)(\1))”匹配成功,那么中間的“((?=\1\1\1)(\1))”一定可以匹配成功,所以中間的限制條件(?=\1\1\1)就沒有意義了,這時就可以簡寫為“(\1)”,也就是
次數 |
等價表達式 |
1 |
(\w)((?=\1\1\1)(\1)) |
2 |
(\w)(\1)((?=\1\1\1)(\1)) |
3 |
(\w)(\1)(\1)((?=\1\1\1)(\1)) |
… |
… |
可以歸納為等價於
(\w)(\1)*((?=\1\1\1)(\1))
因為“((?=\1\1\1)(\1))”開始和結尾的()原來是用作量詞+修飾范圍的,這里已經沒有什么意義了,所以表達式最后可以歸納為等價於
(\w)(\1)*(?=\1\1\1)(\1)
分析這個表達式就容易多了。“(\w)”匹配一個字符,占一位,“\1”是對“\w”匹配內容的引用,“(\1)*”可以匹配0到無窮多個“(\w)”匹配到的字符,“(?=\1\1\1)(\1)”只占一位,但是“(?=\1\1\1)”要求所在位置右側有三個連續相同的“(\w)”匹配到的字符,所以在“(?=\1\1\1)”這個位置右側應該有三個字符,不過只有這個位置右側的一個字符計入最后的匹配結果,最后兩個只作為限制條件,不計入最后的匹配結果 。
以“999999999”為例,第一個“9”由“(\w)”匹配,第二到第六個“9”由“(\1)*”來匹配,第七個“9”由“(?=\1\1\1)(\1)”中最后的“(\1)”來匹配,而第七、八、九這三個“9”是用來保證滿足“(?=\1\1\1)”這個條件的。
2.3 反向引用的編號
對於普通捕獲組的反向引用,是通過捕獲組的編號來實現的,那么對於一些可能存在歧義的語法又是如何解析的呢?對於正則表達式
([ab])\10
這里的“\10”會被解析成第10個捕獲組的反向引用,還是第1個捕獲組的反向引用加一個普通字符“0”呢?不同語言的處理方式是不一樣的。
string test = "ab0cdebb0cde";
richTextBox2.Text = Regex.Match(test, @"([ab])\10").Value;
在.NET中,以上測試代碼輸出為空,說明這里的“\10”被解析成第10個捕獲組的反向引用,而這個表達式中是不存在第10個捕獲組的,所以匹配結果為空。
<script type="text/javascript">
var str = "ab0cdebb0cde";
var reg = /([ab])\10/;
var arr = str.match(reg);
if(arr != null)
{
document.write(arr[0]);
}
</script>
/*--------輸出--------
bb0
*/
而在JavaScript中,由於瀏覽器解析引擎的不同,得到的結果也不一樣,以上為IE下是可以得到匹配結果“bb0”,說明在IE的瀏覽器引擎中,“\10”被解析成第1個捕獲組的反向引用加一個普通字符“0”。而在Firefox、Opera等瀏覽器中,得到的結果為空,說明“\10”被解析成第10個捕獲組的反向引用,而這個表達式中是不存在第10個捕獲組的。
string test = "ab0cdebb0cde";
richTextBox2.Text = Regex.Match(test, @"([ab])\10", RegexOptions.ECMAScript).Value;
/*--------輸出--------
bb0
*/
而在.NET中,如果正則表達式加了RegexOptions.ECMAScript參數,則這里的“\10”被解析成第1個捕獲組的反向引用加一個普通字符“0”。
至於正則表達式中確實有10個以上的捕獲組時,“\10”的具體意義留給有興趣的讀者去測試了,因為在實際應用當中,如果你的正則表達式中用到了10個以上捕獲組,而同時又用到了第10個以上捕獲組的反向引用時,就要注意分析一下,你的正則是否需要進行優化,甚至於這里是否適合使用正則表達式了。
出於對現實應用場景的分析,第10個以上捕獲組的反向引用幾乎不存在,對它的研究通常僅存在於理論上。而對於10個以內捕獲組反向引用后面還有數字,容易造成混淆的情況,可以通過非捕獲組來解決。
([ab])\1(?:0)
這樣就可以明確,是對第1個捕獲組的反向引用,后面跟一個普通字符“0”。也就不會產生混淆了。
string test = "ab0cdebb0cde";
richTextBox2.Text = Regex.Match(test, @"([ab])\1(?:0)").Value;
/*--------輸出--------
bb0
*/
而事實上,即使是這樣用的場景也非常少,至今為止,只在日期正則表達式中用到過。
^(?:(?!0000)[0-9]{4}([-/.]?)(?:(?:0?[1-9]|1[0-2])\1(?:0?[1-9]|1[0-9]|2[0-8])|(?:0?[13-9]|1[0-2])\1(?:29|30)|(?:0?[13578]|1[02])\1(?:31))|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)([-/.]?)0?2\2(?:29))$
這一節討論的內容,了解一下就可以了,在實際應用當中,如果遇到,注意一下不要出現混淆而導致匹配結果錯誤就可以了。
3 反向引用應用場景分析
反向引用的作用通常是用來查找或限定重復、查找或限定指定標識配對出現等等。以下以實例進行場景分析及應用講解。
3.1 查找重復
查找重復通常的應用場景是查找或驗證源字符串中,是否有重復單詞、重復項等等。
3.1.1 驗證數字元素重復項
需求描述:
驗證源字符串中以“,”分隔的數字是否有重復項。
代碼實現:
string[] test = new string[] { "1,2,3,123,32,13", "12,56,89,123,56,98", "8,2,9,10,38,29,2,9", "8,3,9,238,93,23" };
Regex reg = new Regex(@"\b(\d+)\b.*?\b\1\b");
foreach (string s in test)
{
richTextBox2.Text += "源字符串: " + s.PadRight(20, ' ') + "驗證結果: " + reg.IsMatch(s) + "\n";
}
/*--------輸出--------
源字符串: 1,2,3,123,32,13 驗證結果: False
源字符串: 12,56,89,123,56,98 驗證結果: True
源字符串: 8,2,9,10,38,29,2,9 驗證結果: True
源字符串: 8,3,9,238,93,23 驗證結果: False
*/
源字符串的規則比較明確,就是用“,”分隔的數字,類似於這種查找是否有重復的需求,最簡單的就是用反向引用來解決了。
由於要驗證的是用“,”分隔的元素的整體是否有重復,所以“(\d+)”兩側的“\b”就是必須的,用來保證取到的數字子串是一個元素整體,而不是“123”中的“1”,當然,這里前后兩個“\b”分別換成“(?<!\d)”和“(?!\d)”是一個效果,可能意義上更明確。后面的兩個“\b”也是一樣的作用。
3.1.2 驗證連續數字是否有重復
參考 問兩個正則表達式。
需求描述:
數據:
1985aaa1985bb
bcae1958fiefadf1955fef
atijc1944cvkd
df2564isdjfef2564d
實現1:匹配第一次出現的四個數字.然后后面也存在這四個數字的
如:
1985aaa1985bb
第一次出現的四個數字是1985.然后后面也存在這四個數字,所以這個匹配
bcae1958fiefadf1955fef
第一次出現的四個數字是1958.然后后面不存在這四個數字.所以不匹配
-----
所以實現1.應該匹配
1985aaa1985bb
df2564isdjfef2564d
代碼實現:
//如果是驗證第一個出現的連續4個數字是否有重復
string[] test = new string[] { "1985aaa1985bb", "bcae1958fiefadf1955fef", "atijc1944cvkd", "df2564isdjfef2564d", "abc1234def5678ghi5678jkl" };
Regex reg = new Regex(@"^(?:(?!\d{4}).)*(\d{4})(?:(?!\1).)*\1");
foreach (string s in test)
{
richTextBox2.Text += "源字符串: " + s.PadRight(25, ' ') + "驗證結果: " + reg.IsMatch(s) + "\n";
}
/*--------輸出--------
源字符串: 1985aaa1985bb 驗證結果: True
源字符串: bcae1958fiefadf1955fef 驗證結果: False
源字符串: atijc1944cvkd 驗證結果: False
源字符串: df2564isdjfef2564d 驗證結果: True
源字符串: abc1234def5678ghi5678jkl 驗證結果: False
*/
由於需求要求驗證第一次出現的四個數字是否有重復,所以這里需要用“^(?:(?!\d{4}).)*(\d{4})”來保證捕獲組取得的是第一次出現的四個數字。
這樣寫可能有些復雜,可讀性較差,但這里需要用這種順序環視結合貪婪模式,來達到匹配第一次出現的四個數字的目的,而不能使用非貪婪模式.
對於使用非貪婪模式的正則“^.*?(\d{4})(?:(?!\1).)*\1”,可以看一下它匹配的結果。
string[] test = new string[] { "1985aaa1985bb", "bcae1958fiefadf1955fef", "atijc1944cvkd", "df2564isdjfef2564d", "abc1234def5678ghi5678jkl" };
Regex reg = new Regex(@"^.*?(\d{4})(?:(?!\1).)*\1");
foreach (string s in test)
{
richTextBox2.Text += "源字符串: " + s.PadRight(25, ' ') + "驗證結果: " + reg.IsMatch(s) + "\n";
}
/*--------輸出--------
源字符串: 1985aaa1985bb 驗證結果: True
源字符串: bcae1958fiefadf1955fef 驗證結果: False
源字符串: atijc1944cvkd 驗證結果: False
源字符串: df2564isdjfef2564d 驗證結果: True
源字符串: abc1234def5678ghi5678jkl 驗證結果: True
*/
是的,最后一項的驗證結果也是“True”,為什么會這樣?當捕獲組“(\d{4})”匹配到“1234”時,由於“1234”沒有重復,所以后面的子表達式匹配失敗,此時“.*?”會進行回溯,放棄當前狀態,繼續向前匹配,直到它匹配到“5678”前的“f”,由捕獲組“(\d{4})”匹配到“5678”,后面的子表達式可以匹配成功,報告整個表達式匹配成功。
NFA引擎在有可供回溯的狀態時,會一直嘗試直到所有可能都嘗試失敗后才報告失敗。上例中非貪婪模式在繼續嘗試時是可以找到匹配成功的位置的,而采用貪婪模式的正則“^(?:(?!\d{4}).)*(\d{4})”,由於“^(?:(?!\d{4}).)*”匹配到的內容不可能是連續的四個數字,所以無論怎么回溯,接下來的“(\d{4})”都不可能匹配成功,一直回溯到起始位置“^”,報告整個表達式匹配失敗。
而后面的順序環視+貪婪模式子表達式“(?:(?!\1).)*”則不存在以上問題,所以在源字符串比較簡單時可以寫作“.*?”,不會影響匹配結果。
而對於驗證任意位置是否存在四個重復數字,則不需要加起始位置的限定。
//如果是驗證任意位置出現的連續4個數字是否有重復,可以用我38樓的正則
string[] test = new string[] { "1985aaa1985bb", "bcae1958fiefadf1955fef", "atijc1944cvkd", "df2564isdjfef2564d", "abc1234def5678ghi5678jkl" };
Regex reg = new Regex(@"(\d{4})(?:(?!\1).)*\1");
foreach (string s in test)
{
richTextBox2.Text += "源字符串: " + s.PadRight(25, ' ') + "驗證結果: " + reg.IsMatch(s) + "\n";
}
/*--------輸出--------
源字符串: 1985aaa1985bb 驗證結果: True
源字符串: bcae1958fiefadf1955fef 驗證結果: False
源字符串: atijc1944cvkd 驗證結果: False
源字符串: df2564isdjfef2564d 驗證結果: True
源字符串: abc1234def5678ghi5678jkl 驗證結果: True
*/
3.2 限定指定標識配對
相對於查找重復來說,查找或指定標識配對出現這種應用場景要更多一些。尤其是對於HTML的處理中,這種應用更普遍。
3.2.1 限定標點配對
由於HTML語言的不規范性,導致以下三種寫法可以被解析。
- <a href=www.csdn.net>CSDN</a>
- <a href='www.csdn.net'>CSDN</a>
- <a href="www.csdn.net">CSDN</a>
而這對於一些需要進行字符串解析的應用,造成很大的麻煩。在提取鏈接時,雖然兩側都用“[‘”]?”通常也可以得到正確結果,卻不如用反向引用來得嚴謹、方便。
Regex reg = new Regex(@"(?is)<a(?:(?!href=).)*href=(['""]?)(?<url>[^""'\s>]*)\1[^>]*>(?<text>(?:(?!</a>).)*)</a>");
MatchCollection mc = reg.Matches(yourStr);
foreach (Match m in mc)
{
richTextBox2.Text += m.Groups["url"].Value + "\n";
richTextBox2.Text += m.Groups["text"].Value + "\n";
}
/*--------輸出--------
www.csdn.net
CSDN
www.csdn.net
CSDN
www.csdn.net
CSDN
*/
以下可以正確解析出三種形式的HTML代碼中的鏈接和文本,下面把正則改一下
Regex reg = new Regex(@"(?is)<a(?:(?!href=).)*href=(['""])?(?<url>[^""'\s>]*)\1[^>]*>(?<text>(?:(?!</a>).)*)</a>");
看到區別了嗎?只是把“([‘””]?)”改成了“([‘””])?”,結果會怎么樣呢?
Regex reg = new Regex(@"(?is)<a(?:(?!href=).)*href=(['""])?(?<url>[^""'\s>]*)\1[^>]*>(?<text>(?:(?!</a>).)*)</a>");
MatchCollection mc = reg.Matches(yourStr);
foreach (Match m in mc)
{
richTextBox2.Text += m.Groups["url"].Value + "\n";
richTextBox2.Text += m.Groups["text"].Value + "\n";
}
/*--------輸出--------
www.csdn.net
CSDN
www.csdn.net
CSDN
*/
結果只取到了兩組數據。這是因為對於情況1的HTML字符串,在“([‘””]?)”這種情況下,捕獲組雖然匹配到的只是一個位置,但畢竟是匹配成功了,所以可以用“\1”進行反向引用;而改成“([‘””])?”,捕獲組根本就沒有進行匹配,所以也就無法進行反向引用。
當然,對於HTML來說,還有一些比較復雜的情況,如
<a href="javascript:alert(1 > 2)"/>
這種復雜情況涉及到的場景比較少,通常應用可以不予以考慮,否則考慮的場景太復雜,會影響匹配效率。寫正則的一個一般原則就是,適用就好。這種場景如果遇到,需求根據具體情況,是否需要提取等進行分析,根據分析結果不同,寫出的正則也是不一樣的。
3.2.2 限定標簽配對
這種應用一般是在取某幾個特定標簽,或是動態生成正則表達式時用到。
需求描述:
刪除<script…>…</script>與<style…>…</style>標簽及其中間的內容。
代碼實現:
Regex reg = new Regex(@"(?is)<(script|style)\b[^>]*>(?(?!\1\b).)*</\1>");
string result = reg.Replace(yourStr, "");
因為這里要刪除的標簽不止一個,所以事先無法確定是哪個標簽,需要用到反向引用來限定標簽的配對。
當然,對於標簽有嵌套的情況,就要用到平衡組了。可以參考 .NET正則基礎之——平衡組。
3.2.3 取配對標簽中的內容
需求描述:
[id]5554323[id!][destid]10657302023180404[destid!][srcterminalid]13518841197[srcterminalid!][msgcontent]好的[msgcontent!][receivetime]20090409165217[receivetime!]
源字符串中標簽成對出現,無嵌套,分別提取標簽和對應的內容。
代碼實現:
string test = "[id]5554323[id!][destid]10657302023180404[destid!][srcterminalid]13518841197[srcterminalid!][msgcontent]好的[msgcontent!][receivetime]20090409165217[receivetime!]";
Regex reg = new Regex(@"(?s)\[([^\]]+)\]((?:(?!\[\1).)*)\[\1!\]");
MatchCollection mc = reg.Matches(test);
foreach (Match m in mc)
{
richTextBox2.Text += "Tag: " + m.Groups[1].Value.PadRight(20, ' ') + "Content: " + m.Groups[2].Value + "\n";
}
/*--------輸出--------
Tag: id Content: 5554323
Tag: destid Content: 10657302023180404
Tag: srcterminalid Content: 13518841197
Tag: msgcontent Content: 好的
Tag: receivetime Content: 20090409165217
*/
這種需求通常是由捕獲組匹配到一個標簽,然后向后匹配,直到與之配對的標簽外為止,根據源字符串的特點,中間可以使用非貪婪模式,也可以使用順序否定環視+貪婪模式。
3.3 反向引用的綜合應用
3.3.1 12位數字,其中不能出現6位連續相同數字
需求描述:
只允許12位數字,並且其中不能出現6位連續相同數字。
例如,123456789012是允許的,而123333334567是不允許的。
正則表達式:^(?:([0-9])(?!\1{5})){12}$
類似這種需要判定是否有連續相同元素的需求,其實也是驗證重復,也要用到反向引用。
說下分析過程,需求分解一下:
1、 一個數字
2、 它后面不能連續出現5個與它相同的數字
3、 滿足以上兩條的字符一共12個
那么根據需求分解寫出相應的正則:
1、([0-9])
2、(?!\1{5})
3、.{12}
將以上三個分解后得出的正則,按需求邏輯關系,組合一下:
(([0-9])(?!\1{5})){12}
由於是驗證整個字符串的規則,所以開始和結束標識“^”和“$”是少不了的,不需要用捕獲組的地方,用非捕獲代替,也就成了最后滿足需求的正則:
^(?:([0-9])(?!\1{5})){12}$
其實這個例子的分析過程,也是一些正則問題解析的通用過程,先把復雜的需求由整到零的分解,再各個實現,然后把實現的正則由零到整,考慮一下相互間的邏輯關系,基本上就可以得出正確的正則表達式了。
3.3.2 A-Z以內不重復的10個字母
需求描述:A-Z以內不重復的10個字母
正則表達式1:^(?:([A-Z])(?!.*?\1)){10}$
正則表達式2:^(?:([A-Z])(?=((?!\1).)*$)){10}$
這個需求與上一個需求類似,分析過程也差不多。其實這個問題如果用正則來實現,思路是非常清晰的 。
首先因為是驗證規則,所以“^”和“$”是必不可少的,分別匹配開始和結束的位置 。
然后是10個字母,那么([A-Z]){10},合起來就是^([A-Z]){10}$ 。
最后就是加一個規則,字母不能重復 。
如何保證不能重復,必然是用到反向引用 ,(一個字母)后面任意一個字母不能與這個字母重復,這樣實現起來就有兩種方式,當然,實質都是一樣的
實現方式一:^(?:([A-Z])(?!.*?\1)){10}$
實現方式二:^(?:([A-Z])(?=(?:(?!\1).)*$)){10}$
在這個需求當中,由於可能出現的源字符串不會太長,也不會太復雜,所以這兩個正則表達式在匹配效率上不會有明顯的差異。
解釋一下正則的含義,先解釋一下方式一的正則:
^(?:([A-Z])(?!.*?\1)){10}$
“^”和“$”分別匹配開始和結束位置,“{10}”為量詞,表示修飾的子串重復10次。
“(?:Expression)”是非捕獲組,目的是不將“()”內的“Expression”匹配的內容保存到內存中,之所以要這樣用,是因為后面的反向引用使用的是“\1”,如果不用非捕獲組,那么“([A-Z])”就是編號為2的捕獲組,后面的“\1”就要換成“\2”,來引用第二個捕獲組,替換后對匹配結果當然不會有什么影響,但由於由“(([A-Z])(?!.*?\1))”捕獲的內容我們並不關心,所以還是用非捕獲組,可以提升匹配效率。
“([A-Z])”就是匹配A到Z之間的任意一個字母,並保存匹配結果到捕獲組中。
“(?!.*?\1)”順序環視,它是零寬度的,雖然進行匹配,但不保存匹配結果,可以理解為它就是在所在位置的右側附加了一個條件,用在這里表示,它所在位置的右側,不管間隔多少個字符,都不能出現之前匹配到的那個字符,也就是不能有重復的字母出現。
“(?:([A-Z])(?!.*?\1)){10}”就是匹配到這樣一個字符 :
1、它首先是一個字母;
2、然后這個字母的右側間隔任意多個字符,不能再出現同樣的字母;
3、最后,符合以上兩條規則的字符,一共有10個。
加上首尾限定字符“^”和“$”,就是滿足需求的正則。
接下來討論一下方式二的正則:
^(?:([A-Z])(?=(?:(?!\1).)*$)){10}$
思路和以及其余部分子表達式與方式一完全一樣 ,只有“(?=(?:(?!\1).)*$)”這里不同,這個子表達式表示,它所在位置右側,一直到結尾,都不能是之前匹配到的那個字符。方式一是非貪婪模式的實現,而這個就是貪婪模式的實現。
這里需要用到順序肯定環視“(?=Expression)”,而不能用非捕獲組“(?:(?:(?!\1).)*$)”,是因為這里的表達式不能占有字符,只能作為條件存在,由量詞“{10}”修飾的子表達式最終只能匹配一個字符,否則就無法限定長度了。
3.3.3 提取指定單元長度字符串
需求描述 參考 求一正則表達式(c# ):
例如有字符串 string str = "w1w2w3w2w3w1w3w2w4w5w4w5w4w4w5w4w2w4w3w4w3w2w6w5w6w5w6w4w7",找出有且僅有兩個單元(w+數字作為一個單元,例如:w1,w2)組成的長度大於等於4個單元的字串(必須包括這兩個單元),這個例子,應輸出:"w2w3w2w3","w4w5w4w5w4w4w5w4","w4w3w4w3","w6w5w6w5w6" 。
如果找出有且僅有三個單元長度大於等於6個單元的字串,該如何寫正則表達式?
代碼實現:
//第一個需求,兩單元的
string str = "w7w7w7w5w7w1w2w3w2w3w1w3w2w4w5w4w5w4w4w5w4w2w4w3w4w3w2w6w5w6w5w6w4w7w7w7w5w7";
MatchCollection mc = Regex.Matches(str, @"(?i)(?=(w\d)\1*(w\d))(?:\1|\2){4,}");
foreach (Match m in mc)
{
richTextBox2.Text += m.Value + "\n";
}
/*--------輸出--------
bb0w7w7w7w5w7
w2w3w2w3
w4w5w4w5w4w4w5w4
w4w3w4w3
w6w5w6w5w6
w4w7w7w7
*/
//第二個需求,三單元的
string str = "w7w7w7w5w7w1w2w3w2w3w1w3w2w4w5w4w5w4w4w5w4w2w4w3w4w3w2w6w5w6w5w6w4w7w7w7w5w7";
MatchCollection mc = Regex.Matches(str, @"(?i)(?=(w\d)\1*(w\d)(?:\1|\2)*(w\d))(?:\1|\2|\3){6,}");
foreach (Match m in mc)
{
richTextBox2.Text += m.Value + "\n";
}
/*--------輸出--------
bb0w7w7w7w5w7w1
w2w3w2w3w1w3w2
w4w5w4w5w4w4w5w4w2w4
w2w6w5w6w5w6
w4w7w7w7w5w7
*/
這個實例可以認為是環視和反向引用綜合運用的一個經典實例。主要是用到了環視零寬度,不占有字符的特性,先由環視來取得規定單元的捕獲組的內容,再通過反向引用來進行實際的匹配。