使用xpath實作document.querySelector樣式選擇器進行html解析(一):将html轉成xml
使用xpath實作document.querySelector樣式選擇器進行html解析(二):擴充一下xpath以便支援正則
使用xpath實作document.querySelector樣式選擇器進行html解析(三):實作樣式選擇器
使用xpath實作document.querySelector樣式選擇器進行html解析(四):将選擇結果封裝進行輸出
-----------------------------------------------------------------
文盲做采集工作也做了有些年頭了,一直以來,對采集到的内容都是用正則進行資料提取的,但是使用的時間越長,越覺得使用正則很麻煩。
第一,了解正則的人在行業内真的是少數,而且複雜的業務邏輯寫出來的正則,隔段時間,自己都看不懂了。。。
第二,正則對文檔的格式還是有一定要求的,比如說如何提取一個完整的閉合html标簽,這個正則就很複雜,用到層深計算了,如果一旦html内出現了非法内容,那就是一場災難,正則會整個卡死。。。。。
是以,文盲老顧一直想找一個htmlparser類型的東西來代替正則,恩,比如說Winista.HtmlParser啦、HtmlAgilityPack啦
但是,這裡要說一個但是,這些第三方的東西并不符合咱們的日常使用習慣,什麼是日常使用習慣呢?當然是css選擇器啦!不管是按id找啦,按樣式找啦,還是按标簽找啦,這些方式我相信大部分開發人員都能很快上手。
于是,按照這個目的觸發,那麼文盲老顧找到的第三方工具都需要帕斯掉了,因為他們不支援,或僅支援部分需求,恩。。。。hmmmmmmmm,也許是文盲老顧沒弄明白這些東西到底怎麼來實作這個css選擇器方式的内容查找,總之,文盲決定自己搞一個htmlparser了
廢話說到這裡,下邊開始編寫文盲版的htmlparser
-----------------------------------------------------------------
在開始編寫之前整理一下思路
首先,html是一個格式很随意的文本文檔,不能強求它一定符合xhtml規範
第二,在xml中,可以通過xpath來實作諸如id、樣式、文字包含等css1.0、2.0、3.0各種規範的選擇器(雖然可能比較複雜,但文盲老顧在2014年的确已經實作了很多内容,css僞類沒做實作,有需要的話,各位同學可以在本文後留言共同讨論)
第三,html無法直接轉成xml,是以我們需要對html進行一些處理,使其能正常的轉換到xml格式
最後,定義一個通用方法,來實作css選擇器方式選取節點并得到想提取的資訊
根據這個思路,第一步應該是先把html轉成xml,好了,開始做第一步工作
-----------------------------------------------------------------
首先先定義一個類,用以加載html内容
public class HtmlObject
{
private string _html = string.Empty;
private List<string> _tags = new List<string>();
private List<string> _self = new List<string>();
private XmlDocument _xml = null;
public string Html
{
get
{
return _html;
}
}
public XmlDocument Xml
{
get
{
return _xml;
}
}
public HtmlObject()
{
InitDefine();
}
public HtmlObject(string html)
{
_html = html;
InitDefine();
InitHtml();
}
public void Load(string file)
{
LoadHtml(FileHelper.FileToString(file));
}
public void LoadHtml(string html)
{
_html = html;
InitHtml();
}
public void LoadUrl(string url)
{
Ajax ajax = new Ajax();
ajax.AppendCss = false;
ajax.AddFullPath = true;
ajax.AutoSave = false;
ajax.AutoUpdate = true;
LoadHtml(ajax.Http(url));
}
private void InitDefine()
{
// 聲明自閉合标簽
_self.AddRange(new string[] { "img", "br", "hr", "base", "meta", "link", "area" });
}
private void InitHtml()
{
_tags = new List<string>();
XmlDocument xml = new XmlDocument();
xml.LoadXml("<r />");
MatchCollection mc = Regex.Matches(_html, @"<!(?!-)(?:[^<>'""]|(['""])[^'""]*\1)*?>|<([%\?])[\s\S]*?\2>|<!--[\s\S]*?-->|<(script|style)(?!\w)[^<>]*?>(?:[^'""]|(['""])[^'""]*\4)*?</\3(?!\w)[^<>]*?>|<(?![!%\?])(?:[^<>'""]|(['""])[^'""]*\5)*?>|[^<]+(?=<|$)", RegexOptions.IgnoreCase);
XmlNode node = xml.DocumentElement;
for (int i = 0; i < mc.Count; i++)
{
ParseNode(ref node, mc[i].Value);
}
_xml = xml;
}
private void ParseNode(ref XmlNode node, string value)
{
// 如果是标簽
if (Regex.IsMatch(value, @"^<"))
{
XmlNode xn = null;
string name = string.Empty;
//如果是樣式或腳本
if (Regex.IsMatch(value, @"^<(script|style)(?!\w)", RegexOptions.IgnoreCase))
{
xn = XMLExpand.AppendNode(node, Regex.Match(value, @"(?<=^<)(style|script)", RegexOptions.IgnoreCase).Value.ToLower());
xn.AppendChild(xn.OwnerDocument.CreateCDataSection(Regex.Match(value, @"(?<=^<(style|script)[^<>]*?>)[\s\S]*?(?=</\1[^<>]*?>$)", RegexOptions.IgnoreCase).Value));
}
// 注釋或其他程式語言标簽
if (Regex.IsMatch(value, @"^<[!%\?]"))
{
node.AppendChild(node.OwnerDocument.CreateCDataSection(value));
//XMLExpand.AppendNode(node, "REM").InnerText = value;
}
// 正常标簽
if (Regex.IsMatch(value, @"^<(?!(script|style))\w+"))
{
name = Regex.Match(value, @"(?<=^<)\w+", RegexOptions.IgnoreCase).Value.ToLower();
// 如果不是自閉合标簽則将目前增加的标簽放入到待閉合标簽中
if (!Regex.IsMatch(value, @"/>$") && !_self.Contains(name))
{
_tags.Add(name);
}
xn = XMLExpand.AppendNode(node, name);
node = xn;
}
// 正常标簽結束
if (Regex.IsMatch(value, @"^</"))
{
name = Regex.Match(value, @"(?<=^</)\w+", RegexOptions.IgnoreCase).Value.ToLower();
if (node.Name == name)
{
_tags.RemoveAt(_tags.Count - 1);
node = node.ParentNode;
}
else
{
// 如果待閉合标簽中包含對應标簽則關閉對應标簽,否則忽視
if (_tags.Contains(name))
{
for (int i = _tags.Count; i > 0; i--)
{
if (_tags[i - 1] == name)
{
_tags.RemoveRange(i - 1, _tags.Count - i + 1);
break;
}
}
while (node.Name != name)
{
node = node.ParentNode;
}
}
}
}
if (Regex.IsMatch(value, @"^<(?![/!%\?])") && xn != null)
{
Match m = Regex.Match(value, @"^<[^<>]*?>", RegexOptions.IgnoreCase);
ParseAttribute(xn, m);
}
// 如果是自閉合标簽
if (xn != null && xn == node && !string.IsNullOrEmpty(name) && (Regex.IsMatch(value, @"/>$") || _self.Contains(name)))
{
node = node.ParentNode;
}
}
else
{
// 純文字,将文本内容作為節點文本内容
node.AppendChild(node.OwnerDocument.CreateCDataSection(value));
//XMLExpand.AppendNode(node, "TEXT").InnerText = value;
}
}
private void ParseAttribute(XmlNode node, Match match)
{
string html = match.Value;
MatchCollection mc = Regex.Matches(html, @"(?<=[\r\n\s\t])(\w+)[\r\n\s\t]*=[\r\n\s\t]*((['""])([^'""]*)\3|[^\s\r\t\n>]+)", RegexOptions.IgnoreCase);
for (int i = 0; i < mc.Count; i++)
{
XMLExpand.SetAttribute(node, mc[i].Groups[1].Value.ToLower(), string.IsNullOrEmpty(mc[i].Groups[4].Value) ? (Regex.IsMatch(mc[i].Groups[2].Value, @"^(['""])\1$") ? "" : mc[i].Groups[2].Value) : mc[i].Groups[4].Value);
}
}
}
恩。。。。。。反正就是這麼個代碼,呵呵
構造函數有兩個,一個是帶html文本的,一個是不帶的
加載文檔則有三個方法,一個是直接加載html文本的LoadHtml方法,一個是加載本地檔案的Load方法,一個是加載網址獲得文檔LoadUrl,Hmmmmmmmm,LoadUrl就忽略好了,Load方法也忽略好了。。。。我的代碼中用到的類可以自己去實作後替換,反正意思一樣。。。。
在這個類中,我聲明了兩個私有數組,_tags和_self,_tags是用來存儲解析過程中,未閉合的标簽,而_self則儲存無需閉合的标簽枚舉
然後,就是InitHtml這個核心方法了。。。。。
對html文檔,我使用正則将其切分成一個數組,這個正則大家也可以幫我看看有沒有需要調整的地方
<!(?!-)(?:[^<>'""]|(['""])[^'""]*\1)*?>
|
<([%\?])[\s\S]*?\2>
|
<!--[\s\S]*?-->
|
<(script|style)(?!\w)[^<>]*?>(?:[^'""]|(['""])[^'""]*\4)*?</\3(?!\w)[^<>]*?>
|
<(?![!%\?])(?:[^<>'""]|(['""])[^'""]*\5)*?>
|
[^<]+(?=<|$)
我是這麼想的,html中顯示的文本是在标簽之外的,恩,用最後一個正則片段實作,也就是[^<]+(?=<|$)部分
然後是正常的标簽部分,不管是結束标簽還是閉合标簽還是其他什麼html不識别的标簽,隻要是标簽格式,我都拿出來當标簽處理,恩,用倒數第二個正則片段實作,也就是<(?![!%\?])(?:[^<>'""]|(['""])[^'""]*\5)*?>部分
但是,在實際使用過程中,有些标簽中會包含一些特定文本,比如樣式、比如腳本,那麼把樣式和腳本作為特定标簽處理,于是産生了倒數第三個正則片段。。。恩,主要是為了在腳本片段中允許出現小于号,還有</script>這樣的常量,是以這個正則稍微麻煩了些
再然後,發現還有注釋内容也很蛋疼。。。。例如<!--這裡是包含标簽的注釋内容<a href="" target="_blank" rel="external nofollow" >連結</a>-->。。。。沒辦法,繼續加特例。。。。于是倒數第四個正則片段也出現了。。。。。
哦,寫到這裡,發現還會可能出現其他腳本語言片段。。。例如<% %>啦,例如<? ?>啦。。。得,再來搞個正則用來把它也摘出來
最後。。。。還有html聲明。。。。也就是<!doctype html>這樣的html代碼片段也得特殊聲明下。。。。。。好了,第一步我們完成了。。。。。把html用正則拆開了。。。。
MatchCollection mc = Regex.Matches(_html, @"<!(?!-)(?:[^<>'""]|(['""])[^'""]*\1)*?>|<([%\?])[\s\S]*?\2>|<!--[\s\S]*?-->|<(script|style)(?!\w)[^<>]*?>(?:[^'""]|(['""])[^'""]*\4)*?</\3(?!\w)[^<>]*?>|<(?![!%\?])(?:[^<>'""]|(['""])[^'""]*\5)*?>|[^<]+(?=<|$)", RegexOptions.IgnoreCase);
說真的,如果這個正則還有其他文盲沒有考慮到的情況,請在本文後留言,文盲會盡快測試,或者,同學們要是發現使用這個正則拆分html的時候出現内容丢失或者拆分結果不符合預期的時候,也請留言,并将html片段貼出來
恩。。。。。。第一步完成了,就繼續下一步,解析節點。。。,也就是ParseNode方法了
解析節點的思路也比較簡單,如果是文本,則扔個CDataSection節點到xml裡,如果是标簽,則按照标簽格式扔不同的節點到xml裡,如果是非閉合标簽,則目前标簽修正為新增标簽,如果是閉合标簽,則目前标簽修正為對應的開始标簽的父級,如果新增了标簽,順便把新增标簽的屬性也解析一下,恩,也就是ParseAttribute
不知道會不會有其他異常,也請大家幫忙測試
好了,第一階段完成,可以把Html轉成xml了,實作選擇的的内容,我們下次再說