.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.cnblogs.com/qulianqing/p/7413640.html
http://www.cnblogs.com/rocketRobin/p/8337055.html

获取&解析博客园首页数据

我是用的是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 2ServicePointManager.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;    //获取urlvar 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日的数据里面,不要被误导了。

还提供一个附件便于收集整理:

好了介绍完毕,我自己已经将这个小工具部署到服务器,想要享受这个服务的可以在评论留下邮箱(手动滑稽)。

github:https://github.com/stulzq/CnBlogSubscribeTool 如果你喜欢,欢迎来个star

原文地址:http://www.cnblogs.com/stulzq/p/8448183.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/322109.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux shell echo打印不出换行

一、现象 echo打印不出换行 指令 ps aux | grep python ps aux | grep python | xargs echo 运行结果&#xff1a; 二、使用参数-e echo一样打印不出换行 ps aux | grep python | xargs echo -e 运行结果&#xff1a; 三、使用参数-e和双引号包裹占位符 echo终于可以…

基于Citus和ASP.NET Core开发多租户应用

Citus是基于PsotgreSQL的扩展&#xff0c;用于切分PsotgreSQL的数据&#xff0c;非常简单地实现数据“切片&#xff08;sharp&#xff09;”。如果不使用Citus&#xff0c;则需要开发者自己实现分布式数据访问层&#xff08;DDAL&#xff09;&#xff0c;实现路由和结果汇总等逻…

ASP.NET CORE 微服务(简化版)实战系列-没有比这性价比再高的实战课程了

