一、遲到的下期預告
自從上一篇文章發布到現在,大約差不多有3個月的樣子,其實一直想把這個實戰入門系列的教程寫完,一個是為了支持DotnetSpider,二個是為了.Net 社區發展獻出一份綿薄之力,這個開源項目作者一直都在更新,相對來說還是很不錯的,上次教程的版本還是2.4.4,今天瀏覽了一下這個項目,最近一次更新是在3天前,已經更新到了2.5.0,而且項目star也已經超過1000了,還是挺受大家所喜愛的,也在這感謝作者們不斷的努力。
之所以中間這么長一段時間沒有好好寫文章,是因為筆者為參加3月份PMP考試備考了,然后考完差不多就到4月份,到了4月份,項目催的急,所以一直拖到了現在才有一點空余時間,希望我的文章完成之后,會對大家有一定的幫助。
二、更新DotnetSpider庫
剛剛提到DotnetSpider升級到了2.5.0,所以我們更新一下庫,使用最新的版本,技術要與時俱進嘛,將下圖中兩個類庫添加進去就行了。
三、分析汽車之家商品詳情頁面
3.1分析商品詳情頁數據
①上次我們發現,當點擊參數配置的時候,頁面會發送兩個ajax請求,分別去獲取車型的基本參數,和配置參數,返回的數據是JSON格式。
②通過Chrome的Network抓包可以發現,這兩個請求有一個共同點,提交的參數都有data[specid]:28762,我猜測這應該是skuid,大家可以試着在瀏覽器直接打開這兩個地址,直接就可以返回出車型的相關信息,所以問題的關鍵就是要解決如何獲取skuid的問題,獲取到了這些車型的數據那不就是手到擒來了。
3.2如何獲取商品的sku
其實這個確實是讓我很苦惱的一個問題,因為我們打開頁面的時候頁面的鏈接中並未包含skuid,[https://mall.autohome.com.cn/detail/284641-0-0.html],所以從URL中獲取是不太現實的了,所以我用Element去頁面中搜索這個skuid的值,結果發現兩個地方有這個值,一個存在於HTML元素中,一個是存在js全局變量中,相比較而言,我認為HTML元素中的相對來說會比較好處理一點,直接獲取元素的屬性就行了。
四、開發
4.1准備Processor
這次為什么要單獨的寫一段准備Processor呢,因為此次准備了三個Processor,分別用來處理3個數據,第一個用於獲取skuid,第二個用於獲取車型基本參數,第三個用於獲取車型配置,細心的小伙伴們肯定會發現,這次我們每個Processor都使用了構造函數,里面可以清晰的看到有我們熟悉的正則表達式(PS正則表達式寫的很爛,自己有更好的正則寫法可以回復在評論里面,讓我膜拜一下,嘻嘻),肯定會有人問為什么要這么寫呢?
相比上次的教程,這次的抓取流程更為的復雜了,上次我們只是抓了一個列表頁,一個接口完全可以搞定,此次我們的流程變成了,第一步,我們需要獲取車型詳情頁的頁面,從頁面中找到skuid,第二部,將獲取的skuid拼接好request放入爬蟲的請求集合中,通過新構造的請求去獲取數據,那么我們怎么知道哪個請求用哪個Processor進行處理呢,DotnetSpider提供了對url進行校驗的進行判斷,使用哪個Processor對數據進行處理。
private class GetSkuProcessor : BasePageProcessor //獲取skuid { public GetSkuProcessor() { TargetUrlsExtractor = new RegionAndPatternTargetUrlsExtractor(".", @"^https:\/\/mall.autohome.com.cn\/detail\/*[0-9]+-[0-9]+-[0-9]+\.html$"); } protected override void Handle(Page page) { string skuid = string.Empty; skuid = page.Selectable.XPath(".//a[@class='carbox-compare_detail']/@link").GetValue(); page.AddResultItem("skuid", skuid); page.AddTargetRequest(@"https://mall.autohome.com.cn/http/data.html?data[_host]=//car.api.autohome.com.cn/v1/carprice/spec_paramsinglebyspecid.ashx&data[_appid]=mall&data[specid]=" + skuid); page.AddTargetRequest(@"https://mall.autohome.com.cn/http/data.html?data[_host]=//car.api.autohome.com.cn/v2/carprice/Config_GetListBySpecId.ashx&data[_appid]=mall&data[specid]=" + skuid); } } private class GetBasicInfoProcessor : BasePageProcessor //獲取車型基本參數 { public GetBasicInfoProcessor() { TargetUrlsExtractor = new RegionAndPatternTargetUrlsExtractor(".", @"^https://mall\.autohome\.com\.cn/http/data\.html\?data\[_host\]=//car\.api\.autohome\.com\.cn/v1/carprice/spec_paramsinglebyspecid\.ashx*"); } protected override void Handle(Page page) { page.AddResultItem("BaseInfo", page.Content); } } private class GetExtInfoProcessor : BasePageProcessor //獲取車型配置 { public GetExtInfoProcessor() { TargetUrlsExtractor = new RegionAndPatternTargetUrlsExtractor(".", @"^https://mall\.autohome\.com\.cn\/http\/data\.html\?data\[_host\]=//car\.api\.autohome\.com\.cn/v2/carprice/Config_GetListBySpecId\.ashx*"); } protected override void Handle(Page page) { page.AddResultItem("ExtInfo", page.Content); } }
4.2、創建Pipeline
Pipeline基本上變化不大,稍微改造了一下,so easy。
private class PrintSkuPipe : BasePipeline { public override void Process(IEnumerable<ResultItems> resultItems, ISpider spider) { foreach (var resultItem in resultItems) { if (resultItem.GetResultItem("skuid") != null) { Console.WriteLine(resultItem.Results["skuid"] as string); } if (resultItem.GetResultItem("BaseInfo") != null) { var t = JsonConvert.DeserializeObject<AutoCarParam>(resultItem.Results["BaseInfo"]); //Console.WriteLine(resultItem.Results["BaseInfo"]); } if (resultItem.GetResultItem("ExtInfo") != null) { var t = JsonConvert.DeserializeObject<AutoCarConfig>(resultItem.Results["ExtInfo"]); //Console.WriteLine(resultItem.Results["ExtInfo"]); } } } }
新增兩個實體類AutoCarParam,AutoCarConfig,其實有重復的,子項可以再進行抽象一下,代碼可以減少,還可以節省一點硬盤空間
public class AutoCarConfig { public string message { get; set; } public ConfigResult result { get; set; } public string returncode { get; set; } } public class ConfigResult { public string specid { get; set; } public List<configtypeitem> configtypeitems { get; set; } } public class configtypeitem { public string name { get; set; } public List<configitem> configitems { get; set; } } public class configitem { public string name { get; set; } public string value { get; set; } } public class AutoCarParam { public string message { get; set; } public ParamResult result { get; set; } public string returncode { get; set; } } public class ParamResult { public string specid { get; set; } public List<paramtypeitem> paramtypeitems { get; set; } } public class paramtypeitem { public string name { get; set; } public List<paramitem> paramitems { get;set;} } public class paramitem { public string name { get; set; } public string value { get; set; } }
4.3、構造爬蟲
這一塊變化也不是很大,變化的的地方看我的注釋,因為我們需要有多個Processor,把這幾個都添加進去就行。
var site = new Site { CycleRetryTimes = 1, SleepTime = 200, Headers = new Dictionary<string, string>() { { "Accept","text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8" }, { "Cache-Control","no-cache" }, { "Connection","keep-alive" }, { "Content-Type","application/x-www-form-urlencoded; charset=UTF-8" }, { "User-Agent","Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36"} } }; List<Request> resList = new List<Request>(); Request res = new Request(); res.Url = "https://mall.autohome.com.cn/detail/284641-0-0.html"; res.Method = System.Net.Http.HttpMethod.Get; resList.Add(res); var spider = Spider.Create(site, new QueueDuplicateRemovedScheduler(), new GetSkuProcessor(),new GetBasicInfoProcessor(),new GetExtInfoProcessor()) //因為我們有多個Processor,所以都要添加進來 .AddStartRequests(resList.ToArray()) .AddPipeline(new PrintSkuPipe()); spider.ThreadNum = 1; spider.Run(); Console.Read();
五、執行結果
六、總結
這次憋了那么久才寫第二篇文章的,實屬慚愧,本來2月底這篇文章就要出來的,一直拖到現在,上一篇文章的閱讀量整體來說還是不錯的超出了我的預期,下面評論也有小伙伴希望我快點出這篇文章,整體來說還是不錯的。這次的文章我希望就是說不僅僅是大家學會了如何去使用DotnetSpider,並且能夠讓大家了解一下如何爬數據,給大家提供一點點思路,所以我會結合實際場景來寫這篇文章,不然的話,感覺會太過於枯燥了。
希望大家多多拍磚
七、下期預告
下次文章會涉及文件抓取
順便說一句,有需要參加PMP或者高項考試的可以聯系我,我有一些資料可以提供
2018年5月13日