用Node EJS寫一個爬蟲腳本每天定時給心愛的她發一封暖心郵件

本文首發於個人博客:Vince'Blog

項目源碼:NodeMail,歡迎star,說不定哪天脫單了就能用到了

寫在前面

自從用郵箱註冊了很多賬號後,便會收到諸如以下類似的郵件,剛開始還以爲是一張圖片,後來仔細一看不是圖片呀,好像還是HTML呀,於是好奇寶寶我Google一下,查閱多篇資料後總結出怎麼用前端知識和Node做一個這樣的“郵件網頁”。

image

確認主題

知道怎麼實現功能後,思考着我該寫什麼主題呢,用一個HTML模板隨便給小夥伴們發個郵件炫個技?不行,作爲一個很cool的程序員怎麼能這麼low呢,最近天氣變化幅度大,溫度捉摸不定,女朋友總是抱怨穿少了又冷穿多了又熱,嗨呀,要不我就寫個每天定時給寶寶發送天氣預報的郵件,另外想起寶寶喜歡看ONE·一個這個APP上的每日更新,要不發天氣預報的同時,再附贈一個“ONE的每日訂閱”?機智又浪漫,開始搬磚~

劇透

本來是想最後放效果圖的,怕你們看到一半就沒興趣了,就在前面劇透一下我最後做出來的效果圖吧~

image

待解決的問題

1. 如何獲取天氣預報和ONE上的data?

答:獲取data有兩種方法,第一種方法是獲取天氣預報和ONE的API,第二種是用node爬蟲獲取天氣預報和ONE網頁的信息。後來找了下,發現ONE並沒有API接口,爲了讓兩者統一,於是決定使用node上的一個插件叫cheerio,配合superagent能夠很方便地爬取網頁上的信息。

2. 如何做出HTML的這種郵件?

答:之前學過一段時間的express這個框架,接觸到模版引擎這個概念,傳入data便可獲得html文件,再結合node的fs模塊,獲取到這個html文件,便可以結合node的郵件插件發送HTML郵件啦!

3. 如何用node發送郵件?

感謝無私的開源開發者,開發了一款發送郵件的Node插件nodemailer,兼容主流的Email廠商,只需要配置好郵箱賬號和smtp授權碼,便可以用你的郵箱賬號在node腳本上發文件,很cool有沒有~

4. 如何做到每日定時發送?

其實可以通過各種hack的方式寫這麼一個定時任務,但是既然node社區有這個定時的輪子,那我們直接用就好了,node-schedule是一個有着各種配置的定時任務發生器,可以定時每個月、每個禮拜、每天具體什麼時候執行什麼任務,這正符合每天早晨定時給寶寶發送郵件的需求。

一切準備就緒,開始做一次浪漫的程序員

編寫代碼

網頁爬蟲

這裏我們使用到superagentcheerio組合來實現爬蟲:

  • 分析網頁DOM結構,如下圖所示:

image

  • 用superagent來獲取指定網頁的所有DOM:
