这一节咱们只作两件事,第一是创建相应的爬虫系统,从网页连接上提取合适的信息,第二则是将这些信息储存在数据库中,render须要展现时再查询予以显示。开始构建代码前咱们先思考一下这样作的好处是什么。javascript
在news-feed应用中,咱们把爬虫逻辑放在客户应用里而非服务端,这是正确的,考虑到用户增长的状况下咱们没法负担全部的爬虫任务,若是咱们将这些任务进行合理的分配是最优的,利用一些客户端资源。在生产环境里还能够考虑用户每次爬取完毕后发送处理好的字符串发送回服务端进行存储,甚至能够根据服务器返回不一样得资源来考虑返回给用户不一样的任务。虽然在news-feed中咱们不会作这些事,但咱们不妨考虑这样的系统是如何工做的:html
应用内部储存一张映射表,可更新,做为当前应用的基础爬虫任务。前端
根据用户下载应用IP不一样分发不一样的应用包,基础数据库的标识有一些区别。java
根据用户请求的标识+IP地址返回给用户不一样的爬虫任务。node
短期的工做后将数据返回给服务端。git
用户每次查看的新闻一部分是本身客户端爬取的,另外一部分则从服务器下载。github
这样的系统颇有意思,积累众多格式化数据资源后甚至能够转为开发的新闻API供你们使用,不过它很复杂(你能够本身尝试一下),目前咱们但愿应用的全部数据都可以自行完成,为此咱们至少须要一个数据库存储格式化数据,一段可配置的代码爬取与分析数据。在作全部事情以前,我准备加入一个新的语法糖,以适应爬虫任务。数据库
async是ES7的新语法,简单的说,async是一个基于Generator的语法糖。若是你对Generator还不了解,建议先学习一些ES6基础知识。爬虫任务可能涉及到不少的异步任务,但大多数时候咱们更但愿它们能够同步执行(并发过大很容易被网站屏蔽IP地址),async函数能够帮助咱们轻松的用同步函数的方式写异步逻辑,并且它足够简单,学习它也是理所应当的,这是javascript的趋势之一。npm
首先咱们须要安装一些必要的npm包:json
npm i --save transform-async-to-generator syntax-async-functions transform-regenerator npm i --save babel-core babel-polyfill babel-preset-es2016
这里我但愿代码不要通过频繁的转码,应用能够不考虑兼容性,因此我加入一些垫片使语法糖可以正常工做便可。
在根文件夹下创建一个.babelrc
文件:
{ "presets": ["es2016"], "plugins": ["transform-async-to-generator", "syntax-async-functions", "transform-regenerator"] }
并在根文件夹创建一个main.js
,集合这些文件:
require('babel-core/register'); require("babel-polyfill"); require("./index");
从如今开始咱们每次只需运行electron main.js
就可以轻松的启动富含ES7语法糖的应用。固然,你能够引入任何语法,甚至是Gulp/Webpack编译代码,只要你开心。
做为一个桌面应用,数据存储是必不可少的一环,但这里并无使用已携带的浏览器存储:
浏览器的各种存储老是有限的。
它们很难存储复杂结构的数据,你须要为此作不少转换。
最大的局限在于不可以随意的释放窗口对象,这会带来不少的存储丢失问题,这对将来的扩展必然有影响。
除此以外咱们还能够选用一些流行的云储存,远程数据库等等,但我但愿应用可以在脱机时正常工做,为此咱们须要一个安装简单,在本地即时编译的轻量级数据库。
这里我选用的流行的nedb,它的社区环境足够好,有不少的使用者(保证库可以及时更新并解决各种问题),并且与electron可以很好的结合。
安装nedb:
npm i --save nedb
在根目录的index.js
中启动数据库:
const Datastore = require('nedb') global.Storage = new Datastore({filename: `${__dirname}/.database/news-feed.db`, autoload: true })
nedb有多种储存方式,包括内存。这里的
autoload
表明每次更新时都会更新数据库的本地文件,将数据写入硬盘。你也能够选择每次使用loadDatabase
来手动触发写入硬盘的动做。
在动手以前咱们先尝试分析爬虫代码的逻辑:这里至少须要一个实际工做的爬虫函数,它从http请求获得数据而且开始分析html,最后存储这些数据。不一样的网站结构不一样意味着须要不一样的解析函数,但其中至少能够将基础的http服务抽离出来(它们老是相同的),将来咱们能够从服务端获取一些解析代码填充在这里。
手动发起http请求与处理字符串工做量很是大,咱们能够借助一下库来完成这些工做:
* https://github.com/request/request npm i --save request * https://github.com/cheeriojs/cheerio npm i --save cheerio
1.新建http请求函数
在/browser/task
下新建base.js
:
const req = require('request') module.exports = class Base { constructor (){ } static makeOptions (url){ return { url: url, port: 8080, method: 'GET', headers: { 'User-Agent': 'nodejs', 'Content-Type': 'application/json' } } } static request (url){ return new Promise((resolve, reject) =>{ req(Base.makeOptions(url), (err, response, body) =>{ if (err) return reject(err) resolve(body) }) }) } }
Base类有两个静态方法,makeOptions
负责根据url生成一个option对象,为每次请求设置配置项使用,当将来须要验证token/cookie时咱们再来扩充此方法,request
返回一个Promise对象,显然它会发起一个请求,但更多的做用是在使用时优先返回body而非response。这很重要。
也许你开始注意到,这两个静态函数彻底不依赖this
,它们仅仅是类的静态方法,无需实例化便可使用,同时也可以被继承。这样的目的在于暗示这些函数是彻底不依赖状态的纯函数,它们老是返回相同的结果,也没有反作用,这样的函数在将来可以被更好的阅读与扩展。
2.新建爬虫文件
假定这个文件只负责单个网站(例如ifeng.com)的功能,固然之后这样的文件会愈来愈多,如今先为这些功能文件建立一个集合文件负责导出:
// /browser/task/index.js module.exports = { ifeng: require('./ifeng') }
在task文件夹下再建立一个ifeng.js
:
const cheerio = require('cheerio') const Base = require('./base') module.exports = new class Self extends Base { constructor (){ super() this.url = 'http://news.ifeng.com/xijinping/' } start (){ global.Storage.count({}, (err, c) =>{ if (c || c > 0) return ; this.request() .then(res =>{ console.log('所有储存完毕!'); global.Storage.loadDatabase() }) .catch(err =>{ console.log(err); }) }) } async request (){ try{ const body = await Self.request(this.url) let links = await this.parseLink(body) for (let index = 1; index< links.length; index++){ const content = await Self.request(links[index -1]) const article = await this.parseContent(content) await this.saveContent(Object.assign({id: index}, article)) console.log(`第${index}篇文章:${article&&article.title}储存完毕`); } } catch (err){ return Promise.reject(err) } } parseLink (html){ const $ = cheerio.load(html) return $('.con_lis > a') .map((i, el) => $(el) .attr('href')) } parseContent (html){ if (!html) return; const $ = cheerio.load(html) const title = $('title').text() const content = $('.yc_con_txt').html() return {title: title, content: content} } saveContent (article){ if (!article|| !article.title) return ; return global.Storage.insert(article) } }()
ifeng.js
的主体是request函数,它作了如下几件事:
try
代码块,捕获await可能抛出的错误。
利用继承的request静态方法得到基础的列表文件,使用parseLink
解析html得到一个连接数组。cheerio
是一个相似于JQuery的库,能够帮助咱们解析这些html文件。
循环体内,分别请求文章主体,利用parseContent
分析文章并集合成对象,若是对象获取成功,接下来还会为这篇文章对象合并一个序列号,便于后面的查询/分类。
每次循环都插入一次数据库。这样作在于单次插入数据较多失败时,neDB会使全部的数据回滚。固然这其中的量级你能够本身把握。在更大的应用里你能够抽象出一层相似于ORM的服务,专职于有效快速的存储查询,甚至是提供一些语法糖。
这里的global.Storage.count
是一个权宜之计,在将来彻底前端代码后再回过头来解决它,目前咱们只须要在根目录的index.js
里加入require('./browser/task/index').ifeng.start()
便可使它工做起来:
OK,这一节的全部目标都已完成,下一节咱们开始讨论如何在Angular中构建一个合理的展现模块并与数据库通讯。