使用NodeJs 实现新闻Rss爬虫

什么是Rss

简易信息聚合(也叫聚合内容)是一种RSS基于XML标准,在互联网上被广泛采用的内容包装和投递协议。RSS(Really Simple Syndication)是一种描述和同步网站内容的格式,是使用最广泛的XML应用。

通俗的说就是简易的数据,里面包含各种信息

为什么要获取并分析Rss

  1. 通过获取Rss可以将多数据集合(例如自动转载文章)

  2. 分析网站内容,获取有用信息

  3. 用于练习抓包,为以后进行数据分析做铺垫

使用SuperAgent 和 FeedParser 获取并分析 基地博文

SuperAgent 是nodeJs 的一个轻量级请求模块,可以用来方便发送HTTP请求,我们用它来获取Rss

FeedParser 是nodeJs 的一个关于Rss 的分析模块,我们可以使用它来分析获取到的数据

具体操作

当然首先安装这两个模块

npm install superagent feedparser --save-dev

使用superAgent

我们先尝试superAgent 模块使用

在根目录下,新建一个data.xml文件和sa.js

然后在sa.js中写获取的代码

var superagent = require("superagent"),
    fs         = require("fs"),
    feedparser = require("feedparser")

var Url = "http://xgcyjd.com/feed.xml",
    wStream = fs.createWriteStream("./data.xml")  // 开辟写IO流

var req_stream = superagent.get(Url)
    .buffer()
    .type("xml")
    .on("end", function (err, pres) {
        if(err) {
            console.log(err);
        }
    })
req_stream.pipe(wStream) // 数据写入

然后执行

node sa

你会发现数据都被写入到了xml中了

shootpic

使用feedParser

根据刚刚获取的数据,我们就可以使用feedParser进行分析了,让我们尝试打印出博文聚合的文章列表吧

再新建一个 fp.js

并写入如下代码

var fs = require("fs"),
    feedparser = require("feedparser"),
    feed = __dirname + "/data.xml"

fs.createReadStream(feed)
    .on("error", function (err) {
        console.error(err);
    })
    .pipe(new feedparser())
    .on("meta", function (meta) {
        console.log("====== %s ======", meta.title);
    })
    .on("readable", function () {
        var stream = this,
            item

        while(item = stream.read()) {
            console.log("获取到了文章:%s", item.title || item.description);
        }
    })

然后

node fp

shootpic

很优雅的打印出来了

当然,我们可以把superAgent 部分的代码 和 feedparser 的代码合起来

var superagent = require("superagent"),
    fs         = require("fs"),
    feedparser = require("feedparser")

var Url = "http://xgcyjd.com/feed.xml",
    fp  = new feedparser()

var req_stream = superagent.get(Url)
    .buffer()
    .type("xml")
    .end(function (err, pres) {
        if(err) {
            console.log(err);
        }
    })

req_stream.pipe(fp)
    .on("error", function (err) {
        console.error(err)
    })
    .on("meta", function (meta) {
        console.log("===== %s =====", meta.title)
    })
    .on("readable", function () {
        var $stream = this,
            item

            while(item = $stream.read()) {
                console.log("获取到了文章: %s", item.title || item.description)
            }
    })

效果是一样

实战演示

上面我们只是很简单的尝试了这两个模块,现在让我们用这两个模块来干些事儿

例如百度的 Rss新闻集合

目标

“尝试获取百度 国内、国际、社会 三个方面的新闻Rss 转为JSON格式 并且汇总到mongoDB数据库中”

思路是先从 这三个方面分别异步的获取Rss资源,等全部获取完成后 再数据转化为JSON 并存入电脑

这种方式,业内一般专业的称呼为 “异步并发” 先分别异步获取,在一并执行下一步

首先,确保你已经装上了mongoDB 数据库

怎样实现nodeJs异步并发

由于抓取的数据所消耗的时间不一,我们常常需要写一个计数器 进行维护

例如下列伪代码:


var i = 0,
    len = 3,
    dataStack = []

$.get("国内Rss",function (data) {
    i++;
    dataStack.push(data);
    timer();
})

$.get("国外Rss",function (data) {
    i++;
    dataStack.push(data);
    timer();
})

$.get("社会Rss",function (data) {
    i++;
    dataStack.push(data);
    timer();
})

function timer (data) {
    ifi === len {
        handleData (dataStack); // 三个Rss都加载完成后,处理数据栈
    }
}

这种方法不是不行,但是并不优雅,或许我们可以尝试另外一种方法

朴灵 给我们带来了他解决方案 eventproxy

eventproxy 也是一个nodeJs 模块,用于处理异步并发

将上面的代码转换成eventproxy方式

var eventproxy = require("eventproxy"),
    ep         = new eventproxy();

$.get("国内Rss",function (data) {
    ep.emit("get_event",data)
})

$.get("国外Rss",function (data) {
    ep.emit("get_event",data)
})

$.get("社会Rss",function (data) {
    ep.emit("get_event",data)
})


// after 获取Rss后 进行处理
ep.after("get_event", 3 ,function (data) {
    handleData(data)
})

希望大家详细的了解一下这个模块

准备工作

我们首先建立一个简单的文件路径


