1. 程式人生 > >.NetCore實踐爬蟲系統(一)解析網頁內容

.NetCore實踐爬蟲系統(一)解析網頁內容

爬蟲系統的意義

爬蟲的意義在於採集大批量資料,然後基於此進行加工/分析,做更有意義的事情。谷歌,百度,今日頭條,天眼查都離不開爬蟲。

今日目標

今天我們來實踐一個最簡單的爬蟲系統。根據Url來識別網頁內容。

網頁內容識別利器:HtmlAgilityPack

至今Nuget已有超過900多萬的下載量,應用量十分龐大。它提供的文件教程也十分簡單易用。

Parser解析器

HtmlParse可以讓你解析HTML並返回HtmlDocument

  • FromFile從檔案讀取
/// <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); }

Selectors選擇器

選擇器允許您從HtmlDocument中選擇HTML節點。它提供了兩個方法,可以用XPath表示式篩選節點。XPath教程

SelectNodes() 返回多個節點

SelectSingleNode(String) 返回單個節點

檢視網頁結構

我們以部落格園首頁為示例。用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語法

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;
        }

原始碼

總結

demo到此結束。謝謝觀看!

下篇繼續構思如何構建自定義規則,讓使用者可以在頁面自己填寫規則去識別。