←首页

某天下班前想给自己加点菜,学点 Node 吧,写个小爬虫脚本?

爬虫,也叫🕷,写好脚本,爬虫能够自动抓取到网页上的内容。举个例子,我想知道 阿维 最近写了些什么。

1
2
3
4
5
6
7
8
9
10
11
12
const request = require('request')
const cheerio = require('cheerio')

request('http://aevit.xyz', (error, response, body) => {
if (!error && response.statusCode == 200) {
var $ = cheerio.load(body)
$('#posts .post-title a').each((index, ele) => {
var title = $(ele).text().trim()
console.log(title)
})
}
})

结果
几行代码就搞定了。 你可能会想,这不是傻逼吗,我一行代码也不用谢,直接浏览器打开不行吗?那是当然,但是如果每天都让你去翻一遍阿维的博客,你不会吐吗,这么丑的主题。。。

上面的爬虫代码用到了两个库 request 和 cheerio,request 是用来发 http 请求的,正常响应并拿到结果时使用 cheerio 去 load response body,然后接下来的 jQuery like 代码就没什么好说的了。通过 $ 符号去获取页面上的元素。注意这里的选择器是有特异性的,不同网站的代码结构,命名都不同,这是针对阿维博客写的代码。

自己给自己提个需求,很简单,(当然代码也是)是这样的:

  1. 定时爬阿维博客的最新文章
  2. 发现有更新及时发送邮件

为了完成我的需求,我把代码划分为四个部分,定时器 - 爬虫 - 决策者 - 邮递员

  • 定时器是跑一个定时脚本,定时去叫醒爬虫,嘿起来了伙计,看看阿维的博客怎么样了。
  • 爬虫把爬到的结果交给决策者,喏,这是我爬到的结果,最新文章是 《澳门线上赌场,性感荷官在线发牌》,你看着办吧。
  • 决策者拿到结果,回想上次爬虫给我的结果是什么来的? 噢,上次是《邻家少妇的诱惑.avi》,不一样啊!我得赶紧报警。
  • 邮递员拿到信件,我才不管你们什么小泽玛利亚,我只负责送信。
  • ding~ Helkyle 你好,阿维更新了博客《澳门线上赌场,性感荷官在线发牌》

思路有了,代码写起来更清晰,把上面的语言翻译成代码。

使用 nodemailer 免费发送邮件

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
// mailer.js
'use strict';
const nodemailer = require('nodemailer');

function sendEmail (event) {
let transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'xxx',
pass: 'xxx'
},
tls: {
// gmaile 需要开启低安全模式
rejectUnauthorized: false
}
})
let mailOptions = {
from: 'mailer <mailer@foo.com>',
to: '306295636@qq.com',
subject: 'Hello Evan✔',
text: `${event.name} 更新了文章,文章标题:${event.lastPostTitle}`
}

// send mail with defined transport object
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
return console.log(error)
}
console.log('Message sent!!!')
})
}

module.exports = {
sendEmail
}

决策者需要有记忆功能,可以考虑用 fs 直接读写文件保存,也可以用 mongo

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
var mongoose = require('mongoose')

mongoose.Promise = global.Promise
mongoose.connect('mongodb://localhost/test', {useMongoClient: true})

// 创建 Schema
var SitePostSchema = new mongoose.Schema({
url: String,
lastestTitle: String,
name: String
})

// 创建 model
var SitePost = mongoose.model('SitePost', SitePostSchema)

// 新增或更新
function createOrUpdate (siteToJudge) {
return SitePost.findOne({
url: siteToJudge.url
}).then(site => {
if (!site) {
var newSite = new SitePost(siteToJudge)
newSite.save()
return Promise.resolve(newSite)
} else if (site.lastestTitle === siteToJudge.lastestTitle) {
return Promise.resolve(site)
} else {
var tempSite = {
...site
}
site = Object.assign(site, siteToJudge)
site.save()
return Promise.resolve(tempSite)
}
}).catch((err) => {
console.log(err)
})
}

module.exports = {
createOrUpdate
}

爬虫就只负责爬,需要扩展的话,选择器(爬的方法)得从外部传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const request = require('request')
const cheerio = require('cheerio')

function crawl (url) {
return new Promise((resolve, reject) => {
request(url, (error, response, body) => {
if (!error && response.statusCode == 200) {
var $ = cheerio.load(body)
var title = $($('#posts .post-title a')[0]).text().trim()
resolve(title)
} else {
reject()
}
})
})
}

module.exports = {
crawl
}

定时器我用 forever + setTimeout 实现,每半个钟去爬一次,forever 可以让你的脚本在后台持久执行,而不是执行一次之后就退出了。

1
2
3
4
5
6
7
8
9
10
var main = require('./main')

main.loop()
setInterval(() => {
main.loop()
}, 30 * 60 * 1000)
```

``` bash
forever index.js

阿维和潘朵拉博客的代码结构基本一致,同样的选择器也适用,所以同时爬两个也很容易。

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
'use strict';
const mongoose = require('./mongoose')
const mailer = require('./mailer')
const crawler = require('./crawler')

const SITES_TO_CRAWL = [
{
url: 'http://aevit.xyz',
name: '阿维'
},
{
url: 'http://pandara.xyz',
name: '潘*啦'
}
]

function checkUpdate (site) {
return mongoose.createOrUpdate(site)
.then((dataOnDb) => {
console.log(dataOnDb)
if (dataOnDb.lastestTitle === site.lastestTitle) {
return Promise.resolve({
isNew: false,
data: site
})
} else {
return Promise.resolve({
isNew: true,
data: site
})
}
})
}

function sendEmail (data) {
if (data) {
mailer.sendEmail(data)
}
}

function loop () {
for (var i = 0; i < SITES_TO_CRAWL.length; i ++) {
(function (site) {
crawler.crawl(site.url)
.then((lastestTitle) => {
return {
...site,
lastestTitle
}
})
.then(checkUpdate)
.then(({isNew, data}) => {
if (isNew) {
sendEmail(data)
}
})
.catch(() => {
})
})(SITES_TO_CRAWL[i])
}
}

module.exports = {
loop
}

组合在一起之后,阿维每次更新文章,一小时内我都能收到消息推送了~

阿维真不是什么好家伙

使用 request 直接爬取页面已经能够胜任大部分的工作了,但是如果爬的网站使用动态渲染的话,这种方式抓不到东西,可以使用 phantomjs 等待页面加载完毕之后再执行脚本。

花了下班后的三个钟,边学边写,收获良多,有空再继续深入学习。

切页面写多了,会产生狭隘的世界观,看到的天空只有井口那么大,常常想做点突破,为什么总是绕一圈回来了,好奇怪的人类…

如需转载,请注明出处: http://w3ctrain.com / 2017/10/31/node-crawler-pratice/

helkyle

我叫周晓楷

我现在是一名前端开发工程师,在编程的路上我还是个菜鸟,w3ctrain 是我用来记录学习和成长的地方。