爬蟲系統升級改造正式啓動: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 }
注意AngleSharpGrabService的OprGrab方法第33行至42行,在作url的構建。由於咱們抓取到的a標籤的href屬性極可能是相對地址,在這裏咱們須要作判斷替換成絕對地址。
具體邏輯你們能夠參考上面的爬取流程圖。
OprGrab方法的第47行即從抓取的具體詳情頁html中獲取詳情數據(目前主要獲取title和帶html標籤的內容,具體清理與分析由規則化引擎來完成)。
具體實現代碼並沒有太多養分,和抓取列表頁幾乎一致:構建document對象,經過規則匹配出含有title的html片斷和含有內容的html片斷,再對title進行html標籤清洗。
具體清洗一個html文本html標籤的方法已經屬於規則化引擎的範疇,容博主下一篇寫規則化引擎服務的時候再來貼出並給你們做分析。
這時候,咱們部署在服務器上的windows服務就能按咱們配好的規則進行初始數據抓取入庫了。
貼一張博主當前測試抓取的數據截圖:
博主終於算是完成了系統的第一步,接下來就是規則化引擎分析FullContent裏面的數據了。
博主爭取本週寫完規則化引擎相關的代碼,下週再來分享給你們哈!
但是答應了一個月時間要作好第一階段的全部內容並上線呢,哎~~~敲代碼去
硬的怕橫的,橫的怕不要命的,瘋子都是不要命的,因此瘋子力量大,程序員只有一種,瘋狂的程序員。
共勉!!!
原創文章,代碼都是從本身項目裏貼出來的。轉載請註明出處哦,親~~~