.NET Core 实现定时抓取网站文章并发送到邮箱
前言
大家好,我是晓晨。许久没有更新博客了,今天给大家带来一篇干货型文章,一个每隔5分钟抓取博客园首页文章信息并在第二天的上午9点发送到你的邮箱的小工具。比如我在2018年2月14日,9点来到公司我就会收到一封邮件,是2018年2月13日的博客园首页的文章信息。写这个小工具的初衷是,一直有看博客的习惯,但是最近由于各种原因吧,可能几天都不会看一下博客,要是中途错过了什么好文可是十分心疼的哈哈。所以做了个工具,每天归档发到邮箱,妈妈再也不会担心我错过好的文章了。为什么只抓取首页?因为博客园首页文章的质量相对来说高一些。
准备
作为一个持续运行的工具,没有日志记录怎么行,我准备使用的是NLog来记录日志,它有个日志归档功能非常不错。在http请求中,由于网络问题吧可能会出现失败的情况,这里我使用Polly来进行Retry。使用HtmlAgilityPack来解析网页,需要对xpath有一定了解。下面是详细说明:
组件名 | 用途 | github |
---|---|---|
NLog | 记录日志 | https://github.com/NLog/NLog |
Polly | 当http请求失败,进行重试 | https://github.com/App-vNext/Polly |
HtmlAgilityPack | 网页解析 | https://github.com/zzzprojects/html-agility-pack |
MailKit | 发送邮件 | https://github.com/jstedfast/MailKit |
有不了解的组件,可以通过访问github获取资料。
参考文章
https://www.jb51.net/article/112595.htm
获取&解析博客园首页数据
我是用的是HttpWebRequest来进行http请求,下面分享一下我简单封装的类库:
using System; using System.IO; using System.Net; using System.Text; namespace CnBlogSubscribeTool { /// <summary> /// Simple Http Request Class /// .NET Framework >= 4.0 /// Author:stulzq /// CreatedTime:2017-12-12 15:54:47 /// </summary> public class HttpUtil { static HttpUtil() { //Set connection limit ,Default limit is 2 ServicePointManager.DefaultConnectionLimit = 1024; } /// <summary> /// Default Timeout 20s /// </summary> public static int DefaultTimeout = 20000; /// <summary> /// Is Auto Redirect /// </summary> public static bool DefalutAllowAutoRedirect = true; /// <summary> /// Default Encoding /// </summary> public static Encoding DefaultEncoding = Encoding.UTF8; /// <summary> /// Default UserAgent /// </summary> public static string DefaultUserAgent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36" ; /// <summary> /// Default Referer /// </summary> public static string DefaultReferer = ""; /// <summary> /// httpget request /// </summary> /// <param name="url">Internet Address</param> /// <returns>string</returns> public static string GetString(string url) { var stream = GetStream(url); string result; using (StreamReader sr = new StreamReader(stream)) { result = sr.ReadToEnd(); } return result; } /// <summary> /// httppost request /// </summary> /// <param name="url">Internet Address</param> /// <param name="postData">Post request data</param> /// <returns>string</returns> public static string PostString(string url, string postData) { var stream = PostStream(url, postData); string result; using (StreamReader sr = new StreamReader(stream)) { result = sr.ReadToEnd(); } return result; } /// <summary> /// Create Response /// </summary> /// <param name="url"></param> /// <param name="post">Is post Request</param> /// <param name="postData">Post request data</param> /// <returns></returns> public static WebResponse CreateResponse(string url, bool post, string postData = "") { var httpWebRequest = WebRequest.CreateHttp(url); httpWebRequest.Timeout = DefaultTimeout; httpWebRequest.AllowAutoRedirect = DefalutAllowAutoRedirect; httpWebRequest.UserAgent = DefaultUserAgent; httpWebRequest.Referer = DefaultReferer; if (post) { var data = DefaultEncoding.GetBytes(postData); httpWebRequest.Method = "POST"; httpWebRequest.ContentType = "application/x-www-form-urlencoded;charset=utf-8"; httpWebRequest.ContentLength = data.Length; using (var stream = httpWebRequest.GetRequestStream()) { stream.Write(data, 0, data.Length); } } try { var response = httpWebRequest.GetResponse(); return response; } catch (Exception e) { throw new Exception(string.Format("Request error,url:{0},IsPost:{1},Data:{2},Message:{3}", url, post, postData, e.Message), e); } } /// <summary> /// http get request /// </summary> /// <param name="url"></param> /// <returns>Response Stream</returns> public static Stream GetStream(string url) { var stream = CreateResponse(url, false).GetResponseStream(); if (stream == null) { throw new Exception("Response error,the response stream is null"); } else { return stream; } } /// <summary> /// http post request /// </summary> /// <param name="url"></param> /// <param name="postData">post data</param> /// <returns>Response Stream</returns> public static Stream PostStream(string url, string postData) { var stream = CreateResponse(url, true, postData).GetResponseStream(); if (stream == null) { throw new Exception("Response error,the response stream is null"); } else { return stream; } } } }
获取首页数据
string res = HttpUtil.GetString(https://www.cnblogs.com);
解析数据
我们成功获取到了html,但是怎么提取我们需要的信息(文章标题、地址、摘要、作者、发布时间)呢。这里就亮出了我们的利剑HtmlAgilityPack,他是一个可以根据xpath来解析网页的组件。
载入我们前面获取的html:
HtmlDocument doc = new HtmlDocument(); doc.LoadHtml(html);
从上图中,我们可以看出,每条文章所有信息都在一个class为post_item的div里,我们先获取所有的class=post_item的div
//获取所有文章数据项 var itemBodys = doc.DocumentNode.SelectNodes("//div[@class='post_item_body']");
我们继续分析,可以看出文章的标题在class=post_item_body的div下面的h3标签下的a标签,摘要信息在class=post_item_summary的p标签里面,发布时间和作者在class=post_item_foot的div里,分析完毕,我们可以取出我们想要的数据了:
foreach (var itemBody in itemBodys) { //标题元素 var titleElem = itemBody.SelectSingleNode("h3/a"); //获取标题 var title = titleElem?.InnerText; //获取url var url = titleElem?.Attributes["href"]?.Value; //摘要元素 var summaryElem = itemBody.SelectSingleNode("p[@class='post_item_summary']"); //获取摘要 var summary = summaryElem?.InnerText.Replace("\r\n", "").Trim(); //数据项底部元素 var footElem = itemBody.SelectSingleNode("div[@class='post_item_foot']"); //获取作者 var author = footElem?.SelectSingleNode("a")?.InnerText; //获取文章发布时间 var publishTime = Regex.Match(footElem?.InnerText, "\\d+-\\d+-\\d+ \\d+:\\d+").Value; Console.WriteLine($"标题:{title}"); Console.WriteLine($"网址:{url}"); Console.WriteLine($"摘要:{summary}"); Console.WriteLine($"作者:{author}"); Console.WriteLine($"发布时间:{publishTime}"); Console.WriteLine("--------------华丽的分割线---------------"); }
运行一下:
我们成功的获取了我们想要的信息。现在我们定义一个Blog对象将它们装起来。
public class Blog { /// <summary> /// 标题 /// </summary> public string Title { get; set; } /// <summary> /// 博文url /// </summary> public string Url { get; set; } /// <summary> /// 摘要 /// </summary> public string Summary { get; set; } /// <summary> /// 作者 /// </summary> public string Author { get; set; } /// <summary> /// 发布时间 /// </summary> public DateTime PublishTime { get; set; } }
http请求失败重试
我们使用Polly在我们的http请求失败时进行重试,设置为重试3次。
//初始化重试器 _retryTwoTimesPolicy = Policy .Handle<Exception>() .Retry(3, (ex, count) => { _logger.Error("Excuted Failed! Retry {0}", count); _logger.Error("Exeption from {0}", ex.GetType().Name); });
测试一下:
可以看到当遇到exception是Polly会帮我们重试三次,如果三次重试都失败了那么会放弃。
发送邮件
使用MailKit来进行邮件发送,它支持IMAP,POP3和SMTP协议,并且是跨平台的十分优秀。下面是根据前面园友的分享自己封装的一个类库:
using System.Collections.Generic; using CnBlogSubscribeTool.Config; using MailKit.Net.Smtp; using MimeKit; namespace CnBlogSubscribeTool { /// <summary> /// send email /// </summary> public class MailUtil { private static bool SendMail(MimeMessage mailMessage,MailConfig config) { try { var smtpClient = new SmtpClient(); smtpClient.Timeout = 10 * 1000; //设置超时时间 smtpClient.Connect(config.Host, config.Port, MailKit.Security.SecureSocketOptions.None);//连接到远程smtp服务器 smtpClient.Authenticate(config.Address, config.Password); smtpClient.Send(mailMessage);//发送邮件 smtpClient.Disconnect(true); return true; } catch { throw; } } /// <summary> ///发送邮件 /// </summary> /// <param name="config">配置</param> /// <param name="receives">接收人</param> /// <param name="sender">发送人</param> /// <param name="subject">标题</param> /// <param name="body">内容</param> /// <param name="attachments">附件</param> /// <param name="fileName">附件名</param> /// <returns></returns> public static bool SendMail(MailConfig config,List<string> receives, string sender, string subject, string body, byte[] attachments = null,string fileName="") { var fromMailAddress = new MailboxAddress(config.Name, config.Address); var mailMessage = new MimeMessage(); mailMessage.From.Add(fromMailAddress); foreach (var add in receives) { var toMailAddress = new MailboxAddress(add); mailMessage.To.Add(toMailAddress); } if (!string.IsNullOrEmpty(sender)) { var replyTo = new MailboxAddress(config.Name, sender); mailMessage.ReplyTo.Add(replyTo); } var bodyBuilder = new BodyBuilder() { HtmlBody = body }; //附件 if (attachments != null) { if (string.IsNullOrEmpty(fileName)) { fileName = "未命名文件.txt"; } var attachment = bodyBuilder.Attachments.Add(fileName, attachments); //解决中文文件名乱码 var charset = "GB18030"; attachment.ContentType.Parameters.Clear(); attachment.ContentDisposition.Parameters.Clear(); attachment.ContentType.Parameters.Add(charset, "name", fileName); attachment.ContentDisposition.Parameters.Add(charset, "filename", fileName); //解决文件名不能超过41字符 foreach (var param in attachment.ContentDisposition.Parameters) param.EncodingMethod = ParameterEncodingMethod.Rfc2047; foreach (var param in attachment.ContentType.Parameters) param.EncodingMethod = ParameterEncodingMethod.Rfc2047; } mailMessage.Body = bodyBuilder.ToMessageBody(); mailMessage.Subject = subject; return SendMail(mailMessage, config); } } }
测试一下:
说明
关于抓取数据和发送邮件的调度,程序异常退出的数据处理等等,在此我就不详细说明了,有兴趣的看源码(文末有github地址)
抓取数据是增量更新的。不用RSS订阅的原因是RSS更新比较慢。
完整的程序运行截图:
每发送一次邮件,程序就会将记录时间调整到今天的9点,然后每次抓取数据之后就会判断当前时间减去记录时间是否大于等于24小时,如果符合就发送邮件并且更新记录时间。
收到的邮件截图:
截图中的邮件标题为13日但是邮件内容为14日,是因为我为了演示效果,将今天(14日)的数据copy到了13日的数据里面,不要被误导了。
还提供一个附件便于收集整理:
好了介绍完毕,我自己已经将这个小工具部署到服务器,想要享受这个服务的可以在评论留下邮箱(手动滑稽)。
源码分享:https://github.com/stulzq/CnBlogSubscribeTool
相关文章
- 这篇文章主要为大家详细介绍了ASP.NET购物车的实现过程,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-22
- 这篇文章主要介绍了.NET Core下使用Kafka的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-09-22
- 在开发过程中,使用Visual Studio的断点调试功能可以很方便帮我们调试发现程序存在的错误,同样Visual Studio也支持对SQL Server里面的存储过程进行调试,下面就让我们看看具体的调试方法。...2021-09-22
- 这篇文章主要介绍了Win10 IIS 安装及.net 4.5及Win10安装IIS并配置ASP.NET 4.0的方法,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下...2021-09-22
- 这篇文章主要介绍了详解.NET Core 3.0 里新的JSON API,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-09-22
- 这篇文章主要介绍了.net数据库操作框架SqlSugar的简单入门,帮助大家更好的理解和学习使用.net技术,感兴趣的朋友可以了解下...2021-09-22
ASP.NET Core根据环境变量支持多个 appsettings.json配置文件
这篇文章主要介绍了ASP.NET Core根据环境变量支持多个 appsettings.json配置文件,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-09-22- 这篇文章主要介绍了记一次EFCore类型转换错误及解决方案,帮助大家更好的理解和学习使用asp.net core,感兴趣的朋友可以了解下...2021-09-22
- ZXing是一个开放源码的,用Java实现的多种格式的1D/2D条码图像处理库,它包含了联系到其他语言的端口。这篇文章主要给大家介绍了.NET C#利用ZXing生成、识别二维码/条形码的方法,文中给出了详细的示例代码,有需要的朋友们可以参考借鉴。...2020-06-25
详解ASP.NET Core 中基于工厂的中间件激活的实现方法
这篇文章主要介绍了ASP.NET Core 中基于工厂的中间件激活的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-09-22- 这篇文章主要介绍了C#使用Ado.Net更新和添加数据到Excel表格的方法,较为详细的分析了OLEDB的原理与使用技巧,可实现较为方便的操作Excel数据,需要的朋友可以参考下...2020-06-25
asp.net通过消息队列处理高并发请求(以抢小米手机为例)
这篇文章主要介绍了asp.net通过消息队列处理高并发请求(以抢小米手机为例),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-09-22- Underscore 是一个 JavaScript 工具库,它提供了一整套函数式编程的实用功能,但是没有扩展任何 JavaScript 内置对象。这篇文章主要介绍了underscore源码分析相关知识,感兴趣的朋友一起学习吧...2016-01-02
ASP.NET单选按钮控件RadioButton常用属性和方法介绍
RadioButton又称单选按钮,其在工具箱中的图标为 ,单选按钮通常成组出现,用于提供两个或多个互斥选项,即在一组单选钮中只能选择一个...2021-09-22ASP.NET 2.0中的数据操作:使用两个DropDownList过滤的主/从报表
在前面的指南中我们研究了如何显示一个简单的主/从报表, 该报表使用DropDownList和GridView控件, DropDownList填充类别,GridView显示选定类别的产品. 这类报表用于显示具有...2016-05-19详解.NET Core 使用HttpClient SSL请求出错的解决办法
这篇文章主要介绍了.NET Core 使用HttpClient SSL请求出错的解决办法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2021-09-22- 这篇文章主要介绍了Python调用.NET库的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-05-09
ASP.NET中iframe框架点击左边页面链接 右边显示链接页面内容
这篇文章主要介绍了ASP.NET中iframe框架点击左边页面链接,右边显示链接页面内容的实现代码,感兴趣的小伙伴们可以参考一下...2021-09-22- ASP.NET Web API具有与ASP.NET MVC类似的编程方式,ASP.NET Web API不仅仅具有一个完全独立的消息处理管道,而且这个管道比为ASP.NET MVC设计的管道更为复杂,功能也更为强大。下面创建一个简单的Web API项目,需要的朋友可以参考下...2021-09-22
- 这篇文章主要介绍了ASP.NET连接MySql数据库的2个方法及示例,使用的是MySQL官方组件和ODBC.NET,需要的朋友可以参考下...2021-09-22