記一次企業級爬蟲系統升級改造(二):基於AngleSharp實現的抓取服務

 

  爬蟲系統升級改造正式啓動:html

    在第一篇文章,博主主要介紹了本次改造的爬蟲系統的業務背景與全局規劃構思:程序員

    將來Support雲系統,不只僅是爬蟲系統,是集爬取數據、數據建模處理統計分析、支持全文檢索資源庫、其餘業務部門和公司資訊系統重要數據來源、輔助決策等功能於一身的企業級Support系統。正則表達式

    介於好多園友對博主的任務排期表感興趣,便介紹一下博主當時針對這個系統作的工做任務排期概要(排期表就是更加詳細細分外加估算工時的一份excel表格,就不貼出來了):sql

      1.總分四大階段,逐步上線,最終達到預期規劃數據庫

      2.第一階段實現一個新的採集系統,自動實時化爬取數據、初步規則引擎實現數據規則化、統計郵件自動推送、開放數據檢索,並上線替換原有爬蟲系統windows

      3.第二階段實現規則化引擎升級,擴展成長式規則引擎,並開放採集源提交、管理、規則配置、基礎數據服務等基本系統操做設計模式

      4.第三階段引入全文檢索,針對規則化數據建立索引,提供數據全文搜索功能,開放工單申請,可定製數據報告服務器

      5.第四階段引入數據報表功能,開放統計分析結果,並向輿情監控與決策支持方向擴展框架

    固然,在博主未爭取到更多資源的狀況下,第一階段的排期要求了一個月,後面各階段只作了功能規劃,並未作時間排期。分佈式

    這也算是一個小手段吧,畢竟第一階段上線,boss們是可能提不少其餘意見,或者遇到其餘任務安排的,不能一開始就把時間節點寫死,否則最終受傷的多是程序員本身。