./
 |
  `--database
        |
         `--dao.js
 |
  `--app.js
 |
  `--service.js
 |
  `--package.Json

package.Json 如下

{
  "name": "get_info",
  "version": "1.0.0",
  "description": "get_info by superagent, cheerio, eventproxy and mongoDB",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "babel index.js -d dist -s | node dist/index.js"
  },
  "author": "你的名儿",
  "dependencies" : {
    "eventproxy": "",
    "superagent": "",
    "superagent-charset": "",
    "feedparser": "",
    "iconv-lite": "",
    "mongoose": ""
  },
  "license": "MIT"
}

service.js 用于实际的获取数据

app.js 是入口文件可以用来消费数据(这里暂时不涉及消费数据,所以app逻辑比较简单)

dao.js 用于配置数据库

实际演练

现在让我们投入实战,并且讲解一下一些坑

首先在 service 中我们可以新建一个class (开始涉及一点Es6语法),然后

class service {
   constructor() {
        this.result = []; // 数据暂存
   } 

   getData (valueList, callback) {
        
   }

   saveData () {

   }
}

主要逻辑是getData 和 saveData,我们先来比较简单的app.js

var fs              = require("fs"),
    service         = require("./service.js")

var DataPort = [
   {  
       name: "国内焦点",
       category: "civilnews",
       url: "http://news.baidu.com/n?cmd=1&class=civilnews&tn=rss"
   },{
       name: "国际焦点",
       category: "internews",
       url: "http://news.baidu.com/n?cmd=1&class=internews&tn=rss"
   },{   name: "社会焦点",
       category: "socianews",
       url: "http://news.baidu.com/n?cmd=1&class=socianews&tn=rss"
   }
];


service
.getData(DataPort,function ($self) {
  
  $self.saveData();
})

很简单就是简单的引入模块,执行函数

superAgent 的两个大坑

再来看看获取数据

获取数据很简单,我们使用上文的方式处理就行了,我这次换成流的方式来写

var fp = new feedParser();

superAgent.get(url)
    .type("xml")
    .on("error", function (err) {
       console.log("===== 获取数据失败 =====") 
       console.log(err);
    })    
    .pipe(fp)
    .on("error", function (err) {
        console.log("===== 解析数据失败 =====");
        console.error(err);
    })
    .on("readable", function () {
        var $stream = this,
            item

        while(item = $stream.read()) {                         
            // 处理你分析好的数据
        }
    })
    .on("end", function () {
         if(err) {
            console.log(">>>>>> 出现错误!!!");
            console.log(err);
        } else {
            
            console.log("===== 已解析完数据 =====");
        }
    })

上面的code 展示了获取单个数据的情况

注意,这里会有两个大坑,那就是转码问题

  1. 百度存储默认的是以GB2312存储,但是nodeJs 暂时并不支持GB2312编码 所以我们需要进行转码
  2. superAgent 暂时不支持除utf-8以外的其他格式, 对于任何非utf-8 的文档,会强行以utf-8进行编码

这两个问题加起来很坑(主要是一开始并没有意识到)找错误费了不少功夫

首先转码,我们选用 iconv-lite 模块

只需要在流入fp前 进行转码即可:

var iconv = require("iconv-lite")

...

.pipe(iconv.decodeStream("gb2312"))
.pipe(iconv.encodeStream("utf-8"))
.pipe(iconv.decodeStream("utf-8"))

再来就是 将superAgent 的文件修改编码

我们选用 一个国人写模块 superagent-charset

将superAgent 重新配置一下

var request         = require("superagent"),
    charsetCompoent = require('superagent-charset'),
    superagent      = charsetCompoent(request);

然后在发送请求之前

superagent.get(value.url)
    .charset('gb2312')
    .type("xml")

将编码设置为GB2312

feedparser 的一个大坑

feedparser 没什么,但是依然遇到了个BUG,跑去问公司大牛,弄了一个半小时解决~

主要就是获取多类新闻数据组的时候,解析出现了数据丢失,

经过分析,最终确认是,fp的问题

一开始写的是

var fp = new feedparser();

DataPort = forEach(function(value, idx) {
    superAgent
        .xxx
        .xxx

        .pipe(fp)

解决完后是

DataPort = forEach(function(value, idx) {
    var fp = new feedparser();
    superAgent
        .xxx
        .xxx

        .pipe(fp)

因为 superAgent 获取数据是异步的,而单个fp解析能力是有限的

第一组内容完成后,fp开始解析,在解析过程中,第二组内容也获取完毕 依照一开始写的方式,fp就会丢掉正在解析的第一组数据,转而去解析第二组,从而产生数据丢失

而第二种方式,每次解析完成后都会新生成一个fp实例进行解析,2组对应2个fp 这样就不会发生数据丢失的行为!

数据存储

我们再来看saveData

for(var i = 0; i<$self.result.length; i++) {
    // 存储数据
    ep.emit("save_Data");

}

// 每组数据存储完毕后
ep.after("save_Data", $self.result.length , function () {
    db.close(); // 关闭数据库
})

很简单的逻辑

具体源码

上文只是解析了部分代码,具体源码请见:Get_BaiDuNewsRss_By_NodeJS

- CATALOG -