基于Angular+express的定时爬虫信息综合application

来源:互联网 发布:虚拟固话软件 编辑:程序博客网 时间:2024/06/13 22:41

之前,我写过一篇博文:用node从零开始去写一个简单的爬虫

之后,我就顺势做了一个信息综合的网页,定时采取那些我们必须关注,经常发布公告的机构网站的信息,然后存储到数据库,然后加以处理综合,显示在网页上,这样就不用每次去一个个的点各个机构的网站,不怕麻烦,也不怕漏掉哪个网站的公告。

这是目前初步做好了2个网站应用逻辑之后的界面,暂时还未进行UI的美化


这篇博文的代码已传到github:点击这里

我们来开始一步步的分析这个应用。

这个应用的逻辑很清楚:爬取网页,存储数据到数据库,然后从数据库中读取数据,数据传输到前端显示

所以我们把前后端分离,采用RESTFUL架构,因为我们这个应用只会用get,所以更加简单。

如果不了解RESTFUL API的话,可以看看阮老师的这两篇博文:
理解RESTful架构
RESTful API 设计指南
简单的来说:

  • 每一个URI代表一种资源;
  • 客户端和服务器之间,传递这种资源的某种表现层;
  • 客户端通过四个HTTP动词,对服务器端资源进行操作,实现”表现层状态转化”。

先来分析后端程序

后端主要是2个操作,一个是爬取网页,存储数据到数据库;二是根据前端传过来的参数查询数据库并返回相应的json数据

爬取主要用request模块和cherrio模块,我在之前那篇博文已经说了具体的,就不再多说了,来贴上代码,以教务处网站实际分析

Read.js

