# node爬虫案例-定时发送邮件

# 1. 说明

  1. 主要实现功能
  • 利用node实现网页爬取数据
  • 利用模板引擎制作html邮件
  • 利用node发送电子邮件
  • 利用node实现定时任务
  1. 项目依赖

所有依赖均在npm官网 (opens new window)中可以找到

  • superagent => 向服务器发起http请求
  • cheerio => 解析html
  • art-template => 模板引擎
  • nodemailer => 发送邮件
  • node-schedule => 定时任务
  1. 需要抓取的页面
  • http://tianqi.moji.com/
  • http://wufazhuce.com/

# 2. 环境安装

  • 创建文件夹及初始化
    • mkdir nodemail
    • npm init -y
  • 安装所有依赖包
    • yarn add superagent cheerio art-template nodemailer node-schedule
    • 查看文件目录 => lsls -al
    • 打开文件 => start package.json

# 3. 处理日期格式

  • main.js
// 获取时间信息
let getDayDate = ()=> {
    // 由于很多数据要提前准备好,才能渲染到页面,最终才能发送
    // 这里就使用promise
    return new Promise((resolve, reject)=> {
        // 现在的时间
        let today = new Date();
        // 设定的时间(2019-5-20)
        let anniversary = new Date('2019-05-20');

        // 计算时间差毫秒值
        let count = today - anniversary;
        // 时间格式转换(转换为天并向上取整)
        count = Math.ceil(count/1000/60/60/24);
        // count = Math.floor(count/1000/60/60/24); // 向下取整
        // console.log(count);

        let format = `${today.getFullYear()}/${today.getMonth()+1}/${today.getDate()}`

        let dayData = {
            count,
            format
        }
        // console.log(dayData);
        resolve(dayData);
    }).catch(err=> {
        reject(err);
    });
}

getDayDate();  // 获取时间信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 4. 请求墨迹天气获取数据

// 向服务器发起http请求
const superagent = require('superagent');  
const cheerio = require('cheerio');

// 请求墨迹天气获取数据
let getMojiData = ()=> {
    // 发起http请求
    superagent.get('https://tianqi.moji.com/weather/china/zhejiang/xiaoshan-district').end((err, res)=> {
        // 如果可以成功返回
        if(err) {
            return console.log("数据请求失败,请检查路径");
        }

        // console.log(res.text);

        // 把字符串解析成html,并可用jquery核心选择器获取内容
        let $ = cheerio.load(res.text);

        // 获取图标
        let icon = $('.wea_weather span img').attr('src');
        // console.log(icon);

        // 获取天气
        let weather = $('.wea_weather b').text();
        // console.log(weather);

        // 获取温度
        let temperature = $('.wea_weather em').text();
        // console.log(temperature);

        // 获取提示
        let tip = $('.wea_tips em').text();
        // console.log(tip);

        let mojiData = {
            icon,
            weather, 
            temperature,
            tip
        }
        console.log(mojiData);
    });
}
getMojiData();  // 请求墨迹天气获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 5. 请求one网站

该网站每天会出一张图和一句话

// 请求one抓取数据
let getOneData = ()=> {
    superagent.get("http://wufazhuce.com/").end((err, res)=> {
        // 如果可以成功返回
        if(err) {
            return console.log("数据请求失败,请检查路径");
        }
        // console.log(res.text);

        // 把字符串解析成html,并可用jquery核心选择器获取内容
        let $ = cheerio.load(res.text);

        // 获取图片
        let onePic = $('.carousel-inner .item.active img').attr('src');
        // let onePic = $('#carousel-one > div > div.item img').eq(0).attr('src');
        console.log(onePic);

        // 获取文本
        let oneTxt = $('.carousel-inner .item.active .fp-one-cita a').text();
        // let oneTxt = $('.carousel-inner .item .fp-one-cita a').eq(0).text();
        console.log(oneTxt);

        let oneData = {
            onePic, 
            oneTxt
        }

        console.log(oneData);
    });
}
getOneData();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 5. 等待数据请求完成

需要注意执行顺序,将所有方法全部改成promise的方式,并使用async和await控制

const superagent = require('superagent');  // 向服务器发起http请求
const cheerio = require('cheerio');

