基于 abp vNext 和 .NET Core 开发博客项目 - 定时任务最佳实战(一)
上一篇(https://www.cnblogs.com/meowv/p/12966092.html)文章使用AutoMapper来处理对象与对象之间的映射关系,本篇主要围绕定时任务和数据抓取相关的知识点并结合实际应用,在定时任务中循环处理爬虫任务抓取数据。
开始之前可以删掉之前测试用的几个HelloWorld,没有什么实际意义,直接干掉吧。抓取数据我主要用到了,HtmlAgilityPack
和PuppeteerSharp
,一般情况下HtmlAgilityPack
就可以完成大部分的数据抓取需求了,当在抓取动态网页的时候可以用到PuppeteerSharp
,同时PuppeteerSharp
还支持将图片保存为图片和PDF等牛逼的功能。
关于这两个库就不多介绍了,不了解的请自行去学习。
先在.BackgroundJobs
层安装两大神器:Install-Package HtmlAgilityPack
、Install-Package PuppeteerSharp
。我在使用Package Manager安装包的时候一般都不喜欢指定版本号,因为这样默认是给我安装最新的版本。
之前无意中发现爱思助手的网页版有很多手机壁纸(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
层中。
//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获取。
回到WallpaperJob
,因为我们要抓取的是图片,所以获取到HTML中的img标签就可以了。
查看源代码发现图片是一个列表呈现的,并且被包裹在//article[@id='wper']/div[@class='jbox']/div[@class='kbox']
下面,学过XPath语法的就很容易了,关于XPath语法这里也不做介绍了,对于不会的这里有一篇快速入门的文章:https://www.cnblogs.com/meowv/p/11310538.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
抓取数据后存入数据库,你学会了吗?
开源地址:https://github.com/Meowv/Blog/tree/blog_tutorial
基于 abp vNext 和 .NET Core 开发博客项目 - 定时任务最佳实战(一)的更多相关文章
- 基于 abp vNext 和 .NET Core 开发博客项目 - 定时任务最佳实战(三)
上一篇(https://www.cnblogs.com/meowv/p/12974439.html)完成了全网各大平台的热点新闻数据的抓取,本篇继续围绕抓取完成后的操作做一个提醒.当每次抓取完数据后, ...
- 基于 abp vNext 和 .NET Core 开发博客项目 - 定时任务最佳实战(二)
上一篇(https://www.cnblogs.com/meowv/p/12971041.html)使用HtmlAgilityPack抓取壁纸数据成功将图片存入数据库,本篇继续来完成一个全网各大平台的 ...
- 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(一)
系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...
- 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(二)
系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...
- 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(三)
系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...
- 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(四)
系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...
- 基于 abp vNext 和 .NET Core 开发博客项目 - 博客接口实战篇(五)
系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...
- 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(一)
系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...
- 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(二)
系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...
随机推荐
- Codeforces Round #460 (Div. 2)-A Supermaket(贪心)
A. Supermarket time limit per test 2 seconds memory limit per test 256 megabytes input standard inpu ...
- Java—线程池ThreadPoolExecutor详解
引导 要求:线程资源必须通过线程池提供,不允许在应用自行显式创建线程: 说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题.如果不使用线程池,有可能造成系统 ...
- 【K8S】K8S 1.18.2安装dashboard(基于kubernetes-dashboard 2.0.0版本)
[K8S]K8S 1.18.2安装dashboard(基于kubernetes-dashboard 2.0.0版本) 写在前面 K8S集群部署成功了,如何对集群进行可视化管理呢?别着急,接下来,我们一 ...
- Android Library 发布开源库 JCenter & JitPack 攻略
对于Android 的开源库,一般通过 JCenter 或者 JitPack 发布开源.两种方式均可~ 当你造了一个好玩有用的东西想要分享给大家时,开源出来便是一种好方式~ 一. 上传开源库到 JCe ...
- 微软2016校园招聘在线笔试之Magic Box
题目1 : Magic Box 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 The circus clown Sunny has a magic box. When ...
- 向Redis里存入数据
实现思路:1. 从Redis缓存获取URL统计网址清单2. 逐条拼凑SQL统计语句,暂时不能支持批量计算,因为按单个网址统计.3. 发送到HIVE JDBC执行SQL并等待返回结果4 ...
- java使用window builder图形界面开发简易计算器
界面效果: /** * */ package calculator; import java.awt.BorderLayout; import java.awt.EventQueue; import ...
- HttpClient之Post接口代码范例
核心包: import com.alibaba.fastjson.JSONArray;import com.alibaba.fastjson.JSONObject; 一:接收数据 json数据格式如下 ...
- Mysql常用sql语句(16)- inner join 内连接
测试必备的Mysql常用sql语句系列 https://www.cnblogs.com/poloyy/category/1683347.html 前言 利用条件表达式来消除交叉连接(cross joi ...
- SpringData:关联查询
一.查询方式 1.导航式查询 使用“对象.属性” 进行查询:对于多的查询, 默认就是延迟加载,添加注解@Transactional 在OneToMany 注解中需要添加属性 fetch:值:F ...