用Node + EJS寫一個爬蟲指令碼每天定時女朋友發一封暖心郵件

Vince_發表於2019-02-27

本文首發於個人部落格: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: 'like@vince.studio', // 接收者的郵箱地址
    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(){
 &emsp;&emsp;&emsp;&emsp;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/NodeMail
  2. 開啟main.js,修改配置項
//紀念日
let startDay = "2016/6/24";

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

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

//接收者郵箱地
let EmailTo = "like@vince.studio";
//郵件主題
let EmailSubject = "一封暖暖的小郵件";

//每日傳送時間
let EmailHour = 6;
let EmialMinminute= 30;
複製程式碼
  1. 終端輸入npm install安裝依賴,再輸入node main.js,執行指令碼,當然你的電腦不可能不休眠,建議你部署到你的雲伺服器上執行。

最後

冬天到了,是不是也該用程式設計師的專業知識給身邊的人帶來一些溫暖呢,原始碼與demo已經放到github上,要不試一試?

GitHub:github.com/Vincedream/…

相關文章