.NetCore實踐爬蟲系統(二)自定義規則

回顧

上篇文章NetCore實踐爬蟲系統(一)解析網頁內容
我們講了利用HtmlAgilityPack,輸入XPath路徑,識別網頁節點,獲取我們需要的內容。評論中也得到了大家的一些支持與建議。下面繼續我們的爬蟲系統實踐之路。本篇文章不包含依賴注入/數據訪問/UI界面等,只包含核心的爬蟲相關知識,只能作爲Demo使用,拋磚引玉,共同交流。

抽象規則

爬蟲系統之所以重要,正是他能支持各種各樣的數據。要支持識別數據,第一步就是要將規則剝離出來,支持用戶自定義。

爬蟲規則,實際上是跟商品有點類似,如動態屬性,但也有它特殊的地方,如規則可以循環嵌套,遞歸,相互引用,鏈接可以無限下去抓取。更復雜的,就需要自然語言識別,語義分析等領域了。

我用PPT畫了個演示圖。用於演示支持分析文章,活動,天氣等各種類型的規則。

PPT示意圖

編碼實現

先來定義個採集規則接口,根據規則獲取單個或一批內容。

    /// <summary>
    /// 採集規則接口
    /// </summary>
    public interface IDataSplider
    {
        /// <summary>
        /// 得到內容
        /// </summary>
        /// <param name="rule"></param>
        /// <returns></returns>
        List<SpliderContent> GetByRule(SpliderRule rule);

        /// <summary>
        /// 得到屬性信息
        /// </summary>
        /// <param name="node"></param>
        /// <param name="rule"></param>
        /// <returns></returns>
        List<Field> GetFields(HtmlNode node, SpliderRule rule);
    }

必不可少的規則類,用來配置XPath根路徑。

 /// <summary>
    /// 採集規則-能滿足列表頁/詳情頁。
    /// </summary>
    public class SpliderRule
    {
        public string Id { get; set; }

        public string Url { get; set; }
        /// <summary>
        /// 網頁塊
        /// </summary>
        public string ContentXPath { get; set; }
        /// <summary>
        /// 支持列表式
        /// </summary>
        public string EachXPath { get; set; }
        /// <summary>
        /// 
        /// </summary>
        public List<RuleField> RuleFields { get; set; }
    }
    

然後就是屬性字段的自定義設置,這裏根據內容特性,加入了正則支持。例如評論數是數字,可用正則篩選出數字。還有Attribute字段,用來獲取node的Attribute信息。

/// <summary>
    /// 自定義屬性字段
    /// </summary>
    public class RuleField
    {
        public string Id { get; set; }

        public string DisplayName { get; set; }
        /// <summary>
        /// 用於存儲的別名
        /// </summary>
        public string FieldName { get; set; }
        public string XPath { get; set; }
        public string Attribute { get; set; }
        /// <summary>
        /// 針對獲取的HTml正則過濾
        /// </summary>
        public string InnerHtmlRegex { get; set; }
        /// <summary>
        /// 針對獲取的Text正則過濾
        /// </summary>
        public string InnerTextRegex { get; set; }
        /// <summary>
        /// 是否優先取InnerText
        /// </summary>
        public bool IsFirstInnerText { get; set; }

    }

下面是根據文章爬蟲規則的解析步驟,實現接口IDataSplider