你比他好一點,他不會認可你,反而會嫉妒你,只有你比他好不少,他纔會認可你,而後還會很崇拜你,因此要作,就必定要比別人作得好不少。

 

  代碼框架搭建:

    雖然你們都對個人「SupportYun」命名很有異議,可是我依然我行我素,哈哈~~~總感受讀起來很和諧

    先上一張截止今天,項目結構的總體圖:

    博主一直很喜好DDD的設計模式,也在不少項目中引用了一些經典DDD模式的框架,可是明顯此次的任務是不適合作DDD的。

    引入了EF Code First作數據持久化,未引入相關的各類操做擴展,此次打算純拉姆達表達式來寫,畢竟吃多了葷的,偶爾也想嘗幾口素,調劑調劑口味~

    兩個WinServices分別是爬蟲服務與規則化引擎服務。全文檢索相關因爲近期不會涉及,故暫未引入,相信其餘的類庫你們看命名就明白是幹什麼的了。

 一匹真正的好馬,即便沒有伯樂賞識,也能飛奔千里。

 

  爬蟲服務剖析:

      1.先來看Support.Domain,sorrry,原諒我對DDD愛得深沉,老是喜歡用Domain這個命名。

      Basic和Account是一些常規表模型,就不一一介紹了。

      順帶給你們共享一份一直在用的全國省市縣數據sql,下載地址(不要積分,放心下載):http://download.csdn.net/detail/cb511612371/9700143

      Migrations熟悉EF的都應該知道,是DB遷移文件夾,每次模型有所改變,直接命令行執行,生成遷移文件,update數據庫就OK了。命令行以下:

        a)Enable-Migrations -ProjectName EFModel命名空間
          -- 開啓數據遷移(開啓後,該類庫下會生成Migrations文件夾,無需屢次開啓)

        b)Add-Migration Name -ProjectName EFModel命名空間
          -- 添加數據遷移方案(指定一個名稱,添加後會在Migrations文件夾下生成對應遷移方案代碼)

        c)Update-Database -ProjectName EFModel命名空間
          -- 執行數據遷移方案(匹配數據庫遷移方案,修改數據庫)

      再來看爬蟲服務的模型:

      

      博主設計了四張表來處理爬蟲服務,分別存儲採集源<-1:n->採集規則<-1:n->初始採集數據,規則分組(主要用於將執行間隔相同的規則分爲一組,以便後期抓取任務量大時,拆分服務部署)

      

      2.再來看SupportYun.GrabService,顧名思義,這就是咱們爬蟲抓取服務的核心邏輯所在。

       

      因爲時間緊急,博主當前只作了使用AngleSharp來抓取的服務,之後會逐步擴充基於正則表達式以及其餘第三方組件的抓取服務。

      CrawlerEngineService 是爬蟲服務的對外引擎,全部爬取任務都應該是啓動它來執行爬取。

      其實,爬取別人網頁服務的本質很簡單,就是一個獲取html頁面,而後解析的過程。那麼咱們來看看針對博主的模型設計,具體又該是怎樣一個流程:

      能夠看到,博主目前是在爬蟲引擎裏面循環全部的規則分組,當之後規則擴張,抓取頻率多樣化後,能夠分佈式部署多套任務框架,指定各自的任務規則組來啓動引擎,便可達到面向服務的任務分流效果。

      3.最後,咱們須要建立一個Windows服務來作任務調度(博主當前使用的比較簡單,引入其餘任務調度框架來作也是能夠的哈~),它就是:SupportYun.CrawlerWinServices

      

      windows服務裏面的邏輯就比較簡單啦,就是起到一個定時循環執行任務的效果,直接上核心代碼:      

 1     public partial class Service1 : ServiceBase
 2     {
 3         private CrawlerEngineService crawlerService=new CrawlerEngineService();
 4 
 5         public Service1()
 6         {
 7             InitializeComponent();
 8         }
 9 
10         protected override void OnStart(string[] args)
11         {
12             try
13             {
14                 EventLog.WriteEntry("【Support雲爬蟲服務啓動】");
15                 CommonTools.WriteLog("【Support雲爬蟲服務啓動】");
16 
17                 Timer timer = new Timer();
18                 // 循環間隔時間(默認5分鐘)
19                 timer.Interval = StringHelper.StrToInt(ConfigurationManager.AppSettings["TimerInterval"].ToString(), 300) * 1000;
20                 // 容許Timer執行
21                 timer.Enabled = true;
22                 // 定義回調
23                 timer.Elapsed += new ElapsedEventHandler(TimedTask);
24                 // 定義屢次循環
25                 timer.AutoReset = true;
26             }
27             catch (Exception ex)
28             {
29                 CommonTools.WriteLog("【服務運行 OnStart:Error" + ex + "");
30             }
31         }
32 
33         private void TimedTask(object source, System.Timers.ElapsedEventArgs e)
34         {
35             System.Threading.ThreadPool.QueueUserWorkItem(delegate
36             {
37                 crawlerService.Main();
38             });
39         }
40 
41         protected override void OnStop()
42         {
43             CommonTools.WriteLog(("【Support雲爬蟲服務中止】"));
44             EventLog.WriteEntry("【Support雲爬蟲服務中止】");
45         }
46     }

      第35行是啓用了線程池,放進隊列的是爬蟲抓取引擎服務的啓動方法。

      windows服務的具體部署,相信你們都會,園子裏也有不少園友寫過相關文章,就不詳細解釋了。

      4.那麼咱們再來梳理一下當前博主整個爬蟲服務的總體流程:

不論對錯,只要你敢思考,並付諸行動,你就能夠被稱爲「軟件工程師」,而再也不是「碼農」。

 

  爬取服務核心代碼:

    上面說的都是博主針對整個系統爬蟲服務的梳理與設計。最核心的固然仍是咱們最終實現的代碼。

