基于 abp vNext 和 .NET Core 开发博客项目 - 定时任务最佳实战(一)

上一篇(http://www.javashuo.com/article/p-mvnspptf-gm.html)文章使用AutoMapper来处理对象与对象之间的映射关系,本篇主要围绕定时任务和数据抓取相关的知识点并结合实际应用,在定时任务中循环处理爬虫任务抓取数据。html

开始以前能够删掉以前测试用的几个HelloWorld,没有什么实际意义,直接干掉吧。抓取数据我主要用到了,HtmlAgilityPackPuppeteerSharp,通常状况下HtmlAgilityPack就能够完成大部分的数据抓取需求了,当在抓取动态网页的时候能够用到PuppeteerSharp,同时PuppeteerSharp还支持将图片保存为图片和PDF等牛逼的功能。git

关于这两个库就很少介绍了,不了解的请自行去学习。github

先在.BackgroundJobs层安装两大神器:Install-Package HtmlAgilityPackInstall-Package PuppeteerSharp。我在使用Package Manager安装包的时候通常都不喜欢指定版本号,由于这样默认是给我安装最新的版本。web

以前无心中发现爱思助手的网页版有不少手机壁纸(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层中。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吧。老样子,继承IBackgroundJobasync

//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获取。

2

回到WallpaperJob,由于咱们要抓取的是图片,因此获取到HTML中的img标签就能够了。

3

查看源代码发现图片是一个列表呈现的,而且被包裹在//article[@id='wper']/div[@class='jbox']/div[@class='kbox']下面,学过XPath语法的就很容易了,关于XPath语法这里也不作介绍了,对于不会的这里有一篇快速入门的文章:http://www.javashuo.com/article/p-ghzirlpi-gk.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

相关文章
相关标签/搜索