HTML 解析類庫HtmlAgilityPack
1. HtmlAgilityPack簡介
網站中首先遇到的問題是爬蟲和解析HTML的問題,一般情況在獲取頁面少量信息的情況下,我們可以使用正則來精確匹配目標。不過本身正則表達式就比較復雜,同時正則表達式的精確程度很難拿捏,太精確和原網頁耦合太嚴重,頁面代碼稍改動就會使正則無效;太寬泛的正則由可能會匹配目標過多。所以我們今天介紹的是通過解析HTML結構來獲取目標的方式——HtmlAgilityPack。
HtmlAgilityPack是一個解析HTML的類庫,支持用XPath來解析HTML,可以像XML一樣來解析HTML。
HtmlAgilityPack的代碼托管在codeplex上:http://htmlagilitypack.codeplex.com/,不過建議通過Nuget來獲取最新版本。
2. XPath簡介
XPath即為XML路徑語言,它是一種用來確定XML文檔中某部分位置的語言。XPath基於XML的樹狀結構,提供在數據結構樹中找尋節點的能力。下圖列舉了XPath主要的路徑表達式:
這種針對XML的路徑能在解析HTML中用的原因是HtmlAgilityPack將下載下來的HTML頁面進行規格化處理,讓原本對語義支持並不好的HTML文檔格式變為更嚴謹的Xhtml格式,甚至可以轉換為XML格式;並使用XPath來選擇、處理dom中的element。下圖表示HTML格式化之後的節點示意圖:
3. HtmlAgilityPack中常用的API
在HtmlAgilityPack中常用到的類有HtmlDocument、HtmlNodeCollection、HtmlNode和HtmlWeb。
首先是加載HTML,如果是已經存在的靜態HTML代碼,可以用HtmlDocument的Load()或LoadHtml()來加載,如果是網絡上的URL則需要用HtmlWeb的Get()或Load()方法來加載。
不管是哪種加載方式,我們得到的都是HtmlDocument的實例。此時我們需要得到的是HtmlNode或者HtmlNodeCollection對象,使用HtmlDocument的DocumentNode屬性,它整個HTML文檔的根節點,它本身也是一個HtmlNode。
得到文檔根節點後即可使用前一節介紹的XPath得到你想要的文檔中任意一個節點的信息。
下面是一個典型的獲得有效內容的例子:
1 2 3 4 |
HtmlWeb htmlWeb = new HtmlWeb();
HtmlDocument htmlDoc = htmlWeb.Load( "http://www.baidu.com" );
HtmlNode htmlNode = htmlDoc.DocumentNode.SelectSingleNode( "//title" );
string title = htmlNode.InnerText;
|
由於使用最多的類是HtmlNode,這裏將它常用的屬性和方法列在下面,方便各位查閱。
屬性:
Attributes 獲取節點的屬性集合
ChildNodes 獲取子節點集合(包括文本節點)
FirstChild 獲取第一個子節點
HasAttributes 判斷該節點是否含有屬性
HasChildNodes 判斷該節點是否含有子節點
Id 獲取該節點的Id屬性
InnerHtml 獲取該節點的Html代碼
InnerText 獲取該節點的內容,與InnerHtml不同的地方在於它會過濾掉Html代碼,而InnerHtml是連Html代碼一起輸出
LastChild 獲取最後一個子節點
Name Html元素名
NextSibling 獲取下一個兄弟節點
ParentNode 獲取該節點的父節點
PreviousSibling 獲取前一個兄弟節點
XPath 根據節點返回該節點的XPath
方法:
HtmlNode AppendChild(HtmlNode newChild); 將參數元素追加到為調用元素的子元素(追加在最後)
void AppendChildren(HtmlNodeCollection newChildren); 將參數集合中的元素追加為調用元素的子元素(追加在最後)
HtmlNode PrependChild(HtmlNode newChild); 將參數中的元素作為子元素,放在調用元素的最前面
void PrependChildren(HtmlNodeCollection newChildren); 將參數集合中的所有元素作為子元素,放在調用元素前面
HtmlNode Clone(); 本節點克隆到一個新的節點
HtmlNode CloneNode(bool deep); 節點克隆到一個新的幾點,參數確定是否連子元素一起克隆
HtmlNode CloneNode(string newName); 克隆的同時更改元素名
HtmlNode CloneNode(string newName, bool deep); 克隆的同時更改元素名。參數確定是否連子元素一起克隆
void CopyFrom(HtmlNode node); 創建重復的節點和其下的子樹。
void CopyFrom(HtmlNode node, bool deep); 創建節點的副本。
static HtmlNode CreateNode(string html); 靜態方法,允許用字符串創建一個新節點
IEnumerable<HtmlNode> DescendantNodes(); 獲取所有子代節點
IEnumerable<HtmlNode> DescendantNodesAndSelf(); 獲取所有的子代節點以及自身
IEnumerable<HtmlNode> Descendants(); 獲取枚舉列表中的所有子代節點
IEnumerable<HtmlNode> Descendants(string name); 獲取枚舉列表中的所有子代節點,註意元素名要與參數匹配
IEnumerable<HtmlNode> DescendantsAndSelf(); 獲取枚舉列表中的所有子代節點以及自身
IEnumerable<HtmlNode> DescendantsAndSelf(string name); 獲取枚舉列表中的所有子代節點以及自身,註意元素名要與參數匹配
HtmlNode Element(string name); 根據參數名獲取一個元素
IEnumerable<HtmlNode> Elements(string name); 根據參數名獲取匹配的元素集合
bool GetAttributeValue(string name, bool def); 幫助方法,用來獲取此節點的屬性的值(布爾類型)。如果未找到該屬性,則將返回默認值。
int GetAttributeValue(string name, int def); 幫助方法,用來獲取此節點的屬性的值(整型)。如果未找到該屬性,則將返回默認值。
string GetAttributeValue(string name, string def); 幫助方法,用來獲取此節點的屬性的值(字符串類型)。如果未找到該屬性,則將返回默認值。
HtmlNode InsertAfter(HtmlNode newChild, HtmlNode refChild); 將一個節點插入到第二個參數節點的後面,與第二個參數是兄弟關系
HtmlNode InsertBefore(HtmlNode newChild, HtmlNode refChild); 講一個節點插入到第二個參數節點的後面,與第二個參數是兄弟關系
static bool IsCDataElement(string name); 確定是否一個元素節點是一個 CDATA 元素節點。
static bool IsClosedElement(string name); 確定是否封閉的元素節點
static bool IsEmptyElement(string name); 確定是否一個空的元素節點。
static bool IsOverlappedClosingElement(string text); 確定是否文本對應於一個節點可以保留重疊的結束標記。
void Remove(); 從父集合中移除調用節點
void RemoveAll(); 移除調用節點的所有子節點以及屬性
void RemoveAllChildren(); 移除調用節點的所有子節點
HtmlNode RemoveChild(HtmlNode oldChild); 移除調用節點的指定名字的子節點
HtmlNode RemoveChild(HtmlNode oldChild, bool keepGrandChildren);移除調用節點調用名字的子節點,第二個參數確定是否連孫子節點一起移除
HtmlNode ReplaceChild(HtmlNode newChild, HtmlNode oldChild); 將調用節點原有的一個子節點替換為一個新的節點,第二個參數是舊節點
HtmlNodeCollection SelectNodes(string xpath); 根據XPath獲取一個節點集合
HtmlNode SelectSingleNode(string xpath); 根據XPath獲取唯一的一個節點
HtmlAttribute SetAttributeValue(string name, string value); 設置調用節點的屬性
string WriteContentTo(); 將該節點的所有子級都保存到一個字符串中。
void WriteContentTo(TextWriter outText); 將該節點的所有子級都保存到指定的 TextWriter。
string WriteTo(); 將當前節點保存到一個字符串中。
void WriteTo(TextWriter outText); 將當前節點保存到指定的 TextWriter。
void WriteTo(XmlWriter writer); 將當前節點保存到指定的則 XmlWriter。
4. 實戰
基礎的都熟悉了,我們來做練習。代碼片段如下:
1 HtmlDocument doc = new HtmlDocument(); 2 doc.LoadHtml(html);//加載html 3 string pageNumberPath = @"//*[@id=‘J_topPage‘]/span/i"; 4 HtmlNode pageNumberNode = doc.DocumentNode.SelectSingleNode(pageNumberPath); 5 if (pageNumberNode != null) 6 { 7 string sNumber = pageNumberNode.InnerText; 8 for (int i = 1; i < int.Parse(sNumber) + 1; i++) 9 { 10 string pageUrl = string.Format("{0}&page={1}", category.Url, i); 11 try 12 { 13 List<Commodity> commodityList = GetCommodityList(category, pageUrl.Replace("&page=1&", string.Format("&page={0}&", i))); 14 commodityRepository.SaveList(commodityList); 15 } 16 catch (Exception ex)//保證一頁的錯誤不影響另外一頁 17 { 18 logger.Error("Crawler的commodityRepository.SaveList(commodityList)出現異常", ex); 19 } 20 }
5. 後記
HtmlAgilityPack 的確是一個功能強大的HTML解析類庫,我目前僅僅使用了它的一小部分功能,但是已經能完全滿足我的需求。如果童鞋們有類似需求,可以試試。
HTML 解析類庫HtmlAgilityPack