// 获取时间信息
let getDayDate = ()=> {
    // 由于很多数据要提前准备好,才能渲染到页面,最终才能发送
    // 这里就使用promise
    return new Promise((resolve, reject)=> {
        // 现在的时间
        let today = new Date();
        // 设定的时间(2019-5-20)
        let anniversary = new Date('2019-05-20');

        // 计算时间差毫秒值
        let count = today - anniversary;
        // 时间格式转换(转换为天并向上取整)
        count = Math.ceil(count/1000/60/60/24);
        // count = Math.floor(count/1000/60/60/24); // 向下取整
        // console.log(count);

        let format = `${today.getFullYear()}/${today.getMonth()+1}/${today.getDate()}`

        let dayData = {
            count,
            format
        }
        // console.log(dayData);

        resolve(dayData);  // 返回数据
    });
}
// getDayDate();  // 获取时间信息

// 请求墨迹天气获取数据
let getMojiData = ()=> {
    return new Promise((resolve, reject)=> {
        // 发起http请求
        superagent.get('https://tianqi.moji.com/weather/china/zhejiang/xiaoshan-district').end((err, res)=> {
            // 如果可以成功返回
            if(err) {
                return console.log("数据请求失败,请检查路径");
            }

            // console.log(res.text);

            // 把字符串解析成html,并可用jquery核心选择器获取内容
            let $ = cheerio.load(res.text);

            // 获取图标
            let icon = $('.wea_weather span img').attr('src');
            // console.log(icon);

            // 获取天气
            let weather = $('.wea_weather b').text();
            // console.log(weather);

            // 获取温度
            let temperature = $('.wea_weather em').text();
            // console.log(temperature);

            // 获取提示
            let tip = $('.wea_tips em').text();
            // console.log(tip);

            let mojiData = {
                icon,
                weather, 
                temperature,
                tip
            }
            // console.log(mojiData);

            resolve(mojiData);
        });
    });
}
// getMojiData();  // 请求墨迹天气获取数据

// 请求one抓取数据
let getOneData = ()=> {
    return new Promise((resolve, reject)=> {
        superagent.get("http://wufazhuce.com/").end((err, res)=> {
            // 如果可以成功返回
            if(err) {
                return console.log("数据请求失败,请检查路径");
            }
            // console.log(res.text);

            // 把字符串解析成html,并可用jquery核心选择器获取内容
            let $ = cheerio.load(res.text);

            // 获取图片
            let oneImg = $('.carousel-inner .item.active img').attr('src');
            // let onePic = $('#carousel-one > div > div.item img').eq(0).attr('src');
            // console.log(oneImg);

            // 获取文本
            let oneTxt = $('.carousel-inner .item.active .fp-one-cita a').text();
            // let oneTxt = $('.carousel-inner .item .fp-one-cita a').eq(0).text();
            // console.log(oneTxt);

            let oneData = {
                oneImg, 
                oneTxt
            }
            // console.log(oneData);

            resolve(oneData);
        });
    });
}
// getOneData();

// 通过模板引擎替换html
// 需要注意执行顺序
let renderTemplate = async ()=> {
    // 在方法里面涉及到数据请求(异步),会造成数据还没拿到,就开始渲染dom了
    // 使用async和await
    // 如果要用await调用得到函数,那么该函数必须返回的是promise对象

    // 获取日期 - 等待请求完成之后才进行赋值
    let dayDate = await getDayDate();

    // 获取墨迹天气 - 等待请求完成之后才进行赋值
    let mojiData = await getMojiData();

    // 获取one数据 - 等待请求完成之后才进行赋值
    let oneData = await getOneData();

    // 所有数据获取成功之后,才进行模板引擎数据的替换
    console.log(dayDate);
    console.log(mojiData);
    console.log(oneData);
}
renderTemplate();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135

# 6. 通过模板引擎替换html

art-template

const template = require('art-template');  // 导入模板引擎
const path = require('path');

// 通过模板引擎替换html
// 需要注意执行顺序
let renderTemplate = async ()=> {
    // ......
    // 代码见上

    // 所有数据获取成功之后,才进行模板引擎数据的替换
    /* console.log(dayDate);
    console.log(mojiData);
    console.log(oneData); */

    // 使用模板方法
    let html = template(path.join(__dirname, './res.html'), {
        // 在这里定义的值,在模板文件可以直接使用双花括号的形式引用
        // test: '测试'
        dayDate,
        mojiData,
        oneData
    })
    console.log(html);
}
renderTemplate();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  • res.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>res</title>
</head>
<body>
    <section id="date">
        {{ dayDate.count }}天
        {{ dayDate.format }}
    </section>
    <section id="weather">
        <img src="{{mojiData.icon}}" alt="">
        {{ mojiData.temperature }}
        {{ mojiData.tip }}
    </section>
    <section id="tips">
        <img src="{{oneData.oneImg}}" alt="">
        {{ oneData.oneTxt }}
    </section>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 7. 使用node发送邮件

nodemailer

const nodemailer = require('nodemailer');

