基於 abp vNext 和 .NET Core 開發博客項目 - 定時任務最佳實戰(一)

上一篇(https://www.cnblogs.com/meowv/p/12966092.html)文章使用AutoMapper來處理對象與對象之間的映射關係,本篇主要圍繞定時任務和數據抓取相關的知識點並結合實際應用,在定時任務中循環處理爬蟲任務抓取數據。

開始之前可以刪掉之前測試用的幾個HelloWorld,沒有什麼實際意義,直接幹掉吧。抓取數據我主要用到了,HtmlAgilityPackPuppeteerSharp,一般情況下HtmlAgilityPack就可以完成大部分的數據抓取需求了,當在抓取動態網頁的時候可以用到PuppeteerSharp,同時PuppeteerSharp還支持將圖片保存爲圖片和PDF等牛逼的功能。

關於這兩個庫就不多介紹了,不瞭解的請自行去學習。

先在.BackgroundJobs層安裝兩大神器:Install-Package HtmlAgilityPackInstall-Package PuppeteerSharp。我在使用Package Manager安裝包的時候一般都不喜歡指定版本號,因爲這樣默認是給我安裝最新的版本。

之前無意中發現愛思助手的網頁版有很多手機壁紙(https://www.i4.cn/wper_4_0_1_1.html),於是我就動了小心思,把所有手機壁紙全部抓取過來自嗨,可以看看我個人博客中的成品吧:https://meowv.com/wallpaper 😝😝😝

1

最開始我是用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層中。

//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

//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,默認只抓取第一頁最新的數據。

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獲取。

2

回到WallpaperJob,因爲我們要抓取的是圖片,所以獲取到HTML中的img標籤就可以了。

3

查看源代碼發現圖片是一個列表呈現的,並且被包裹在//article[@id='wper']/div[@class='jbox']/div[@class='kbox']下面,學過XPath語法的就很容易了,關於XPath語法這裏也不做介紹了,對於不會的這裏有一篇快速入門的文章:https://www.cnblogs.com/meowv/p/11310538.html

利用XPath Helper工具我們在瀏覽器上模擬一下選擇的節點是否正確。

4

使用//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界面手動執行看看效果。

5

完美,數據庫已經存入了不少數據了,還是要提醒一下:爬蟲有風險,抓數需謹慎。

Hangfire定時處理爬蟲任務,用HtmlAgilityPack抓取數據後存入數據庫,你學會了嗎?😁😁😁

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章