/// <summary>
    /// 支持列表和詳情頁
    /// </summary>
    public class ArticleSplider : IDataSplider
    {
        /// <summary>
        /// 根據Rule
        /// </summary>
        /// <param name="rule"></param>
        /// <returns></returns>
        public List<SpliderContent> GetByRule(SpliderRule rule)
        {
            var url = rule.Url;
            HtmlWeb web = new HtmlWeb();
            //1.支持從web或本地path加載html
            var htmlDoc = web.Load(url);
            var contentnode = htmlDoc.DocumentNode.SelectSingleNode(rule.ContentXPath);

            var list = new List<SpliderContent>();
            //列表頁
            if (!string.IsNullOrWhiteSpace(rule.EachXPath))
            {
                var itemsNodes = contentnode.SelectNodes(rule.EachXPath);
                foreach (var item in itemsNodes)
                {
                    var fields = GetFields(item, rule);
                    list.Add(new SpliderContent()
                    {
                        Fields = fields,
                        SpliderRuleId = rule.Id
                    });
                }
                return list;
            }
            //詳情頁
            var cfields = GetFields(contentnode, rule);
            list.Add(new SpliderContent()
            {
                Fields = cfields,
                SpliderRuleId = rule.Id
            });
            return list;
        }

        public List<Field> GetFields(HtmlNode item, SpliderRule rule)
        {
            var fields = new List<Field>();

            foreach (var rulefield in rule.RuleFields)
            {
                var field = new Field() { DisplayName = rulefield.DisplayName, FieldName = "" };

                var fieldnode = item.SelectSingleNode(rulefield.XPath);
                if (fieldnode != null)
                {

                    field.InnerHtml = fieldnode.InnerHtml;
                    field.InnerText = fieldnode.InnerText;
                    field.AfterRegexHtml = !string.IsNullOrWhiteSpace(rulefield.InnerHtmlRegex) ? Regex.Replace(fieldnode.InnerHtml, rulefield.InnerHtmlRegex, "") : fieldnode.InnerHtml;
                    field.AfterRegexText = !string.IsNullOrWhiteSpace(rulefield.InnerTextRegex) ? Regex.Replace(fieldnode.InnerText, rulefield.InnerTextRegex, "") : fieldnode.InnerText;

                    //field.AfterRegexHtml = Regex.Replace(fieldnode.InnerHtml, rulefield.InnerHtmlRegex, "");
                    //field.AfterRegexText = Regex.Replace(fieldnode.InnerText, rulefield.InnerTextRegex, "");
                    if (!string.IsNullOrWhiteSpace(rulefield.Attribute))
                    {
                        field.Value = fieldnode.Attributes[rulefield.Attribute].Value;
                    }
                    else
                    {
                        field.Value = rulefield.IsFirstInnerText ? field.AfterRegexText : field.AfterRegexHtml;
                    }
                    }
                fields.Add(field);
            }
            return fields;
        }
    }

還是以博客園爲例,配置內容和屬性的自定義規則

        /// <summary>
        /// 
        /// </summary>
        public void RunArticleRule()
        {
            var postitembodyXPath = "div[@class='post_item_body']//";
            var postitembodyFootXPath = postitembodyXPath+ "div[@class='post_item_foot']//";
            var rule = new SpliderRule()
            {
                ContentXPath = "//div[@id='post_list']",
                EachXPath = "div[@class='post_item']",
                Url = "https://www.cnblogs.com",
                RuleFields = new List<RuleField>() {
                         new RuleField(){ DisplayName="推薦", XPath="*//span[@class='diggnum']", IsFirstInnerText=true },
                         new RuleField(){ DisplayName="標題",XPath=postitembodyXPath+"a[@class='titlelnk']", IsFirstInnerText=true },
                         new RuleField(){ DisplayName="URL",XPath=postitembodyXPath+"a[@class='titlelnk']",Attribute="href", IsFirstInnerText=true },
                         new RuleField(){ DisplayName="簡要",XPath=postitembodyXPath+"p[@class='post_item_summary']", IsFirstInnerText=true },
                         new RuleField(){ DisplayName="作者",XPath=postitembodyFootXPath+"a[@class='lightblue']", IsFirstInnerText=true },
                         new RuleField(){ DisplayName="作者URL",XPath=postitembodyFootXPath+"a[@class='lightblue']",Attribute="href", IsFirstInnerText=true },
                         new RuleField(){ DisplayName="討論數", XPath="span[@class='article_comment']",IsFirstInnerText=true, InnerTextRegex=@"[^0-9]+"  },
                         new RuleField(){ DisplayName="閱讀數", XPath=postitembodyFootXPath+"span[@class='article_view']",IsFirstInnerText=true, InnerTextRegex=@"[^0-9]+"  },
                    }
            };
            var splider = new ArticleSplider();
            var list = splider.GetByRule(rule);
            foreach (var item in list)
            {
                var msg = string.Empty;
                item.Fields.ForEach(M =>
                {
                    if (M.DisplayName != "簡要" && !M.DisplayName.Contains("URL"))
                    {
                        msg += $"{M.DisplayName}:{M.Value}";
                    }
                });
                Console.WriteLine(msg);
            }
        }
        

運行效果

效果完美!

運行效果圖

經過簡單的重構,我們已經達到了上篇的效果。

常用規則模型和自定義規則模型

