上一篇(http://www.javashuo.com/article/p-mvnspptf-gm.html)文章使用AutoMapper來處理對象與對象之間的映射關係,本篇主要圍繞定時任務和數據抓取相關的知識點並結合實際應用,在定時任務中循環處理爬蟲任務抓取數據。html
開始以前能夠刪掉以前測試用的幾個HelloWorld,沒有什麼實際意義,直接幹掉吧。抓取數據我主要用到了,HtmlAgilityPack
和PuppeteerSharp
,通常狀況下HtmlAgilityPack
就能夠完成大部分的數據抓取需求了,當在抓取動態網頁的時候能夠用到PuppeteerSharp
,同時PuppeteerSharp
還支持將圖片保存爲圖片和PDF等牛逼的功能。git
關於這兩個庫就很少介紹了,不瞭解的請自行去學習。github
先在.BackgroundJobs
層安裝兩大神器:Install-Package HtmlAgilityPack
、Install-Package PuppeteerSharp
。我在使用Package Manager安裝包的時候通常都不喜歡指定版本號,由於這樣默認是給我安裝最新的版本。web
以前無心中發現愛思助手的網頁版有不少手機壁紙(https://www.i4.cn/wper_4_0_1_1.html),因而我就動了當心思,把全部手機壁紙所有抓取過來自嗨,能夠看看我我的博客中的成品吧:https://meowv.com/wallpaper 😝😝😝數據庫
最開始我是用Python實現的,如今咱們在.NET中抓它。瀏覽器
我數了一下,一共有20個分類,直接在.Domain.Shared
層添加一個壁紙分類的枚舉WallpaperEnum.cs
。多線程
//WallpaperEnum.cs using System.ComponentModel; namespace Meowv.Blog.Domain.Shared.Enum { public enum WallpaperEnum { [Description("美女")] Beauty = 1, [Description("型男")] Sportsman = 2, [Description("萌娃")] CuteBaby = 3, [Description("情感")] Emotion = 4, [Description("風景")] Landscape = 5, [Description("動物")] Animal = 6, [Description("植物")] Plant = 7, [Description("美食")] Food = 8, [Description("影視")] Movie = 9, [Description("動漫")] Anime = 10, [Description("手繪")] HandPainted = 11, [Description("文字")] Text = 12, [Description("創意")] Creative = 13, [Description("名車")] Car = 14, [Description("體育")] PhysicalEducation = 15, [Description("軍事")] Military = 16, [Description("節日")] Festival = 17, [Description("遊戲")] Game = 18, [Description("蘋果")] Apple = 19, [Description("其它")] Other = 20, } }
查看原網頁能夠很清晰的看到,每個分類對應了一個不一樣的URL,因而手動建立一個抓取的列表,列表內容包括URL和分類,而後我又想用多線程來訪問URL,返回結果。新建一個通用的待抓項的類,起名爲:WallpaperJobItem.cs
,爲了規範和後續的壁紙查詢接口,咱們放在.Application.Contracts
層中。app
//WallpaperJobItem.cs using Meowv.Blog.Domain.Shared.Enum; namespace Meowv.Blog.Application.Contracts.Wallpaper { public class WallpaperJobItem<T> { /// <summary> /// <see cref="Result"/> /// </summary> public T Result { get; set; } /// <summary> /// 類型 /// </summary> public WallpaperEnum Type { get; set; } } }
WallpaperJobItem<T>
接受一個參數T,Result的類型由T決定,在.BackgroundJobs
層Jobs文件夾中新建一個任務,起名叫作:WallpaperJob.cs
吧。老樣子,繼承IBackgroundJob
。async
//WallpaperJob.cs using Meowv.Blog.Application.Contracts.Wallpaper; using Meowv.Blog.Domain.Shared.Enum; using System.Collections.Generic; using System.Threading.Tasks; namespace Meowv.Blog.BackgroundJobs.Jobs.Wallpaper { public class WallpaperJob : IBackgroundJob { public async Task ExecuteAsync() { var wallpaperUrls = new List<WallpaperJobItem<string>> { new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_1_1.html", Type = WallpaperEnum.Beauty }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_58_1.html", Type = WallpaperEnum.Sportsman }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_66_1.html", Type = WallpaperEnum.CuteBaby }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_4_1.html", Type = WallpaperEnum.Emotion }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_3_1.html", Type = WallpaperEnum.Landscape }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_9_1.html", Type = WallpaperEnum.Animal }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_13_1.html", Type = WallpaperEnum.Plant }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_64_1.html", Type = WallpaperEnum.Food }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_11_1.html", Type = WallpaperEnum.Movie }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_5_1.html", Type = WallpaperEnum.Anime }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_34_1.html", Type = WallpaperEnum.HandPainted }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_65_1.html", Type = WallpaperEnum.Text }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_2_1.html", Type = WallpaperEnum.Creative }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_10_1.html", Type = WallpaperEnum.Car }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_14_1.html", Type = WallpaperEnum.PhysicalEducation }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_63_1.html", Type = WallpaperEnum.Military }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_17_1.html", Type = WallpaperEnum.Festival }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_15_1.html", Type = WallpaperEnum.Game }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_12_1.html", Type = WallpaperEnum.Apple }, new WallpaperJobItem<string> { Result = "https://www.i4.cn/wper_4_19_7_1.html", Type = WallpaperEnum.Other } }; } } }
先構建一個要抓取的列表 wallpaperUrls,這裏準備用 HtmlAgilityPack
,默認只抓取第一頁最新的數據。ide
public async Task RunAsync() { ... var web = new HtmlWeb(); var list_task = new List<Task<WallpaperJobItem<HtmlDocument>>>(); wallpaperUrls.ForEach(item => { var task = Task.Run(async () => { var htmlDocument = await web.LoadFromWebAsync(item.Result); return new WallpaperJobItem<HtmlDocument> { Result = htmlDocument, Type = item.Type }; }); list_task.Add(task); }); Task.WaitAll(list_task.ToArray()); }
上面這段代碼,先new了一個HtmlWeb
對象,咱們主要用這個對象去加載咱們的URL。
web.LoadFromWebAsync(...)
,它會返回一個HtmlDocument
對象,這樣就和上面的list_task對應起來,從而也應證了前面添加的WallpaperJobItem
是通用的一個待抓項的類。
循環處理 wallpaperUrls,等待全部請求完成。這樣就拿到了20個HtmlDocument
,和它的分類,接下來就能夠去處理list_task就好了。
在開始處理以前,要想好抓到的圖片數據存放在哪裏?我這裏仍是選擇存在數據庫中,由於有了以前的自定義倉儲之增刪改查的經驗,能夠很快的處理這件事情。
添加實體類、自定義倉儲、DbSet、Code-First等一些列操做,就不一一介紹了,我相信看過以前文章的人都能完成這一步。
Wallpaper實體類包含主鍵Guid,標題Title,圖片地址Url,類型Type,和一個建立時間CreateTime。
自定義倉儲包含一個批量插入的方法:BulkInsertAsync(...)
。
貼一下完成後的圖片,就不上代碼了,若是須要能夠去GitHub獲取。
回到WallpaperJob
,由於咱們要抓取的是圖片,因此獲取到HTML中的img標籤就能夠了。
查看源代碼發現圖片是一個列表呈現的,而且被包裹在//article[@id='wper']/div[@class='jbox']/div[@class='kbox']
下面,學過XPath語法的就很容易了,關於XPath語法這裏也不作介紹了,對於不會的這裏有一篇快速入門的文章:http://www.javashuo.com/article/p-ghzirlpi-gk.html 。
利用XPath Helper工具咱們在瀏覽器上模擬一下選擇的節點是否正確。
使用//article[@id='wper']/div[@class='jbox']/div[@class='kbox']/div/a/img
能夠成功將圖片高亮,說明咱們的語法是正確的。
public async Task RunAsync() { ... var wallpapers = new List<Wallpaper>(); foreach (var list in list_task) { var item = await list; var imgs = item.Result.DocumentNode.SelectNodes("//article[@id='wper']/div[@class='jbox']/div[@class='kbox']/div/a/img[1]").ToList(); imgs.ForEach(x => { wallpapers.Add(new Wallpaper { Url = x.GetAttributeValue("data-big", ""), Title = x.GetAttributeValue("title", ""), Type = (int)item.Type, CreateTime = x.Attributes["data-big"].Value.Split("/").Last().Split("_").First().TryToDateTime() }); }); } ... }
在 foreach 循環中先拿到當前循環的Item對象,即WallpaperJobItem<HtmlDocument>
。
經過.DocumentNode.SelectNodes()
語法獲取到圖片列表,由於在a標籤下面有兩個img標籤,取第一個便可。
GetAttributeValue()
是HtmlAgilityPack
的擴展方法,用於直接獲取屬性值。
在看圖片的時候,發現圖片地址的規則是根據時間戳生成的,因而用TryToDateTime()
擴展方法將其處理轉換成時間格式。
這樣咱們就將全部圖片按分類存進了列表當中,接下來調用批量插入方法。
在構造函數中注入自定義倉儲IWallpaperRepository
。
... private readonly IWallpaperRepository _wallpaperRepository; public WallpaperJob(IWallpaperRepository wallpaperRepository) { _wallpaperRepository = wallpaperRepository; } ...
... var urls = (await _wallpaperRepository.GetListAsync()).Select(x => x.Url); wallpapers = wallpapers.Where(x => !urls.Contains(x.Url)).ToList(); if (wallpapers.Any()) { await _wallpaperRepository.BulkInsertAsync(wallpapers); }
由於抓取的圖片可能存在重複的狀況,咱們須要作一個去重處理,先查詢到數據庫中的全部的URL列表,而後在判斷抓取到的url是否存在,最後調用BulkInsertAsync(...)
批量插入方法。
這樣就完成了數據抓取的所有邏輯,在保存數據到數據庫以後咱們能夠進一步操做,好比:寫日誌、發送郵件通知等等,這裏你們自由發揮吧。
寫一個擴展方法每隔3小時執行一次。
... public static void UseWallpaperJob(this IServiceProvider service) { var job = service.GetService<WallpaperJob>(); RecurringJob.AddOrUpdate("壁紙數據抓取", () => job.ExecuteAsync(), CronType.Hour(1, 3)); } ...
最後在模塊內中調用。
... public override void OnApplicationInitialization(ApplicationInitializationContext context) { ... service.UseWallpaperJob(); }
編譯運行,打開Hangfire界面手動執行看看效果。
完美,數據庫已經存入了很多數據了,仍是要提醒一下:爬蟲有風險,抓數需謹慎。
Hangfire定時處理爬蟲任務,用HtmlAgilityPack
抓取數據後存入數據庫,你學會了嗎?😁😁😁