var originRequest = require('request');var cheerio = require('cheerio');/** * 请求指定URL */function request (url, callback) {  originRequest(url, callback);}/** * 获取公告文章列表 */exports.newsList = function (callback) {    request('http://jwc.scu.edu.cn/jwc/frontPage.action', function (err, res) {        if (err) return callback(err);        // 根据网页内容创建DOM操作对象        var $ = cheerio.load(res.body.toString());        // 读取列表        var newsList = [];        $('body').find('table').eq(4).children().each(function () {            var tableTitle = $(this).children('td').eq(0).children('span').children('a');            var newsID = $(tableTitle).attr('href').toString().substring(24);            var newsTitle = $(tableTitle).children('span').eq(0).text();            var newsTime = $(this).children('td').eq(1).children().text().toString().substring(2, 12);            var item = {                title: newsTitle,                id:  newsID,                time:newsTime            };            newsList.push(item);        });    });};/** * 依次获取所有公告消息的内容 */exports.articleList = function (id, callback) {  request('http://jwc.scu.edu.cn/jwc/newsShow.action?news.id='+id, function (err, res) {    if (err) return callback(err);    // 根据网页内容创建DOM操作对象    var $ = cheerio.load(res.body.toString());    // 读取    var articleList = [];    var fileList = [];    $('body').children('table').eq(3).find('tr').each(function () {        if($(this).find('td').length>1) {            var TableFile = $(this).children().last().children('a');            var fileId = TableFile.attr('href').toString().substring(25);            var fileName = TableFile.text();            var item = {                id:   id,                fileid:   fileId,                fileName: fileName            };            fileList.push(item);        }    });      var newslink = 'http://jwc.scu.edu.cn/jwc/newsShow.action?news.id=' + id;      var article= $('#news_content').attr('value');      var newitem = {          id: id,          content: article,          link:newslink      };      articleList.push(newitem);  });};

代码的结构很清晰,一个读取主页上公告的列表,另外一个读取具体公告的内容。

读取的时候将读取到的数据存储到相应的json数组中

然后要做的是将爬取到的数据存储到Mysql数据库中

我建立了3个表,分别用来:存储主页公告列表中的公告ID,公告名字,公告时间,公告站点;存储公告的内容;存储公告的附件及链接

这是sql文件的设计格式
news-main:

CREATE TABLE `news-main` (  `newsId` int(10) NOT NULL,  `newsTitle` varchar(255) NOT NULL,  `newsTime` varchar(255) DEFAULT NULL,  `newsSite` varchar(255) NOT NULL,  PRIMARY KEY (`newsId`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;

news-content:

CREATE TABLE `news-content` (  `newsId` int(10) NOT NULL AUTO_INCREMENT,  `newsContent` text NOT NULL,  `newsSite` varchar(255) NOT NULL,  `newsLink` varchar(255) DEFAULT NULL,  PRIMARY KEY (`newsId`)) ENGINE=InnoDB AUTO_INCREMENT=7395 DEFAULT CHARSET=utf8;

news-file:

CREATE TABLE `news-file` (  `newsId` int(10) NOT NULL,  `fileName` varchar(255) NOT NULL,  `fileId` varchar(255) NOT NULL,  `newsSite` varchar(255) NOT NULL,  PRIMARY KEY (`fileId`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;

以此来设计存储数据到数据库中的代码
不过首先,我们得先写好连接Mysql数据库的配置
config.js

// MySQL数据库连接配置var mysql = require('mysql');exports.db = mysql.createConnection({  host:            '127.0.0.1',   // 数据库地址  port:            3306,          // 数据库端口  database:        'news',   // 数据库名称  user:            'ylx',        // 数据库用户  password:        'ylx'             // 数据库用户对应的密码});

然后是存储数据到数据库的代码
Save.js

var async = require('async');var db = require('../config').db;var debug = require('debug')('blog:update:save');/** * 保存公告消息列表 */exports.newsList = function (site,list, callback) {  async.eachSeries(list, function (item, next) {    // 查询公告是否已存在    db.query('SELECT * FROM `news-main` WHERE `newsId`=? AND `newsSite`=? LIMIT 1', [item.id,site], function (err, data) {      if (err) return next(err);      if (Array.isArray(data) && data.length >= 1) {        // 公告已存在,更新一下        db.query('UPDATE `news-main` SET `newsTitle`=?, `newsTime`=? WHERE `newsId`=?', [item.title, item.time, item.id], next);      } else {        // 公告不存在,添加        db.query('INSERT INTO `news-main`(`newsId`, `newsTitle`, `newsTime`,`newsSite`) VALUES (?, ?, ?, ?)', [item.id, item.title, item.time,site], next);      }    });  }, callback);};/** * 保存公告内容 */exports.article = function (site,id,list, callback) {  async.eachSeries(list,function (newitem,next) {    // 查询公告是否已存在    db.query('SELECT * FROM `news-content` WHERE `newsId`=? AND `newsSite`=? LIMIT 1',      [newitem.id,site], function (err, data) {      if (err) return next(err);      if (Array.isArray(data) && data.length >= 1) {        // 公告已存在,更新一下        db.query('UPDATE `news-content` SET `newsContent`=?,`newsLink`=? WHERE `newsId`=?',          [newitem.content,newitem.link,newitem.id], next);      } else {        // 公告不存在,添加        db.query('INSERT INTO `news-content`(`newsId`, `newsContent`,`newsSite`,`newsLink`) VALUES (?, ?, ?, ?)',          [newitem.id,newitem.content,site,newitem.link], next);      }    });  }, callback);};/** * 保存公告附件信息 */exports.file = function (site,id,list, callback) {    async.eachSeries(list, function (item,next) {        // 查询附件是否已存在        db.query('SELECT * FROM `news-file` WHERE `fileId`=? AND `newsSite`=?',            [item.fileid,site], function (err, data) {                if (err) return next(err);                if (Array.isArray(data) && data.length >= 1) {                    // 附件已存在,更新一下                    db.query('UPDATE `news-file` SET `fileName`=?,`newsId`=? WHERE `fileId`=?',                        [item.fileName,item.id,item.fileid], next);                } else {                    // 附件不存在,添加                    db.query('INSERT INTO `news-file`(`newsId`, `fileName`,`fileId`,`newsSite`) VALUES (?, ?, ?, ?)',                        [item.id,item.fileName,item.fileid,site], next);                }            });    }, callback);};

然后需要注意的是,爬取网页的这个动作和存储数据到数据库这个动作都是异步请求的

所以我们还需要一个管理这些异步操作的模块,这个模块是很有名的async模块

all.js

var async = require('async');var read = require('./read');var save = require('./save');var site = 'jwc';var newsList;var articleList = {};var fileList = {};async.series([  // 获取公告消息列表  function (done) {    read.newsList(function (err, list) {      newsList = list;      done(err);    });  },  // 保存公告消息列表  function (done) {    save.newsList(site,newsList, done)  },  // 依次获取所有公告消息的内容  function (done) {    async.eachSeries(newsList, function (c, next) {      read.articleList(c.id, function (err, articlelist,filelist) {        articleList[c.id] = articlelist;        fileList[c.id] = filelist;        next(err);      });    }, done);  },  // 保存公告内容    function (done) {        async.eachSeries(Object.keys(articleList), function (articleListId, next) {            save.article(site,articleListId, articleList[articleListId], next);        }, done);    },  //保存附件内容    function (done) {        async.eachSeries(Object.keys(fileList), function (fileListId, next) {            save.file(site,fileListId, fileList[fileListId], next);        }, done);    }], function (err) {  if (err) console.error(err.stack);  console.log('教务处完成');});

通过async.series()函数,将众多异步操作合并为1个异步操作,并让它们按照顺序串行执行~

关于async的模块,介绍在这里:Async详解

然后直接执行all.js操作,就能够自动完成采取教务处网页公告并存储到数据库的操作了~

然后现在需要做的就是根据前端返回来的参数来查询相应的表并返回json数据

所以我们要先建立一个服务器,我们用express来快速的建立

搭建好之后,我们来写查询数据库的代码,其实和save.js的代码很像

data.js

var db = require('./update/config').db;/** * 获取公告列表 */exports.newsList = function (site,callback) {    db.query('SELECT * FROM `news-main` WHERE `newsSite`=? ORDER BY `newsId` DESC',[site], callback);};/** * 获取公告标题 */exports.newsTitle = function (site,id,callback) {    db.query('SELECT * FROM `news-main` WHERE `newsId`=? AND `newsSite`=? LIMIT 1',[id,site], callback);};/** * 获取公告内容 */exports.newsContent = function (site,id,callback) {    db.query('SELECT * FROM `news-content` WHERE `newsId`=? AND `newsSite`=? LIMIT 1',[id,site], callback);};/** * 获取公告附件 */exports.newsFile = function (site,id,callback) {    db.query('SELECT * FROM `news-file` WHERE `newsId`=? AND `newsSite`=?',[id,site], callback);};

很直观,也很简单,就是根据传入的id和site查询相应的数据

接下来,我们来搭建路由,并让它符合RESTFUL的架构

router.js

/** * Created by seekhow on 2017/8/24. */var async = require('async');var data = require('./data');module.exports = function (app) {    app.get('/', function (req, res) {        res.sendfile('./public/index.html');    });    app.get('/api/:site/all', function (req, res) {        var site=req.params.site;        data.newsList(site,function (err, list) {            //console.log(list);            res.send(list);        });    });    app.get('/api/:site/:id', function (req, res) {        var site=req.params.site;        var newsID = req.params.id;        var news=[];        async.series([            function (done) {                data.newsTitle(site,newsID, function (err, title) {                    news.push(title);                    done(err);                })            },            function (done) {                data.newsContent(site,newsID, function (err, content) {                    news.push(content);                    done(err);                })            },            function (done) {                data.newsFile(site,newsID, function (err, File) {                    news.push(File);                    done(err);                })            }        ], function (err) {            if (err) console.error(err.stack);            res.send(news);        });    });    app.get('/api/test', function (req, res) {        res.send('test');    });};

每一个site就是一类资源,因为我们只用显示,也不用对数据进行修改等等,所以只会用到get方法

通过:site来获取传过来的网站参数,来判断是属于教务处的还是学工部还是其他网站的

同时在返回具体一个公告的内容的时候,因为要同时查询3个数据,所以又用到了async模块来控制异步请求

好的,到此后端API服务器算是完全搭建完毕了,我们可以直接通过url就能得到数据了

让我们来试一试

启动服务器

输入:http://localhost:3000/api/jwc/all

成功返回数据

我们再来试试具体的一个公告

输入:http://localhost:3000/api/jwc/4051

成功返回数据~

因为要定时,所以我们用到了node_schedule模块再加上node的一个核心模块child_process模块来配合使用,以便能够达到定时爬取数据的效果

schedule.scheduleJob('30 * * * * *', function(){    var update = childProcess.spawn(process.execPath, [path.resolve(__dirname, 'update/jwc/all.js')]);    update.stdout.pipe(process.stdout);   update.stderr.pipe(process.stderr);});

OK,我们简陋的,精简的API服务器算是搭建完毕了~


接下来开始前端的代码编写

用的是Angular.js+Bootstrap,不过我现在有点想用Angular2重构……

首先下载好Angualr.js和Bootstrap,以及后面要用到的前端路由Angular-ui-router,并加到项目依赖项中

index.html

<!DOCTYPE html><html lang="en" ng-app="information"><head>    <meta charset="UTF-8">    <title>信息综合</title>    <link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css">    <link rel="stylesheet" href="/public/stylesheets/style.css">    <script type="text/javascript" src="/node_modules/jquery/dist/jquery.min.js"></script>    <script type="text/javascript" src="/node_modules/bootstrap/dist/js/bootstrap.min.js"></script>    <script type="text/javascript" src="/node_modules/angular/angular.js"></script>    <script type="text/javascript" src="/node_modules/angular-ui-router/release/angular-ui-router.min.js"></script></head><body><div class="container">    <div class="row clearfix">        <div class="col-md-12 column">            <div class="page-header">                <h1>                    <small>信息综合</small>                </h1>            </div>            <ul class="nav nav-tabs">                <li >                    <a ui-sref="/jwc">教务处</a>                </li>                <li >                    <a ui-sref="/xgb">学工部</a>                </li>                <li >                    <a ui-sref="/cs">计算机学院</a>                </li>                <li class="dropdown pull-right">                    <a href="#" data-toggle="dropdown" class="dropdown-toggle">下拉<strong class="caret"></strong></a>                    <ul class="dropdown-menu">                        <li>                            <a href="#">操作</a>                        </li>                        <li>                            <a href="#">设置栏目</a>                        </li>                        <li>                            <a href="#">更多设置</a>                        </li>                        <li class="divider">                        </li>                        <li>                            <a href="#">分割线</a>                        </li>                    </ul>                </li>            </ul>        </div>    </div>    <hr>    <div class="row clearfix" ui-view="">    </div></div><script type="text/javascript" src="/public/index.js"></script></body></html>

特别需要注意的是,ui-viewui-sref这两个属性,它们是Angular-ui-router前端路由工作用的

ui-view属性所在的那个标签就相当于一个容器,会根据Angular的工作来填充里面的内容,ui-sref相当于是a标签的变型

具体的Angular-ui-router学习资料:AngularJS ui-router

然后是main.html,用来显示公告列表,就是一个表格

<div class="col-md-12 column "   >        <table class="table table-hover">            <thead>            <tr>                <th>标号</th>                <th>标题</th>                <th>时间</th>            </tr>            </thead>            <tbody>            <tr ng-repeat="x in newslist">                <td>{{x.newsId}}</td>                <td ui-sref="{{x.newsSite}}/content({id:{{x.newsId}}})">{{x.newsTitle}}</td>                <td>{{x.newsTime}}</td>            </tr>            </tbody>        </table></div><div class="col-md-8" ui-view="" ></div>

接着是content.html,用来显示具体的一个公告的内容

<div class="jumbotron" >    <h1>       {{title}}    </h1>    <p ng-bind-html="content">    </p>    <p>原文链接<a target="_blank" ng-href="{{link}}">{{link}}</a></p>    <hr>    <div>        <a class="file" ng-repeat="x in file" ng-href="http://jwc.scu.edu.cn/jwc/download.action?fileName={{x.fileId}}">            {{x.fileName}} <br>        </a>    </div></div>

然后是index.js,用来控制整个前端页面

/** * Created by seekhow on 2017/7/30. */var webapp=angular.module('information', ['ui.router']);webapp.config(function ($stateProvider, $urlRouterProvider) {    $urlRouterProvider.when("", "/jwc");    $stateProvider        .state("/jwc", {            url: "/jwc",            templateUrl: "public/pages/main.html",            controller: function ($scope, $http) {                $scope.newslist = [];                $http.get('/api/jwc/all').then(                    function (res) {                        //console.log(res.data);                        $scope.newslist = res.data;                    }, function (res) {                        console.log('error');                    });            }        })        .state("jwc/content", {            url: "/content?id",            templateUrl: "public/pages/content.html",            controller:function ($scope,$http,$stateParams,$sce) {                var newsid = $stateParams.id;                //$http.get('')                $http.get('/api/jwc/'+newsid).then(                    function (res) {                        $scope.title =res.data[0][0].newsTitle;                        $scope.content = $sce.trustAsHtml(res.data[1][0].newsContent);                        $scope.link = res.data[1][0].newsLink;                        $scope.file = res.data[2];                    },function (res) {                        console.log('error');                    }                );            }        })        .state("/xgb", {            url: "/xgb",            templateUrl: "public/pages/main.html",            controller: function ($scope, $http) {                $scope.newslist = [];                $http.get('/api/xgb/show').then(                    function (res) {                        //console.log(res.data);                        $scope.newslist = res.data;                    }, function (res) {                        console.log('error');                    });            }        })        .state("xgb/content", {            url: "/content?id",            templateUrl: "public/pages/content.html",            controller:function ($scope,$http,$stateParams,$sce) {                var newsid = $stateParams.id;                //$http.get('')                $http.get('/api/xgb/'+newsid).then(                    function (res) {                        $scope.title =res.data[0][0].newsTitle;                        $scope.content = $sce.trustAsHtml(res.data[1][0].newsContent);                        $scope.link = res.data[1][0].newsLink;                        $scope.file = res.data[2];                    },function (res) {                        console.log('error');                    }                );            }        })});

可以看的出来,整个最大的结构是web.config(),传入内置的stateProvider,urlRouterProvider2个对象,用来配置路由

路由的配置是根据.state来配置的,前端路由拦截到相应的路由之后,会执行controller里面的内容,而controller里面的内容就是一个被Angular包装好的Ajax请求,向API服务器发送请求,然后在绑定在scope对象上,显示在html模版上

然后到此,程序算是完成了


总结:算是一个架构很清晰的应用,实用性还是很强的,有时间会继续完善,增加其他网站的,另外打算用Angular2重构前端了,写的比较匆忙,很多细节没有细说=。=

阅读全文
0 0
原创粉丝点击