// 通过模板引擎替换html
// 需要注意执行顺序
let renderTemplate = async ()=> {
  // ......
  代码见上

  // 使用模板方法
  return new Promise((resolve, reject)=> {
      let html = template(path.join(__dirname, './res.html'), {
        // 在这里定义的值,在模板文件可以直接使用双花括号的形式引用
        // test: '测试'
        dayDate,
        mojiData,
        oneData
      })
      // console.log(html);

      resolve(html);  // html作为成功之后的参数
  });
}

// 发送电子邮件
let sendNodeMail = async ()=> {
    // 必须要等数据全部渲染完成之后,才往后面走
    let html = await renderTemplate();
    // console.log(html);

    // 使用默认SMTP传输,创建可重用邮箱对象
    let transporter = nodemailer.createTransport({
        host: "smtp.163.com",  // 网易邮箱SMTP服务器
        port: 465,
        secure: true, // 开启加密协议,需要使用465端口(网易ssl协议)
        auth: {
            user: 'xxxx@163.com', // 邮箱地址
            pass: "JXOKFCNHYHGKGSRE", // 客户端授权密码(网易邮箱界面、设置、开启IMAP/SMTP)
        },
    });

    // 设置电子邮件数据
    let mailOptions = {
        from: "'xxxx' <xxxx@163.com>",  // 发件人邮箱
        to: 'xxxx@qq.com',  // 收件人列表(可以用逗号隔开)
        subject: '测试邮件',
        html: html  // html内容
    }

    // 发送邮件
    transporter.sendMail(mailOptions, (err, info={})=> {
        if(err) {
            console.log(err);
            sendNodeMail();  // 再次发送
        }

        console.log('邮件发送成功', `${info.response}`);
        console.log('等待下一次发送');
    });
}
sendNodeMail();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

# 8. 设置定时任务

