爬蟲系統升級改造正式啟動:
在第一篇文章,博主主要介紹了本次改造的爬蟲系統的業務背景與全局規划構思:
未來Support雲系統,不僅僅是爬蟲系統,是集爬取數據、數據建模處理統計分析、支持全文檢索資源庫、其他業務部門和公司資訊系統重要數據來源、輔助決策等功能於一身的企業級Support系統。
介於好多園友對博主的任務排期表感興趣,便介紹一下博主當時針對這個系統做的工作任務排期概要(排期表就是更加詳細細分外加估算工時的一份excel表格,就不貼出來了):
1.總分四大階段,逐步上線,最終達到預期規划
2.第一階段實現一個新的采集系統,自動實時化爬取數據、初步規則引擎實現數據規則化、統計郵件自動推送、開放數據檢索,並上線替換原有爬蟲系統
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里面的數據了。
博主爭取本周寫完規則化引擎相關的代碼,下周再來分享給大家哈!
可是答應了一個月時間要做好第一階段的所有內容並上線呢,哎~~~敲代碼去
硬的怕橫的,橫的怕不要命的,瘋子都是不要命的,所以瘋子力量大,程序員只有一種,瘋狂的程序員。
共勉!!!
原創文章,代碼都是從自己項目里貼出來的。轉載請注明出處哦,親~~~