用Node + EJS寫一個爬蟲指令碼每天定時女朋友發一封暖心郵件
自從用郵箱註冊了很多賬號後,便會收到諸如以下類似的郵件,剛開始還以為是一張圖片,後來仔細一看不是圖片呀,好像還是HTML呀,於是好奇寶寶我Google一下,查閱多篇資料後總結出怎麼用前端知識和Node做一個這樣的“郵件網頁”。

確認主題
知道怎麼實現功能後,思考著我該寫什麼主題呢,用一個HTML模板隨便給小夥伴們發個郵件炫個技?不行,作為一個很cool的程式設計師怎麼能這麼low呢,最近天氣變化幅度大,溫度捉摸不定,女朋友總是抱怨穿少了又冷穿多了又熱,嗨呀,要不我就寫個每天定時給寶寶傳送天氣預報的郵件,另外想起寶寶喜歡看ONE·一個這個APP上的每日更新,要不發天氣預報的同時,再附贈一個“ONE的每日訂閱”?機智又浪漫,開始搬磚~
劇透
本來是想最後放效果圖的,怕你們看到一半就沒興趣了,就在前面劇透一下我最後做出來的效果圖吧~

待解決的問題
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
是一個有著各種配置的定時任務發生器,可以定時每個月、每個禮拜、每天具體什麼時候執行什麼任務,這正符合每天早晨定時給寶寶傳送郵件的需求。
一切準備就緒,開始做一次浪漫的程式設計師
編寫程式碼
網頁爬蟲
這裡我們使用到 superagent
和 cheerio
組合來實現爬蟲:
- 分析網頁DOM結構,如下圖所示:

- 用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(); }); 複製程式碼
思路與步驟
當所有的問題都解決後,便是開始結合程式碼成一段完整的程式,思路很簡單,我們來逐步分析:
- 由於獲取資料是非同步的,並且不能判斷出哪個先獲取到資料,這個是可以將獲取資料的函式封裝成一個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 } 複製程式碼
- 將爬取資料統一處理,作為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); }) } 複製程式碼
- 傳送郵件具體程式碼
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); }); } 複製程式碼
安裝與使用
如果你覺得這封郵件的內容適合你傳送的物件,可以按照以下步驟,改少量引數即可執行程式;
- git clone github.com/Vincedream/…
- 開啟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; 複製程式碼
- 終端輸入
npm install
安裝依賴,再輸入node main.js
,執行指令碼,當然你的電腦不可能不休眠,建議你部署到你的雲伺服器上執行。