阅读 495

学习node.js,简单爬虫并定时爬取并去重插入阿里云ECS云端的数据库

      学习vue技术栈很久了,一直没有实践,想着自己私下时间做一个功能简单的app玩,并把vue这个技术栈可以用在app相关的相关功能都试着用一次,首先做一个app需要数据支撑的,就想着自己去别的网站抓一点数据过来,虽然团队有python爬虫,但相比python,node.js对于前端来说学起来更加友好,而且前端时间抽空也看了一点node.js相关的文档,就选择node.js来写爬虫,而且cnode社区,掘金,github等很多社区对于node.js写爬虫的相关技术支撑还是很强大的,废话不多说,接下来开始一步步来吧。

一、新建项目

打开命令提示符,进入你想放文件的文件夹下


然后 npm init

npm init 用来初始化生成一个新的 package.json 文件。它会向用户提问一系列问题,如果你觉得不用修改默认配置,一路回车就可以了

这个设置的是项目的详情,可以不填写,直接都enter下来


这样一个项目就初始化好了。

二、抓取数据

新建app.js

用到的依赖库superagent(visionmedia.github.io/superagent/)、cheeriogithub.com/cheeriojs/c…)、mysql、node-schedule、nodemailer、fnv-plus

  • superagent 是个 http 方面的库,可以发起 get 或 post 请求
  • cheerio 可以理解成一个 Node.js 版的 jquery,用来从网页中以 css selector 取数据,使用方式跟 jquery 一样一样的
  • mysql 连接mysql数据库必须的依赖库
  • nodemailer 邮箱发送的依赖模块
  • fnv-plus 字符串转hash的依赖库
  • node-schedule 定时模块定时启动抓取数据函数函数

我们应用的核心逻辑长这样

const cheerio = require('cheerio'); //引用cheerio模块,使在服务器端像在客户端上操作DOM,不用正则表达式
const superagent = require('superagent');  //是个 http 方面的库,可以发起 get 或 post 请求
const mysql = require('mysql');
const schedule =require('node-schedule');
//nodemailer模块邮箱发送
const nodemailer = require('nodemailer');
//转hash模块
const fnv = require('fnv-plus');
//盛放每次抓取的内容
let items = [];

//创建数据库连接
const db = mysql.createConnection({
    host: 'xxxxxxx', //云端服务器公共ip
    user: 'xxxxxxx', //mysql数据库的用户名
    password: 'xxxxxx', //mysql数据库的密码
    port: '3306', //端口
    database: 'newsdb' //mysql数据库存储数据的数据库名称
});
db.connect(function (err) {
    if (err) console.log('连接失败');
    else {
        console.log('连接成功');
    }
});
//抓取数据
function fetchData() {
    var link = "http://news.baidu.com/ent";
    superagent.get(link)
        .end(function (err, sres) {
            if (err) {
                return next(err);
            }
            let $ = cheerio.load(sres.text);

            $('#instant-news ul li a').each(function (idx, element) {
                let $element = $(element);
                //转hash哈希值
                let titleHash = fnv.hash($element.text(), 64).str();
                let info = {
                    title: $element.text(),
                    link: $element.attr('href'),
                    source: link,
                    typeId: 1,
                    typeName: $('#col_toparea .mod .hd h3').text(),
                    grabDate: new Date().toLocaleString(),
                    // grabDate: new Date()
                    titleHash: titleHash
                };
                items.push(info);
                //保存数据 test_table是存放数据的表
                db.query('insert into test_table set ?', info, function (err, result) {
                    // if (err) throw err;
                    try {
                        if (!!result) {
                            console.log('插入成功');
                            console.log(result.insertId);
                        } else {
                            console.log('插入失败');
                        }
                    }catch (err){
                        console.log('插入失败');
                    }
                });
            });
            // db.end(); //在每次定时任务插入数据库后我写了一个end()方法,取消数据库的连接,结果下一次重启抓取信息后,上面连接数据库的方法不会再执行,就会报Cannot enqueue quit after invoking quit.连接不上数据库,只能把这个方法去掉
            //执行发送邮件的函数
            console.log(items);
            sendmail();
        });
}

//抓取的内容发送QQ邮箱(是为了每次抓取的内容都发送邮箱);
let transporter = nodemailer.createTransport({
    service: 'qq',
    auth: {
        user: 'xxxxxxxxxx@qq.com', //邮箱登录账号
        pass: 'xxxxxxxxxxxxxx' //使用QQ邮箱登录密码是不正确的,必须使用QQ邮箱里的授权码,这个请注意,底部有说明
    }
});
let mailOptions = {
    //发送者
    from: 'xxxxxxxxxx@qq.com',
    //接受者,可以同时发送给多个邮箱,以逗号隔开
    to: 'xxxxxxxxxx@qq.com,xxxxxxxxxx@qq.com',
    //发送内容的标题
    subject: '实时新闻抓取内容',
    //内容
    //text: 'Hello world', // 文本
    html: '<h2>实时新闻已经抓取</h2>'
};
//执行发送邮件的函数
function sendmail() {
    transporter.sendMail(mailOptions, function (err, info) {
        if (err){
            console.log('发送失败,失败原因是:'+err);
            return;
        }else {
            console.log('发送成功');
        }
    });
}


//定时模块定时启动抓取数据函数函数
let rule     = new schedule.RecurrenceRule();
let times    = [1,3,5,7,9,11,13,15,17,19,21,23];
rule.hour  = times;
rule.minute = 0;
schedule.scheduleJob(rule, function(){
    fetchData();
});复制代码

注意事项:

1、数据去重的思路

因为我试验抓的是百度新闻其中一个版块的实时新闻,总共就10条,然后两个小时抓一次,每次抓取肯定有重复数据,就需要每次抓取数据后跟库里存的数据对比去重,去重思路如下:

  • 去重是通过索引去重,去重依据的数据是根据新闻的标题,一个字段存标题,对应字段为title,另外新建一个专门的字段titleHash,存放抓取数据后使用
    fnv-plus复制代码
    把标题转成哈希散列值,存放titleHash这个字段里,然后建表时对titleHash建一个索引,索引类型选Unique(唯一类型),索引方法选BTREE,然后存数据的时候数据库会自动对比titleHash这个字段存的哈希散列值,如果库里存在同样的哈希散列值,就会报错,通过try catch抛出错误,就会继续把抓到的数据对比插入数据。
  • 专门把新闻标题转成哈希散列值好处是,如果数量级比较大时,因为title转成哈希值了,而哈希值的长度固定,也不长,这样生成的索引也不长,索引占的磁盘也就不会太大  


此次使用node.js抓取数据,往库里存数据的疑惑:

此次使用 fnv-plus 往邮箱发送抓取数据通知时,设想是把抓取的数据以列表的形式发送到预定邮箱,代码如下:


我是把抓取的数据放在全局的items的数组中,然后放在textContent()函数返回,打印textContent()是有数据的,但是发送到邮箱里的就是空的,有大神可以知道一下怎么实现我的这个设想吗?


文章分类
前端