一切不以最終實踐爲目的的構思設計,都是耍流氓。

    咱們首先從看看抓取服務引擎的啓動方法:

 1         public void Main()
 2         {
 3             using (var context = new SupportYunDBContext())
 4             {
 5                 var groups = context.RuleGroup.Where(t => !t.IsDelete).ToList();
 6                 foreach (var group in groups)
 7                 {
 8                     try
 9                     {
10                         var rules =
11                             context.CollectionRule.Where(r => !r.IsDelete && r.RuleGroup.Id == group.Id).ToList();
12                         if (rules.Any())
13                         {
14                             foreach (var rule in rules)
15                             {
16                                 if (CheckIsAllowGrab(rule))
17                                 {
18                                     // 目前只開放AngleSharp方式抓取
19                                     if (rule.CallScriptType == CallScriptType.AngleSharp)
20                                     {
21                                         angleSharpGrabService.OprGrab(rule.Id);
22                                     }
23                                 }
24                             }
25                         }
26                     }
27                     catch (Exception ex)
28                     {
29                         // TODO:記錄日誌
30                         continue;
31                     }
32                 }
33             }
34         }

    上面說了,當前只考慮一個爬蟲服務,故在這兒循環了全部規則組。

    第16行主要是校驗規則是否容許抓取(根據記錄的上次抓取時間和所在規則組的抓取頻率作計算)。

    咱們看到,引擎服務只起到一個調度具體抓取服務的做用。那麼咱們來看看具體的AngleSharpGrabService,基於AngleSharp的抓取服務:

    IsRepeatedGrab 這個方法應該是抽象類方法,博主就不換圖了哈。

    它對外暴露的是一個OprGrab抓取方法:

 1         /// <summary>
 2         /// 抓取操做
 3         /// </summary>
 4         /// <param name="ruleId">規則ID</param>
 5         public void OprGrab(Guid ruleId)
 6         {
 7             using (var context = new SupportYunDBContext())
 8             {
 9                 var ruleInfo = context.CollectionRule.Find(ruleId);
10                 if (ruleInfo == null)
11                 {
12                     throw new Exception("抓取規則已不存在!");
13                 }
14 
15                 // 獲取列表頁
16                 string activityListHtml = this.GetHtml(ruleInfo.WebListUrl, ruleInfo.GetCharset());
17 
18                 // 加載HTML
19                 var parser = new HtmlParser();
20                 var document = parser.Parse(activityListHtml);
21 
22                 // 獲取列表
23                 var itemList = this.GetItemList(document, ruleInfo.ListUrlRule);
24 
25                 // 讀取詳情頁信息
26                 foreach (var element in itemList)
27                 {
28                     List<UrlModel> urlList = GetUrlList(element.InnerHtml);
29                     foreach (UrlModel urlModel in urlList)
30                     {
31                         try
32                         {
33                             var realUrl = "";
34                             if (urlModel.Url.Contains("http"))
35                             {
36                                 realUrl = urlModel.Url;
37                             }
38                             else
39                             {
40                                 string url = urlModel.Url.Replace(ruleInfo.CollectionSource.SourceUrl.Trim(), "");
41                                 realUrl = ruleInfo.CollectionSource.SourceUrl.Trim() + url;
42                             }
43 
44                             if (!IsRepeatedGrab(realUrl, ruleInfo.Id))
45                             {
46                                 string contentDetail = GetHtml(realUrl, ruleInfo.GetCharset());
47                                 var detailModel = DetailAnalyse(contentDetail, urlModel.Title, ruleInfo);
48 
49                                 if (!string.IsNullOrEmpty(detailModel.FullContent))
50                                 {
51                                     var ruleModel = context.CollectionRule.Find(ruleInfo.Id);
52                                     ruleModel.LastGrabTime = DateTime.Now;
53                                     var newData = new CollectionInitialData()
54                                     {
55                                         CollectionRule = ruleModel,
56                                         CollectionType = ruleModel.CollectionType,
57                                         Title = detailModel.Title,
58                                         FullContent = detailModel.FullContent,
59                                         Url = realUrl,
60                                         ProcessingProgress = ProcessingProgress.未處理
61                                     };
62                                     context.CollectionInitialData.Add(newData);
63                                     context.SaveChanges();
64                                 }
65                             }
66 
67                         }
68                         catch
69                         {
70                             // TODO:記錄日誌
71                             continue;
72                         }
73                     }
74                 }
75             }
76         }

    第16行用到的GetHtml()方法,來自於它所繼承的抓取基類BaseGrabService:

    具體代碼以下: 

  1     /// <summary>
  2     /// 抓取服務抽象基類
  3     /// </summary>
  4     public abstract class BaseGrabService
  5     {
  6         /// <summary>
  7         /// 線程休眠時間 毫秒
  8         /// </summary>
  9         private readonly static  int threadSleepTime = 1000;
 10 
 11         /// <summary>
 12         /// 加載指定頁面
 13         /// </summary>
 14         /// <param name="url">加載地址</param>
 15         /// <param name="charsetType">編碼集</param>
 16         /// <returns></returns>
 17         public string GetHtml(string url, string charsetType)
 18         {
 19             string result = null;
 20             HttpHelper httpHelper = new HttpHelper();
 21             result = httpHelper.RequestResult(url, "GET", charsetType);
 22             result = ConvertCharsetUTF8(result);
 23             
 24             // 簡單的休眠,防止IP被封
 25             // TODO:後期視狀況作更進一步設計
 26             Thread.Sleep(threadSleepTime);
 27             return result;
 28         }
 29 
 30         /// <summary>
 31         /// 強制將html文本內容轉碼爲UTF8格式
 32         /// </summary>
 33         /// <param name="strHtml"></param>
 34         /// <returns></returns>
 35         public string ConvertCharsetUTF8(string strHtml)
 36         {
 37             if (!strHtml.Contains("Content-Type") && !strHtml.Contains("gb2312"))
 38             {
 39                 if (strHtml.Contains("<title>"))
 40                 {
 41                     strHtml = strHtml.Insert(strHtml.IndexOf("<title>", StringComparison.Ordinal), "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">");
 42                 }
 43             }
 44             else
 45             {
 46                 strHtml = strHtml.Replace("gb2312", "utf-8").Replace("gbk", "utf-8");
 47             }
 48             return strHtml;
 49         }
 50 
 51         /// <summary>
 52         /// 根據規則,從html中返回匹配結果
 53         /// </summary>
 54         /// <param name="doc">html doc</param>
 55         /// <param name="rule">規則</param>
 56         /// <returns></returns>
 57         public IEnumerable<IElement> GetItemList(IDocument doc,string rule)
 58         {
 59             var itemList = doc.All.Where(m => m.Id == rule.Trim());
 60             if (!itemList.Any())
 61             {
 62                 itemList = doc.All.Where(m => m.ClassName == rule.Trim());
 63             }
 64             return itemList;
 65         }
 66 
 67         /// <summary>
 68         /// 獲取列表項中的url實體
 69         /// </summary>
 70         /// <returns></returns>
 71         public List<UrlModel> GetUrlList(string strItems)
 72         {
 73             List<UrlModel> itemList = new List<UrlModel>();
 74             Regex reg = new Regex(@"(?is)<a[^>]*?href=(['""]?)(?<url>[^'""\s>]+)\1[^>]*>(?<text>(?:(?!</?a\b).)*)</a>");
 75             MatchCollection mc = reg.Matches(strItems);
 76             foreach (Match m in mc)
 77             {
 78                 UrlModel urlModel = new UrlModel();
 79                 urlModel.Url = m.Groups["url"].Value.Trim().Replace("amp;", "");
 80                 urlModel.Title = m.Groups["text"].Value.Trim();
 81                 itemList.Add(urlModel);
 82             }
 83 
 84             return itemList;
 85         }
 86     }
 87 
 88     /// <summary>
 89     /// URL對象
 90     /// </summary>
 91     public class UrlModel
 92     {
 93         /// <summary>
 94         /// 鏈接地址
 95         /// </summary>
 96         public string Url { get; set; }
 97 
 98         /// <summary>
 99         /// 鏈接Title
100         /// </summary>
101         public string Title { get; set; }
102     }
103 
104     /// <summary>
105     /// 詳情內容對象
106     /// </summary>
107     public class DetailModel
108     {
109         /// <summary>
110         /// title
111         /// </summary>
112         public string Title { get; set; }
113 
114         /// <summary>
115         /// 內容
116         /// </summary>
117         public string FullContent { get; set; }
118     }
View Code

    注意AngleSharpGrabService的OprGrab方法第33行至42行,在作url的構建。由於咱們抓取到的a標籤的href屬性極可能是相對地址,在這裏咱們須要作判斷替換成絕對地址。

    具體邏輯你們能夠參考上面的爬取流程圖。

    OprGrab方法的第47行即從抓取的具體詳情頁html中獲取詳情數據(目前主要獲取title和帶html標籤的內容,具體清理與分析由規則化引擎來完成)。

    具體實現代碼並沒有太多養分,和抓取列表頁幾乎一致:構建document對象,經過規則匹配出含有title的html片斷和含有內容的html片斷,再對title進行html標籤清洗。

    具體清洗一個html文本html標籤的方法已經屬於規則化引擎的範疇,容博主下一篇寫規則化引擎服務的時候再來貼出並給你們做分析。

 

    這時候,咱們部署在服務器上的windows服務就能按咱們配好的規則進行初始數據抓取入庫了。

    貼一張博主當前測試抓取的數據截圖:

 

    博主終於算是完成了系統的第一步,接下來就是規則化引擎分析FullContent裏面的數據了。

    博主爭取本週寫完規則化引擎相關的代碼,下週再來分享給你們哈!

    但是答應了一個月時間要作好第一階段的全部內容並上線呢,哎~~~敲代碼去

硬的怕橫的,橫的怕不要命的,瘋子都是不要命的,因此瘋子力量大,程序員只有一種,瘋狂的程序員。

    共勉!!!

 

原創文章,代碼都是從本身項目裏貼出來的。轉載請註明出處哦,親~~~

相關文章
相關標籤/搜索