superagent.get(URL).end(function(err,res){
    //
}
  • 用cheerio來篩選superagent獲取到的DOM,取出需要的DOM
imgUrl:$(todayOne).find('.fp-one-imagen').attr('src'),
type:$(todayOne).find('.fp-one-imagen-footer').text().replace(/(^\s*)|(\s*$)/g, ""),
text:$(todayOne).find('.fp-one-cita').text().replace(/(^\s*)|(\s*$)/g, "")

以下就是爬取ONE的代碼,天氣預報網頁也是一個道理:

const superagent = require('superagent'); //發送網絡請求獲取DOM
const cheerio = require('cheerio'); //能夠像Jquery一樣方便獲取DOM節點

const OneUrl = "http://wufazhuce.com/"; //ONE的web版網站

superagent.get(OneUrl).end(function(err,res){
    if(err){
       console.log(err);
    }
    let $ = cheerio.load(res.text);
    let selectItem=$('#carousel-one .carousel-inner .item');
    let todayOne=selectItem[0]; //獲取輪播圖第一個頁面,也就是當天更新的內容
    let todayOneData={  //保存到一個json中
        imgUrl:$(todayOne).find('.fp-one-imagen').attr('src'),
        type:$(todayOne).find('.fp-one-imagen-footer').text().replace(/(^\s*)|(\s*$)/g, ""),
        text:$(todayOne).find('.fp-one-cita').text().replace(/(^\s*)|(\s*$)/g, "")
    };
    console.log(todayOneData);
})

EJS模版引擎生成HTML

通過爬蟲獲取到了數據,那麼我們就能夠通過將date輸入到EJS渲染出HTML,我們在目錄下創建js腳本和ejs模版文件:

  • app.js
const ejs = require('ejs'); //ejs模版引擎
const fs  = require('fs'); //文件讀寫
const path = require('path'); //路徑配置

//傳給EJS的數據
let data={
    title:'nice to meet you~'
}

//將目錄下的mail.ejs獲取到,得到一個模版
const template = ejs.compile(fs.readFileSync(path.resolve(__dirname, 'mail.ejs'), 'utf8'));
//將數據傳入模版中,生成HTML
const html = template(data);

console.log(html)
  • mail.ejs
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>
        <%= title %>
    </h1>
</body>
</html>

用Node發送郵件

這裏我們可以發送純text也可以發送html,注意的是郵箱密碼不是你登錄郵箱的密碼,而是smtp授權碼,什麼是smtp授權碼呢?就是你的郵箱賬號可以使用這個smtp授權碼在別的地方發郵件,一般smtp授權碼在郵箱官網的設置中可以看的到,設置如下注釋。

const nodemailer = require('nodemailer'); //發送郵件的node插件

let transporter = nodemailer.createTransport({
    service: '126', // 發送者的郵箱廠商,支持列表:https://nodemailer.com/smtp/well-known/
    port: 465, // SMTP 端口
    secureConnection: true, // SSL安全鏈接
    auth: {   //發送者的賬戶密碼
      user: '賬戶@126.com', //賬戶
      pass: 'smtp授權碼', //smtp授權碼,到郵箱設置下獲取
    }
  });

let mailOptions = {
    from: '"發送者暱稱" <地址@126.com>', // 發送者暱稱和地址
    to: '[email protected]', // 接收者的郵箱地址
    subject: '一封暖暖的小郵件', // 郵件主題
    text: 'test mail',  //郵件的text
    // html: html  //也可以用html發送  
  };
  
//發送郵件
transporter.sendMail(mailOptions, (error, info) => {  
    if (error) {
    return console.log(error);
    }
    console.log('郵件發送成功 ID:', info.messageId);
});  

Node定時執行任務

這裏我們用到了node-schedule來定時執行任務,示例如下:

var schedule = require("node-schedule");  

//1. 確定的時間執行
var date = new Date(2017,12,10,15,50,0);  
schedule.scheduleJob(date, function(){  
   console.log("執行任務");
});

//2. 秒爲單位執行 
//比如:每5秒執行一次
var rule1     = new schedule.RecurrenceRule();  
var times1    = [1,6,11,16,21,26,31,36,41,46,51,56];  
rule1.second  = times1;  
schedule.scheduleJob(rule1, function(){
    console.log("執行任務");    
});

//3.以分爲單位執行
//比如:每5分種執行一次
var rule2     = new schedule.RecurrenceRule();  
var times2    = [1,6,11,16,21,26,31,36,41,46,51,56];  
rule2.minute  = times2;  
schedule.scheduleJob(rule2, function(){  
    console.log("執行任務");    
});  

//4.以天單位執行
//比如:每天6點30分執行
var rule = new schedule.RecurrenceRule();
rule.dayOfWeek = [0, new schedule.Range(1, 6)];
rule.hour = 6;
rule.minute =30;
var j = schedule.scheduleJob(rule, function(){
     console.log("執行任務");
        getData();
});

思路與步驟

當所有的問題都解決後,便是開始結合代碼成一段完整的程序,思路很簡單,我們來逐步分析:

  1. 由於獲取數據是異步的,並且不能判斷出哪個先獲取到數據,這個是可以將獲取數據的函數封裝成一個Promise對象,最後在一起用Promise.all來判斷所有數據獲取完畢,再發送郵件
// 其中一個數據獲取函數,其他的也是類似
function getOneData(){
    let p = new Promise(function(resolve,reject){
        superagent.get(OneUrl).end(function(err, res) {
            if (err) {
                reject(err);
            }
            let $ = cheerio.load(res.text);
            let selectItem = $("#carousel-one .carousel-inner .item");
            let todayOne = selectItem[0];
            let todayOneData = {
              imgUrl: $(todayOne)
                .find(".fp-one-imagen")
                .attr("src"),
              type: $(todayOne)
                .find(".fp-one-imagen-footer")
                .text()
                .replace(/(^\s*)|(\s*$)/g, ""),
              text: $(todayOne)
                .find(".fp-one-cita")
                .text()
                .replace(/(^\s*)|(\s*$)/g, "")
            };
            resolve(todayOneData)
          });
    })
    return p
}
  1. 將爬取數據統一處理,作爲EJS的參數,發送郵件模板。

function getAllDataAndSendMail(){
    let HtmlData = {};
    // how long with
    let today = new Date();
    let initDay = new Date(startDay);
    let lastDay = Math.floor((today - initDay) / 1000 / 60 / 60 / 24);
    let todaystr =
      today.getFullYear() +
      " / " +
      (today.getMonth() + 1) +
      " / " +
      today.getDate();
    HtmlData["lastDay"] = lastDay;
    HtmlData["todaystr"] = todaystr;

    Promise.all([getOneData(),getWeatherTips(),getWeatherData()]).then(
        function(data){
            HtmlData["todayOneData"] = data[0];
            HtmlData["weatherTip"] = data[1];
            HtmlData["threeDaysData"] = data[2];
            sendMail(HtmlData)
        }
    ).catch(function(err){
        getAllDataAndSendMail() //再次獲取
        console.log('獲取數據失敗: ',err);
    })
}
  1. 發送郵件具體代碼

function sendMail(HtmlData) {
    const template = ejs.compile(
      fs.readFileSync(path.resolve(__dirname, "email.ejs"), "utf8")
    );
    const html = template(HtmlData);
  
    let transporter = nodemailer.createTransport({
      service: EmianService,
      port: 465,
      secureConnection: true,
      auth: EamilAuth
    });
  
    let mailOptions = {
      from: EmailFrom,
      to: EmailTo,
      subject: EmailSubject,
      html: html
    };
    transporter.sendMail(mailOptions, (error, info={}) => {
      if (error) {
        console.log(error);
        sendMail(HtmlData); //再次發送
      }
      console.log("Message sent: %s", info.messageId);
    });
  }

安裝與使用

如果你覺得這封郵件的內容適合你發送的對象,可以按照以下步驟,改少量參數即可運行程序;

  1. git clone https://github.com/Vincedream...
  2. 打開main.js,修改配置項
//紀念日
let startDay = "2016/6/24";

//當地拼音,需要在下面的墨跡天氣url確認
const local = "xiangtan";

//發送者郵箱廠家
let EmianService = "163";
//發送者郵箱賬戶SMTP授權碼
let EamilAuth = {
  user: "[email protected]",
  pass: "xxxxxx"
};
//發送者暱稱與郵箱地址
let EmailFrom = '"name" <[email protected]>';

//接收者郵箱地
let EmailTo = "[email protected]";
//郵件主題
let EmailSubject = "一封暖暖的小郵件";

//每日發送時間
let EmailHour = 6;
let EmialMinminute= 30;
  1. 終端輸入npm install安裝依賴,再輸入node main.js,運行腳本,當然你的電腦不可能不休眠,建議你部署到你的雲服務器上運行。

最後

冬天到了,是不是也該用程序員的專業知識給身邊的人帶來一些溫暖呢,源代碼與demo已經放到github上,要不試一試?

GitHub:https://github.com/Vincedream/NodeMail

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章