ASP.NET CORE 微服务(简化版&#xff09;实战系列&#xff0c;最后1天298&#xff0c;现在注册购买再减50。作者jesse 腾飞在2.14 早上我买了他的课程后&#xff0c;他才做了下面这个活动&#xff1a;作者jesse 腾飞花了大量的时间做了一个非常好的视频教程&#xff0c;我个人也…

祝大家狗年家庭事业旺旺旺

冒泡排序&#xff0c;选择排序&#xff0c;插入排序&#xff0c;快速排序&#xff0c;堆排序&#xff0c;归并排序&#xff0c;希尔排序&#xff0c;桶排序&#xff0c;基数排序新年帮您排忧解难。有向图&#xff0c;无向图&#xff0c;有环图&#xff0c;无环图&#xff0c;完…

携程Apollo(阿波罗)配置中心在.NET Core项目快速集成

.NET Core的支持文档大体上可以参考文档.Net客户端使用指南&#xff1a;https://github.com/ctripcorp/apollo/wiki/.Net%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97登录Apollo上新建App和相关的配置项&#xff0c;可以参考如下配置&#xff1a;在Nuget上…

欢乐纪中A组赛【2019.8.9】

前言 在短暂的比赛时间中&#xff0c;我发现本菜鸡越是功于心计想ACACAC&#xff0c;越是拿不到分&#xff0c;所以。。。 我不写比赛了JOJO!JOJO!JOJO! 成绩 JJJ表示初中&#xff0c;HHH表示高中后面加的是几年级 RankRankRankPersonPersonPersonScoreScoreScoreAAABBBCCC17…

SpringCloud Zuul(四)之工作原理

一、筛选器概述 Zuul的中心是一系列过滤器&#xff0c;这些过滤器能够在HTTP请求和响应的路由期间执行一系列操作。 以下是Zuul过滤器的主要特征&#xff1a; 类型&#xff1a;通常定义路由流程中应用过滤器的阶段&#xff08;尽管它可以是任何自定义字符串&#xff09;执行…

使用Nito.AsyncEx实现异步锁

Lock是常用的同步锁&#xff0c;但是我们无法在Lock的内部实现异步调用&#xff0c;比如我们无法使用await.以下面的代码为例&#xff0c;当你在lock内部使用await时&#xff0c;VS会报错提醒。最简单的解决办法就是使用第三方的库Nito.AsyncEx。可以通过Nuget安装。通过AsyncL…

汽车之家店铺数据抓取 DotnetSpider实战[一]

一、背景春节也不能闲着&#xff0c;一直想学一下爬虫怎么玩&#xff0c;网上搜了一大堆&#xff0c;大多都是Python的&#xff0c;大家也比较活跃&#xff0c;文章也比较多&#xff0c;找了一圈&#xff0c;发现园子里面有个大神开发了一个DotNetSpider的开源库&#xff0c;很…

Comet OJ(Contest #8)-D菜菜种菜【树状数组,指针】

前言 话说昨晚写题的时候贼NMNMNM惊险&#xff0c;最后22秒把程序交了上去竟然过了 正题 题目链接:https://cometoj.com/contest/58/problem/D?problem_id2758 题目大意 nnn个点mmm条单向边&#xff0c;然后每次询问一个区间[L,R][L,R][L,R]求若只选择这个区间的点&#xf…

微软正式开源Blazor ,将.NET带回到浏览器

微软 ASP.NET 团队近日正式开源了 Blazor &#xff0c;这是一个 Web UI 框架&#xff0c;可通过 WebAssembly 在任意浏览器中运行 .Net 。Blazor 旨在简化快速的单页面 .Net 浏览器应用的构建过程&#xff0c;它虽然使用了诸如 CSS 和 HTML 之类的 Web 技术&#xff0c;但它使…

在.NetCore中使用Myrmec检测文件真实格式

Myrmec 是什么&#xff1f;Myrmec 是一个用于检测文件格式的库&#xff0c;Myrmec不同于其它库或者手写检测代码&#xff0c;Myrmec不依赖文件扩展名&#xff08;在实际使用中&#xff0c;你的用户很可能使用虚假的扩展名欺骗你的应用程序&#xff09;&#xff0c;Myrmec会检测…

jzoj3736-[NOI2014模拟7.11]数学题(math)【计算几何】

正题 题目大意 给定两个向量a(x1,y1),b(x2,y2)a(x_1,y_1),b(x_2,y_2)a(x1​,y1​),b(x2​,y2​)&#xff0c;然后求∣λ1aλ2b∣|\lambda _1a\lambda _2b|∣λ1​aλ2​b∣的最小值&#xff0c;要求λ1,λ2\lambda_1,\lambda _2λ1​,λ2​不同时为0。 解题思路 我们先考虑若…

Orleans之EventSourcing

引入:如果没有意外,我再这篇文章中用ES代替EventSourcing,如果碰到"事件回溯","事件溯源","事溯"等词语,都一般代表Eventsourcing.如果引入Orleans而不用es的话,那就只用了Orleans一半的优点,多线程编程的逻辑\排错的简化以及可分布式.下面我聊聊…

Alex: 2018年对混合现实MR的展望

原文作者&#xff1a;Alex Kipman&#xff0c; 微软操作系统工程院技术院士 Hello 大家好&#xff01;难以置信我们已经走过了2018年的头两个月了。每年一月份我都会去巴西省亲&#xff0c;和我的家人欢聚一堂&#xff0c;度过一个美好的假日。在我省亲的同时&#xff0c;我想了…

jzoj3738-[NOI2014模拟7.11]理想城市(city)【树,模型转换】

正题 题目大意 一个理想城市有nnn个块构成&#xff0c;有以下性质 任意两个块之间可以通过其他块到达任意两个块之间可以不通过其他块(通过空位)到达 然后求每个块之间的距离之和。 解题思路 我们将横竖的距离分开计算。 假设现在我们考虑计算竖向的边的距离&#xff0c;我…

SpringCloud Ribbon(二)之自定义负载均衡策略IRule

一、Ribbon负载均衡策略 一个服务对应一个LoadBalancer&#xff0c;一个LoadBalancer只有一个Rule&#xff0c;LoadBalancer记录服务的注册地址&#xff0c;Rule提供从服务的注册地址中找出一个地址的规则。 Ribbon提供七种负载均衡策略&#xff0c;默认的负载均衡策略是轮训策…

欢乐纪中A组赛【2019.8.10】

前言 昨天&#xff1a; MdMdMd今天还真爆零了 顺便%%%ZZYRank1\%\%\%ZZY\ Rank1%%%ZZY Rank1 成绩 JJJ表示初中&#xff0c;HHH表示高中后面加的是几年级 RankRankRankPersonPersonPersonScoreScoreScoreAAABBBCCC111(H−1)ZZY(H-1)ZZY(H−1)ZZY1501501502020203030301001001…

Blazor正式成为Microsoft官方.NET 和WebAssembly项目

Microsoft从Blazor的开发者Steve Sanderson手中接手了这款应用程序&#xff0c;自此&#xff0c;将.NET在浏览器运行的计划又更进了一步。由此&#xff0c;Microsoft又进一步扩充了自己的WebAssembly/.NET栈&#xff0c;更进一步帮助.NET开发人员搭建基于浏览器的应用程序。在一…

SpringCloud Ribbon(一)之自定义负载均衡器ILoadBalancer

一、Ribbon负载均衡 一个服务对应一个LoadBalancer&#xff0c;一个LoadBalancer只有一个Rule&#xff0c;LoadBalancer记录服务的注册地址&#xff0c;提供更新服务的注册地址&#xff0c;Rule提供从服务的注册地址中找出一个地址的规则。 二、 自定义负载均衡 本文自定义负…