原文地址 http://www.cnblogs.com/qiantuwuliang/archive/2011/06/11/2078329.html
概述
Regex是 做文本解析工作必不可少的技能。如Web伺服器日誌分析,網頁前端開發等。很多進階文字編輯器都支援Regex的一個子集,熟練掌握Regex,經常能夠 使你的一些工作事半功倍。例如統計程式碼數,只需一個正則就搞定。嵌套Html標籤的匹配是Regex應用中一個比較難的話題,因為它涉及到的正則文法比 較多,也比較難。因此也就更有研究的價值。
思路
任何複雜的Regex都是由簡單的子表達 式組成的,要想寫出複雜的正則來,一方面需要有化繁為簡的功底,另外一方面,我們需要從正則引擎的角度去思考問題。關於正則引擎的原理,推薦 《Mastering Regular Expression》中文名叫《精通Regex》。挺不錯的一本書。
OK,先確定我們要解決的問題——從一段Html文本中找出特定id的標籤的innerHTML。
這裡面最大的痛點就是,Html標籤是支援嵌套的,怎麼能夠找到指定標籤相對應的閉合標籤呢?
我們可以這樣想,先匹配最前面的起始標籤,假設是div吧(<div),然後一旦遇到嵌套div,就“壓入堆棧”,然後一遇到div結束標籤了,就“彈出堆棧”。如果遇到結束標籤的時候,堆棧裡面已經沒有東西了,那麼匹配結束,此結束標籤為正確的閉合標籤。
我之所以能夠這樣去思考,是因為我瞭解過正則的特性,我知道正則中的平衡組能夠實現我剛才說的“堆棧”操作。所以,如果我們要編寫複雜Regex,需要對正則的一些進階特性至少有所瞭解,這樣我們思考問題才有個方向。
實現
這裡假設我們要匹配的文本是一段合法的Html文本。下面這段Html代碼是從我的部落格上拷貝下來的,作為我們的測試文本。我們要匹配的就是footer這個div的innerHTML,同時把標籤名也捕獲下來。
<div style="background-color:gray;" id="footer">
<a id="gotop" href="#" onclick="MGJS.goTop();return false;">Top</a>
<a id="powered" href="http://wordpress.org/">WordPress</a>
<div id="copyright">
Copyright © 2009 簡單生活 —— Kevin Yang的部落格 </div>
<div id="themeinfo">
Theme by <a href="http://www.neoease.com/">mg12</a>.
Valid <a href="http://validator.w3.org/check?uri=referer">XHTML 1.1</a>
and <a href="http://jigsaw.w3.org/css-validator/">CSS 3</a>.
</div>
</div>
這裡我們需要藉助Expresso工具來構建和測試編寫的Regex。
匹配起始標籤
起始標籤特徵很好提取,以角括弧打頭,然後跟著一連串英文字母,然後一大串屬性中(非角括弧字元)匹配id(不區分大小寫)=footer。需要注意的是,footer可以被雙引號或者單引號包裹,也可以什麼都不加。正則如下:
<(?<HtmlTag>[\w]+)[^>]*\s[iI][dD]=(?<Quote>["']?)footer(?(Quote)\k<Quote>)["']?[^>]*>
上面的Regex需要做幾點說明:
1. <角括弧在正則中算是一個特殊字元,在顯式捕獲分組中用它將分組名括起來。但是因為開頭的角括弧在此上下文下並不會出現解析歧義,因此加不加轉義符效果是一樣的。
2. (?<GroupName>RegEx)格式定義一個命名分組,我們在上面定義了一個HtmlTag的標籤分組,用來存放匹配到的Html標籤名。Quote分組是用來給後面的匹配使用的。
3. (?(GroupName)Then|Else)是條件陳述式,表示當捕獲到GroupName分組時執行Then匹配,否則執行Else匹配。上面的正則 中,我們先嘗試匹配footer字串左邊的引號,並將其存入LeftQuote分組中,然後在footer右側進行條件解析,如果之前匹配到 LeftQuote分組,那麼右側也應該批評LeftQuote分組。這樣一來,我們就能精確匹配id的各種情況了。
匹配閉合標籤
((?<Nested><\k<HtmlTag>[^>]*>)|</\k<HtmlTag>>(?<-Nested>)|.*?)*</\k<HtmlTag>>
在成功匹配到起始標籤之後,後面的Html文本可以分為三種情況:
A. 匹配到嵌套div起始標籤<div,這個時候,需要將其捕獲到Nested分組。
B. 匹配到嵌套div起始標籤的閉合標籤,這個時候,需要將之前的Nested分組釋放
C. 其他任意文本。注意,需要使用.*?方式關閉貪婪匹配,否則最後的閉合標籤可能會過度匹配
使 用(RegEx1|RegEx2|RegEx3)*這種方式,可以將幾個條件以或的形式組合起來,然後再取若干次匹配結果,最終再匹配閉合標籤。其中 (?<-Nested>)是表示釋放之前捕獲的Nested分組。確切的文法是(?<N-M>)即使用N分組替換掉M分組,如果 N分組沒有指定或不存在,則釋放M分組。
update:前面過於側重分析了,最後沒有給出一個完整的正則真是抱歉。
<(?<HtmlTag>[\w]+)[^>]*\s[iI][dD]=(?<Quote>["']?)footer(?(Quote)\k<Quote>)["']?[^>]*>((?<Nested><\k<HtmlTag>[^>]*>)|</\k<HtmlTag>>(?<-Nested>)|.*?)*</\k<HtmlTag>>
上面這個正則能夠匹配任意id=footer的html標籤。
需要注意,此Regex需要設定SingleLine=true,這樣點號才可以把分行符號也匹配進去。
對於domoxz 的問題,如果要匹配p標籤,那麼只需將上述的正則中的HtmlTag替換成p即可。
另加一個 實戰需求: 如果想得到 匹配任意id=footer的html標籤 的 innerHTML,則正則改成如下即可:
<(?<HtmlTag>[\w]+)[^>]*\s[iI][dD]=(?<Quote>["']?)footer(?(Quote)\k<Quote>)["']?[^>]*>(((?<Nested><\k<HtmlTag>[^>]*>)|</\k<HtmlTag>>(?<-Nested>)|.*?)*)</\k<HtmlTag>>
C#參考代碼:
MatchCollection m = Regex.Matches(this.tp4rtbCont.Text, this.tp4txtRegx.Text.Trim(), RegexOptions.IgnoreCase | RegexOptions.Multiline|RegexOptions.Singleline);
if (m.Count > 0)
{
foreach (Match subm in m)
{
this.tp4rtbResult.Text += c.ToString() + "." + subm.Groups[0].Value.Replace("amp;", "") + "\r\n+++++++++++++++++++++++++++++++++\r\n";
}
}
else
{
this.tp4rtbResult.Text += "匹配失敗..." + "\r\n+++++++++++++++++++++++++++++++++\r\n";
}