// 创建定时任务
// 定时每天发送(每天5时20分14秒发送邮件)
(()=> {
    // 每天5时20分14秒
    schedule.scheduleJob('14 20 5 * * *',()=>{
        // 开始发送邮件
        console.log(new Date());
        console.log("===================");
        console.log("====开始发送邮件====");
        console.log("===================");

        sendNodeMail();
    }); 
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 9. 全部代码

const superagent = require('superagent');  // 向服务器发起http请求
const cheerio = require('cheerio');

const template = require('art-template');  // 导入模板引擎
const path = require('path');

const nodemailer = require('nodemailer');  // 邮件

const schedule = require("node-schedule");  // 定时任务

// 工具集
/* let util = {
    // 时间戳
    dateFormate(now) {
        let year = now.getFullYear(); // 年
        let month = now.getMonth() + 1;  // 月
        let date = now.getDate();  // 日

        // 加上0
        month < 10 ? month=`0${month}` : month;  // 月
        date < 10 ? date=`0${date}` : date;  // 日

        return `${year}-${month}-${date}`
    },

    // 计算时间
    getCount() {
        // 现在的时间
        let today = new Date();
        // 设定的时间(2019-5-20)
        let anniversary = new Date('2019-05-20');

        // 计算时间差毫秒值
        let count = today - anniversary;
        // 时间格式转换(转换为天并向上取整)
        count = Math.ceil(count/1000/60/60/24);
        // count = Math.floor(count/1000/60/60/24); // 向下取整
        // console.log(count);

        return count;
    }
} */

// 获取时间信息
let getDayDate = ()=> {
    // 由于很多数据要提前准备好,才能渲染到页面,最终才能发送
    // 这里就使用promise
    return new Promise((resolve, reject)=> {
        // 现在的时间
        let today = new Date();
        // 设定的时间(2019-5-20)
        let anniversary = new Date('2019-05-20');

        // 计算时间差毫秒值
        let count = today - anniversary;
        // 时间格式转换(转换为天并向上取整)
        count = Math.ceil(count/1000/60/60/24);
        // count = Math.floor(count/1000/60/60/24); // 向下取整
        // console.log(count);

        let format = `${today.getFullYear()}/${today.getMonth()+1}/${today.getDate()}`

        let dayData = {
            count,
            format
        }
        // console.log(dayData);

        resolve(dayData);  // 返回数据
    });
}
// getDayDate();  // 获取时间信息

// 请求墨迹天气获取数据
let getMojiData = ()=> {
    return new Promise((resolve, reject)=> {
        // 发起http请求
        superagent.get('https://tianqi.moji.com/weather/china/zhejiang/xiaoshan-district').end((err, res)=> {
            // 如果可以成功返回
            if(err) {
                return console.log("数据请求失败,请检查路径");
            }

            // console.log(res.text);

            // 把字符串解析成html,并可用jquery核心选择器获取内容
            let $ = cheerio.load(res.text);

            // 获取图标
            let icon = $('.wea_weather span img').attr('src');
            // console.log(icon);

            // 获取天气
            let weather = $('.wea_weather b').text();
            // console.log(weather);

            // 获取温度
            let temperature = $('.wea_weather em').text();
            // console.log(temperature);

            // 获取提示
            let tip = $('.wea_tips em').text();
            // console.log(tip);

            let mojiData = {
                icon,
                weather, 
                temperature,
                tip
            }
            // console.log(mojiData);

            resolve(mojiData);
        });
    });
}
// getMojiData();  // 请求墨迹天气获取数据

// 请求one抓取数据
let getOneData = ()=> {
    return new Promise((resolve, reject)=> {
        superagent.get("http://wufazhuce.com/").end((err, res)=> {
            // 如果可以成功返回
            if(err) {
                return console.log("数据请求失败,请检查路径");
            }
            // console.log(res.text);

            // 把字符串解析成html,并可用jquery核心选择器获取内容
            let $ = cheerio.load(res.text);

            // 获取图片
            let oneImg = $('.carousel-inner .item.active img').attr('src');
            // let onePic = $('#carousel-one > div > div.item img').eq(0).attr('src');
            // console.log(oneImg);

            // 获取文本
            let oneTxt = $('.carousel-inner .item.active .fp-one-cita a').text();
            // let oneTxt = $('.carousel-inner .item .fp-one-cita a').eq(0).text();
            // console.log(oneTxt);

            let oneData = {
                oneImg, 
                oneTxt
            }
            // console.log(oneData);

            resolve(oneData);
        });
    });
}
// getOneData();

// 通过模板引擎替换html
// 需要注意执行顺序
let renderTemplate = async ()=> {
    // 在方法里面涉及到数据请求(异步),会造成数据还没拿到,就开始渲染dom了
    // 使用async和await
    // 如果要用await调用得到函数,那么该函数必须返回的是promise对象

    // 获取日期 - 等待请求完成之后才进行赋值
    let dayDate = await getDayDate();

    // 获取墨迹天气 - 等待请求完成之后才进行赋值
    let mojiData = await getMojiData();

    // 获取one数据 - 等待请求完成之后才进行赋值
    let oneData = await getOneData();

    // 所有数据获取成功之后,才进行模板引擎数据的替换
    /* console.log(dayDate);
    console.log(mojiData);
    console.log(oneData); */

    // 使用模板方法
    return new Promise((resolve, reject)=> {
        let html = template(path.join(__dirname, './res.html'), {
            // 在这里定义的值,在模板文件可以直接使用双花括号的形式引用
            // test: '测试'
            dayDate,
            mojiData,
            oneData
        })
        // console.log(html);

        resolve(html);  // html作为成功之后的参数
    });
}
// renderTemplate();

// 发送电子邮件
let sendNodeMail = async ()=> {
    // 必须要等数据全部渲染完成之后,才往后面走
    let html = await renderTemplate();
    // console.log(html);

    // 使用默认SMTP传输,创建可重用邮箱对象
    let transporter = nodemailer.createTransport({
        host: "smtp.163.com",  // 网易邮箱SMTP服务器
        port: 465,
        secure: true, // 开启加密协议,需要使用465端口(网易ssl协议)
        auth: {
            user: 'xxxx@163.com', // 邮箱地址
            pass: "xxxxxx", // 客户端授权密码(网易邮箱界面、设置、开启IMAP/SMTP)
        },
    });

    // 设置电子邮件数据
    let mailOptions = {
        from: "'xxxx' <xxxx@163.com>",  // 发件人邮箱
        to: 'xxxx@qq.com',  // 收件人列表(可以用逗号隔开)
        subject: '测试邮件',
        html: html  // html内容
    }

    // 发送邮件
    transporter.sendMail(mailOptions, (err, info={})=> {
        if(err) {
            console.log(err);
            sendNodeMail();  // 再次发送
        }

        console.log('邮件发送成功', `${info.response}`);
        console.log('等待下一次发送');
    });
}
// sendNodeMail();

// 创建定时任务
// 定时每天发送(每天5时20分14秒发送邮件)
(()=> {
    // 每天5时20分14秒
    schedule.scheduleJob('14 20 5 * * *',()=>{
        // 开始发送邮件
        console.log(new Date());
        console.log("===================");
        console.log("====开始发送邮件====");
        console.log("===================");

        sendNodeMail();  // 发送电子邮件
    }); 
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242


~End~