爬蟲的意義在於採集大批量數據,而後基於此進行加工/分析,作更有意義的事情。谷歌,百度,今日頭條,天眼查都離不開爬蟲。html
今天咱們來實踐一個最簡單的爬蟲系統。根據Url來識別網頁內容。node
GitHub地址git
HtmlAgilityPack官網github
HtmlAgilityPack的stackoverflow地址web
至今Nuget已有超過900多萬的下載量,應用量十分龐大。它提供的文檔教程也十分簡單易用。chrome
HtmlParse可讓你解析HTML並返回HtmlDocumentc#
/// <summary>
/// 從文件讀取
/// </summary>
public void FromFile() {
var path = @"test.html";
var doc = new HtmlDocument();
doc.Load(path);
var node = doc.DocumentNode.SelectSingleNode("//body");
Console.WriteLine(node.OuterHtml);
}
/// <summary>
/// 從字符串讀取
/// </summary>
public void FromString()
{
var html = @"<!DOCTYPE html>
<html>
<body>
<h1>This is <b>bold</b> heading</h1>
<p>This is <u>underlined</u> paragraph</p>
<h2>This is <i>italic</i> heading</h2>
</body>
</html> ";
var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(html);
var htmlBody = htmlDoc.DocumentNode.SelectSingleNode("//body");
Console.WriteLine(htmlBody.OuterHtml);
}
/// <summary>
/// 從網絡地址加載
/// </summary>
public void FromWeb() {
var html = @"https://www.cnblogs.com/";
HtmlWeb web = new HtmlWeb();
var htmlDoc = web.Load(html);
var node = htmlDoc.DocumentNode.SelectSingleNode("//div[@id='post_list']");
Console.WriteLine("Node Name: " + node.Name + "\n" + node.OuterHtml);
}
選擇器容許您從HtmlDocument中選擇HTML節點。它提供了兩個方法,能夠用XPath表達式篩選節點。XPath教程api
SelectNodes() 返回多個節點markdown
SelectSingleNode(String) 返回單個節點網絡
簡介到此爲止,更全的用法參考 http://html-agility-pack.net
咱們以博客園首頁爲示例。用chrome分析下網頁結構,可採集出推薦數,標題,內容Url,內容簡要,做者,評論數,閱讀數。
創建一個Article用來接收文章信息。
public class Article { /// <summary> /// /// </summary> public string Id { get; set; } /// <summary> /// 標題 /// </summary> public string Title { get; set; } /// <summary> /// 概要 /// </summary> public string Summary { get; set; } /// <summary> /// 文章連接 /// </summary> public string Url { get; set; } /// <summary> /// 推薦數 /// </summary> public long Diggit { get; set; } /// <summary> /// 評論數 /// </summary> public long Comment { get; set; } /// <summary> /// 閱讀數 /// </summary> public long View { get; set; } /// <summary> ///明細 /// </summary> public string Detail { get; set; } /// <summary> ///做者 /// </summary> public string Author { get; set; } /// <summary> /// 做者連接 /// </summary> public string AuthorUrl { get; set; } }
而後根據網頁結構,查看XPath路徑,採集內容
/// <summary> /// 解析 /// </summary> /// <returns></returns> public List<Article> ParseCnBlogs() { var url = "https://www.cnblogs.com"; HtmlWeb web = new HtmlWeb(); //1.支持從web或本地path加載html var htmlDoc = web.Load(url); var post_listnode = htmlDoc.DocumentNode.SelectSingleNode("//div[@id='post_list']"); Console.WriteLine("Node Name: " + post_listnode.Name + "\n" + post_listnode.OuterHtml); var postitemsNodes = post_listnode.SelectNodes("//div[@class='post_item']"); var articles = new List<Article>(); var digitRegex = @"[^0-9]+"; foreach (var item in postitemsNodes) { var article = new Article(); var diggnumnode = item.SelectSingleNode("//span[@class='diggnum']"); //body var post_item_bodynode = item.SelectSingleNode("//div[@class='post_item_body']"); var titlenode = post_item_bodynode.SelectSingleNode("//a[@class='titlelnk']"); var summarynode = post_item_bodynode.SelectSingleNode("//p[@class='post_item_summary']"); //foot var footnode = item.SelectSingleNode("//div[@class='post_item_foot']"); var authornode = footnode.ChildNodes[1]; var commentnode = item.SelectSingleNode("//span[@class='article_comment']"); var viewnode = item.SelectSingleNode("//span[@class='article_view']"); article.Diggit = int.Parse(diggnumnode.InnerText); article.Title = titlenode.InnerText; article.Url = titlenode.Attributes["href"].Value; article.Summary = titlenode.InnerHtml; article.Author = authornode.InnerText; article.AuthorUrl = authornode.Attributes["href"].Value; article.Comment = int.Parse(Regex.Replace(commentnode.ChildNodes[0].InnerText, digitRegex, "")); article.View = int.Parse(Regex.Replace(viewnode.ChildNodes[0].InnerText, digitRegex, "")); articles.Add(article); } return articles; }
看到結果就驚呆了,居然全是重複的。難道是Xpath語法理解不對麼?
XPath 使用路徑表達式在 XML 文檔中選取節點。節點是經過沿着路徑或者 step 來選取的
表達式 描述 nodename 選取此節點的全部子節點。 / 從根節點選取。 // 從匹配選擇的當前節點選擇文檔中的節點,而不考慮它們的位置。 . 選取當前節點。 .. 選取當前節點的父節點。 @ 選取屬性。
XPath 通配符可用來選取未知的 XML 元素
通配符 描述 * 匹配任何元素節點。 @* 匹配任何屬性節點。 node() 匹配任何類型的節點。
我測試了幾個語法如:
//例1,會返回20個 var titlenodes = post_item_bodynode.SelectNodes("//a[@class='titlelnk']");
//會報錯,由於這個a並不直接在bodynode下面,而是在子級h3元素的子級。 var titlenodes = post_item_bodynode.SelectNodes("a[@class='titlelnk']");
而後又實驗了一種:
//Bingo,這個能夠,可是強烈指定了下級h3,這就稍微麻煩了點。 var titlenodes = post_item_bodynode.SelectNodes("h3//a[@class='titlelnk']");
這裏就引伸出了一個小問題:如何定位子級的子級?用通配符*能夠麼?
//返回1個。 var titlenodes= post_item_bodynode.SelectNodes("*//a[@class='titlelnk']")
能正確返回1,應該是能夠了,咱們改下代碼看下效果。 而後和博客園首頁數據對比,結果吻合。
因此咱們能夠得出結論:
XPath搜索以//開頭時,會匹配全部的項,並非子項。
直屬子級能夠直接跟上 node名稱。
只想查子級的子級,能夠用*代替子級,實現模糊搜索。
改事後代碼以下:
public List<Article> ParseCnBlogs() { var url = "https://www.cnblogs.com"; HtmlWeb web = new HtmlWeb(); //1.支持從web或本地path加載html var htmlDoc = web.Load(url); var post_listnode = htmlDoc.DocumentNode.SelectSingleNode("//div[@id='post_list']"); //Console.WriteLine("Node Name: " + post_listnode.Name + "\n" + post_listnode.OuterHtml); var postitemsNodes = post_listnode.SelectNodes("div[@class='post_item']"); var articles = new List<Article>(); var digitRegex = @"[^0-9]+"; foreach (var item in postitemsNodes) { var article = new Article(); var diggnumnode = item.SelectSingleNode("*//span[@class='diggnum']"); //body var post_item_bodynode = item.SelectSingleNode("div[@class='post_item_body']"); var titlenode = post_item_bodynode.SelectSingleNode("*//a[@class='titlelnk']"); var summarynode = post_item_bodynode.SelectSingleNode("p[@class='post_item_summary']"); //foot var footnode = post_item_bodynode.SelectSingleNode("div[@class='post_item_foot']"); var authornode = footnode.ChildNodes[1]; var commentnode = footnode.SelectSingleNode("span[@class='article_comment']"); var viewnode = footnode.SelectSingleNode("span[@class='article_view']"); article.Diggit = int.Parse(diggnumnode.InnerText); article.Title = titlenode.InnerText; article.Url = titlenode.Attributes["href"].Value; article.Summary = titlenode.InnerHtml; article.Author = authornode.InnerText; article.AuthorUrl = authornode.Attributes["href"].Value; article.Comment = int.Parse(Regex.Replace(commentnode.ChildNodes[0].InnerText, digitRegex, "")); article.View = int.Parse(Regex.Replace(viewnode.ChildNodes[0].InnerText, digitRegex, "")); articles.Add(article); } return articles; }
感謝apgk也提供了一種辦法,也是ok的。
var titlenodes = post_item_bodynode.SelectNodes(post_item_bodynode.XPath+"//a[@class='titlelnk']");
demo到此結束。謝謝觀看!
下篇繼續構思如何構建自定義規則,讓用戶能夠在頁面本身填寫規則去識別。