前言
大家好,我是曉晨。許久沒有更新部落格了,今天給大家帶來一篇幹貨型文章,一個每隔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("\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
目前學習.NET Core 最好的教程 .NET Core 官方教程 ASP.NET Core 官方教程
.NET Core 交流群:923036995 歡迎加群交流
如果您認為這篇文章還不錯或者有所收獲,您可以點選右下角的【推薦】支援,或請我喝杯咖啡【贊賞】,這将是我繼續寫作,分享的最大動力!
作者:曉晨Master(李志強)
聲明:原創部落格請在轉載時保留原文連結或者在文章開頭加上本人部落格位址,如發現錯誤,歡迎批評指正。凡是轉載于本人的文章,不能設定打賞功能,如有特殊需求請與本人聯系!