最近在瀏覽以前自己上傳的源碼,發現在糗事百科桌面端源碼評論區中,有人說現在程序不能用了。查看了一下源碼運行情況,發現是正則表達式解析問題。由於糗百的網頁版鏈接和網頁格式稍有變化,導致解釋失敗。雖然可以通過更改正則表達,重新獲網頁的信息,但比較復雜,出錯率較高(技術有限)。因此第二個版本采用HtmlAgilityPack類庫解析Html。
1. HtmlAgilityPack類庫
HtmlAgilityPack是一個解析Html文檔的一個類庫,當然也能夠支持XML文件,該類庫比.NET自帶的XML解析庫要方便靈活多,主要是HtmlAgilityPack支持XPath路徑表達式,通過XPath表達式能夠快速定位到文檔中的某個節點。有了它,解析網頁不成問題,接下來簡述一下HtmlAgilityPack的使用方法。
HtmlAgilityPack 有3個比較重要的類型。
- HtmlDocument : 加載Html文檔(string),並解析成有層次的對象結構。
- HtmlNode : 元素節點類型,該類型提供許多非常有用的方法,下面會將重點的方法都介紹一遍。
- HtmlNodeCollection : html節點集合類型。
通過例子來講解這三個類型的使用,可能會更加的清晰,html源碼,如下:
<div>
<div id="content" class="c1">
<p id="p1" class="pStyle" title="HtmlAgility">段落1</p>
<p id="p2" class= "pStyle">段落2</p>
</div>
<div id="content2" class="c1">
<p class="pStyle">段落3</p>
<span>
<h1>hello</h1>
<span>
<div>
</div>
使用HtmlDocument類型解析上面的html源碼。
HtmlDocument htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(htmlStr);
HtmlNode rootNode = htmlDoc.DocumentNode;//獲取文檔的根節點
通過該類型的 LoadHtml 方法解析html字符串,然后獲取html文檔的根節點,當然該類型還有一個 Load方法,支持從文件中,url 或者流中加載html文檔。獲取了HtmlNode 類型的根節點之后,利用根節點與XPath路徑表達可以獲取文檔中任意的節點。
獲取所有class=pStyle的p節點
string xpath = "//p[@class='pStyle']";
HtmlNodeCollection pNodes = rootNode.SelectNodes(xpath);
利用HtmlNode類型的SelectNodes(xpath)方法可以獲取所有class為pStyle的p標簽。但是這次會把文檔中所有的樣式類為pStyle的p標簽都獲取到。如果只想獲取“div id="content"”容器下面的。只需要更改xpath路徑表達式即可做到。
string xpath = "//div[@id='content']/p[@class='pStyle']";
HtmlNodeCollection pNodes = rootNode.SelectNodes(xpath);
通過在之前的路徑上加上div[@id='content']進行限定即可。如果只想找"div id="content"”容器下面的class=pStyle 且 title=HtmlAgility的p標簽代碼如下。
string xpath = @"//div[@id='content']/p[@class='pStyle' and @title='HtmlAgility']";
HtmlNodeCollection pNodes = rootNode.SelectNodes(xpath);
通過XPath可以很方便的定位到文檔中的節點,並通過節點類型的屬性或者方法來獲取節點的屬性值和文本值以及內部html。
獲取節點的文本和class屬性值,代碼如下:
string xpath = @"//div[@id='content']/p[@class='pStyle' and @title='HtmlAgility']";
HtmlNodeCollection pNodes = rootNode.SelectNodes(xpath);
string nodeText=pNodes[0].InnerText;
string classValue = pNodes[0].GetAttributeValue("class", "");
HtmlNode類型的屬性和方法如下:
InnerText : 節點的文本值
InnerHtml: 節點內部的html字符串
GetAttributeValue :通過節點的屬性名字獲取屬性值,該方法有三個重載的版本,可以獲取分別返回bool,string、int 三種類型的屬性值。
上面都在簡介HtmlAgilityPack類庫的使用,下面簡單介紹一下XPath。
- // 所有后代節點,從根部開始,跟SelectNodes方法前面的節點沒關系 例子://div: 所有名為div的節點
- . 表示當前節點,與SelectNodes方法前面的節點相關 例子:./div :當前節點下的所有名為div節點
- .. 表示父節點,與SelectNodes方法前面的節點相關 例子: ../div :當前節點的父節點下的div節點
- @ 選取屬性 例子://div/@id:所有包含id屬性的div
XPath不僅僅可以通過路徑定位,還可以對定位后的節點集做一些限制,使得選擇更加精准。
- /root/book[1] 節點集中的第一個節點
- /root/book[last()] 節點集中最后一個節點
- /root/book[position() - 2] 節點集中倒數第三個節點集
- /root/book[position() < 5] 節點集中前五個節點集
- /root/book[@id] 節點集中含有屬性id的節點集
- /root/book[@id='chinese'] 節點集中id屬性值為chinese的節點集
- /root/book[price > 35]/title 節點集中book的price元素值大於35的title節點集
XPath對於路徑匹配,謂語限制都可以使用通配符。
- /div/* div節點下面的所有節點類型
- //div[@*] div下面的所有屬性
此外XPath還能進行邏輯運算
- | ——例:/root/book[1] | /root/book[3]:兩個節點集的合並—— (
+,*) - //div[@id='test1' and @class='divStyle']—— 找尋所有 id=test1 和class=divStyle 的div節點
- //div[not(@class)]——找尋不包括class屬性div節點
(or, and, not, =, !=, >, <, >=)
最后在提供一個小技巧,在網頁中打開開發者工具,可以直接定位到相應的html節點,然后右鍵可以直接獲取該節點在文檔中的XPath路徑,如下圖:

好了, HtmlAgilityPack類庫的使用就介紹到這里。
2. 使用HtmlAgilityPack解析糗百
關於糗百的網頁結構分析,在這篇文章使用HttpGet協議與正則表達實現桌面版的糗事百科 已經詳細介紹了,在此就不在贅述了,下面簡介一下抓取糗百段子的關鍵代碼。
/// <summary>
/// 獲取笑話列表
/// </summary>
/// <param name="htmlContent"></param>
public static List<JokeItem> GetJokeList(int pageIndex)
{
string htmlContent=GetUrlContent(GetWBJokeUrl(pageIndex));
List<JokeItem> jokeList = new List<JokeItem>();
HtmlDocument htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(htmlContent);
HtmlNode rootNode=htmlDoc.DocumentNode;
string xpathOfJokeDiv = "//div[@class='article block untagged mb15']";
string xpathOfJokeContent = "./a/div[@class='content']/span";
string xpathOfImg = "./div[@class='author clearfix']/a/img";
try
{
HtmlNodeCollection jokeCollection = rootNode.SelectNodes(xpathOfJokeDiv);
int jokeCount = jokeCollection.Count;
JokeItem joke;
foreach (HtmlNode jokeNode in jokeCollection)
{
joke = new JokeItem();
HtmlNode contentNode = jokeNode.SelectSingleNode(xpathOfJokeContent);
if (contentNode != null)
{
joke.JokeContent = Regex.Replace(contentNode.InnerText, "(\r\n)+", "\r\n");
}
else
{
joke.JokeContent = "";
}
HtmlNode imgornameNode = jokeNode.SelectSingleNode(xpathOfImg);
if (imgornameNode != null)
{
joke.NickName = imgornameNode.GetAttributeValue("alt", "");
joke.HeadImage = GetWebImage("http:"+imgornameNode.GetAttributeValue("src", ""));
joke.HeadImage = joke.HeadImage != null ? new Bitmap(joke.HeadImage, 50, 50) : null;
}
else
{
joke.NickName = "匿名用戶";
joke.HeadImage = null;
}
jokeList.Add(joke);
}
}
catch{}
return jokeList;
}
下載圖片的代碼如下:
private static Image GetWebImage(string webUrl)
{
try
{
Encoding encode = Encoding.GetEncoding("utf-8");//網頁編碼==Encoding.UTF8
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(new Uri(webUrl));
HttpWebResponse ress = (HttpWebResponse)req.GetResponse();
Stream sstreamRes = ress.GetResponseStream();
return System.Drawing.Image.FromStream(sstreamRes);
}
catch { return null; }
}
需要注意的是:
獲取糗百頭像的地址為://pic.qiushibaike.com/system/avtnew/3281/32814675/medium/2017070317185381.JPEG" ,一定要在前面加上"http:" 才能正確的下載頭像。
效果如下:

3. 小結
通過HtmlAgilityPack來解析文檔,很輕巧靈活,主要是不容易出錯。覺得這個解析包和Python的Beautiful Soup 庫有異曲同工之妙,都是解析網頁的好工具。
本文的源碼下載鏈接:https://github.com/StartAction/qbDesktop
