1. 程式人生 > 其它 >.NET Core 實現定時抓取部落格園首頁文章資訊併發送到郵箱

.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 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("rn", "").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