寫到這裏,我想到了一般UML圖工具或Axsure原型等,都會內置各種常用組件,那麼文章爬蟲模型也是我們內置的一種常用組件了。後續我們完全可以按照上面的套路支持其他模型。除了常用模型之外,在網頁或客戶端上,高級的爬蟲工具會支持用戶自定義配置,根據配置來獲取內容。

上面的SpliderRule已經能支持大部分內容管理系統單頁面抓取。但無法支持規則相互引用,然後根據抓取的內容引用配置規則繼續抓取。(這裏也許有什麼專門的名詞來描述:遞歸爬蟲?)。

今天主要是在上篇文章的基礎上重構而來,支持了規則配置。爲了有點新意,就多提供兩個配置例子吧。

例子1:文章詳情

我們以上篇文章爲例,獲取文章詳情。
主要結點是標題,內容。其他額外屬性暫不處理。

文章詳情結構圖

編碼實現

        /// <summary>
        /// 詳情
        /// </summary>
        public void RunArticleDetail() {
           

            var rule = new SpliderRule()
            {
                ContentXPath = "//div[@id='post_detail']",
                EachXPath = "",
                Url = " https://www.cnblogs.com/fancunwei/p/9581168.html",
                RuleFields = new List<RuleField>() {
                         new RuleField(){ DisplayName="標題",XPath="*//div[@class='post']//a[@id='cb_post_title_url']", IsFirstInnerText=true },
                         new RuleField(){ DisplayName="詳情",XPath="*//div[@class='postBody']//div[@class='blogpost-body']",Attribute="", IsFirstInnerText=false }
                           }
            };
            var splider = new ArticleSplider();
            var list = splider.GetByRule(rule);
            foreach (var item in list)
            {
                var msg = string.Empty;
                item.Fields.ForEach(M =>
                {
                    Console.WriteLine($"{M.DisplayName}:{M.Value}");
                });
                Console.WriteLine(msg);
            }
        }

運行效果

效果同樣完美!

文章詳情

例子2:天氣預報

天氣預報的例子,我們就以上海8-15天預報爲例

分析結構

點擊鏈接,我們發現 今天/7天/8-15天/40天分別是不同的路由頁面,那就簡單了,我們只考慮當前頁面就行。還有個問題,那個晴天雨天的圖片,是按樣式顯示的。我們雖然能抓到html,但樣式還未考慮,,HtmlAgilityPack應該有個從WebBrowser獲取網頁的,似乎能支持樣式。本篇文章先跳過這個問題,以後再細究。

上海8-15預報網頁結構分析圖

配置規則

根據網頁結構,配置對應規則。

 public void RunWeather() {

            var rule = new SpliderRule()
            {
                ContentXPath = "//div[@id='15d']",
                EachXPath = "*//li",
                Url = "http://www.weather.com.cn/weather15d/101020100.shtml",
                RuleFields = new List<RuleField>() {
                         new RuleField(){ DisplayName="日期",XPath="span[@class='time']", IsFirstInnerText=true },
                         new RuleField(){ DisplayName="天氣",XPath="span[@class='wea']",Attribute="", IsFirstInnerText=false },
                         new RuleField(){ DisplayName="區間",XPath="span[@class='tem']",Attribute="", IsFirstInnerText=false },
                         new RuleField(){ DisplayName="風向",XPath="span[@class='wind']",Attribute="", IsFirstInnerText=false },
                         new RuleField(){ DisplayName="風力",XPath="span[@class='wind1']",Attribute="", IsFirstInnerText=false },
                           }
            };
            var splider = new ArticleSplider();
            var list = splider.GetByRule(rule);
            foreach (var item in list)
            {
                var msg = string.Empty;
                item.Fields.ForEach(M =>
                {
                        msg += $"{M.DisplayName}:{M.Value} ";
                });
                Console.WriteLine(msg);
            }

        }

運行效果

效果再次完美!

天氣預報獲取圖

源碼

上述代碼已提交到GitHub

總結探討

綜上所述,我們實現單頁面的自定義規則,但也遺留了一個小問題。天氣預報晴天陰天效果圖,原文是用樣式展示的。針對這種不規則問題,如果代碼定製當然很容易,但如果做成通用,有什麼好辦法呢?請提出你的建議!心情好的,順便點個推薦…

下篇文章,繼續探討多頁面/遞歸爬蟲自定義規則的實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章