这一节我们只做两件事,第一是建立相应的爬虫系统,从网页链接上提取合适的信息,第二则是将这些信息储存在数据库中,render需要展示时再查询予以显示。开始构建代码前我们先思考一下这样做的好处是什么。

介绍

在news-Feed应用中,我们把爬虫逻辑放在客户应用里而非服务端,这是正确的,考虑到用户增加的情况下我们无法负担所有的爬虫任务,如果我们将这些任务进行合理的分配是最优的,利用一些客户端资源。在生产环境里还可以考虑用户每次爬取完毕后发送处理好的字符串发送回服务端进行存储,甚至可以根据服务器返回不同得资源来考虑返回给用户不同的任务。虽然在news-Feed中我们不会做这些事,但我们不妨考虑这样的系统是如何工作的:

  1. 应用内部储存一张映射表,可更新,作为当前应用的基础爬虫任务。

  2. 根据用户下载应用IP不同分发不同的应用包,基础数据库的标识有一些区别。

  3. 根据用户请求的标识+IP地址返回给用户不同的爬虫任务。

  4. 短时间的工作后将数据返回给服务端。

  5. 用户每次查看的新闻一部分是自己客户端爬取的,另一部分则从服务器下载。

这样的系统很有意思,积累众多格式化数据资源后甚至可以转为开发的新闻API供大家使用,不过它很复杂(你可以自己尝试一下),目前我们希望应用的所有数据都能够自行完成,为此我们至少需要一个数据库存储格式化数据,一段可配置的代码爬取与分析数据。在做所有事情之前,我准备加入一个新的语法糖,以适应爬虫任务。

配置Async

async是ES7的新语法,简单的说,async是一个基于Generator的语法糖。如果你对Generator还不了解,建议先学习一些ES6基础知识。爬虫任务可能涉及到很多的异步任务,但大多数时候我们更希望它们可以同步执行(并发过大很容易被网站屏蔽IP地址),async函数可以帮助我们轻松的用同步函数的方式写异步逻辑,而且它足够简单,学习它也是理所应当的,这是javascript的趋势之一。

首先我们需要安装一些必要的npm包:

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编译代码,只要你开心。

安装数据库

作为一个桌面应用,数据存储是必不可少的一环,但这里并没有使用已携带的浏览器存储:

  1. 浏览器的各类存储总是有限的。

  2. 它们很难存储复杂结构的数据,你需要为此做很多转换。

  3. 最大的局限在于不能够随意的释放窗口对象,这会带来很多的存储丢失问题,这对未来的扩展必然有影响。

除此之外我们还可以选用一些流行的云储存,远程数据库等等,但我希望应用能够在脱机时正常工作,为此我们需要一个安装简单,在本地即时编译的轻量级数据库。

这里我选用的流行的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({},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函数,它做了以下几件事:

  1. try代码块,捕获await可能抛出的错误。

  2. 利用继承的request静态方法获得基础的列表文件,使用parseLink解析html获得一个链接数组。cheerio是一个类似于JQuery的库,可以帮助我们解析这些html文件。

  3. 循环体内,分别请求文章主体,利用parseContent分析文章并集合成对象,如果对象获取成功,接下来还会为这篇文章对象合并一个序列号,便于后面的查询/分类。

  4. 每次循环都插入一次数据库。这样做在于单次插入数据较多失败时,neDB会使所有的数据回滚。当然这其中的量级你可以自己把握。在更大的应用里你可以抽象出一层类似于ORM的服务,专职于有效快速的存储查询,甚至是提供一些语法糖。

这里的global.Storage.count是一个权宜之计,在未来完全前端代码后再回过头来解决它,目前我们只需要在根目录的index.js里加入require('./browser/task/index').ifeng.start()即可使它工作起来:

OK,这一节的所有目标都已完成,下一节我们开始讨论如何在Angular中构建一个合理的展示模块并与数据库通信。

使用Angular与TypeScript构建Electron应用(四)的更多相关文章

  1. 详解前端HTML5几种存储方式的总结

    本篇文章主要介绍了前端HTML5几种存储方式的总结 ,主要包括本地存储localstorage,本地存储sessionstorage,离线缓存(application cache),Web SQL,IndexedDB。有兴趣的可以了解一下。

  2. PhoneGap / iOS上的SQLite数据库 – 超过5mb可能

    我误解了什么吗?Phonegap中的sqlitedbs真的有5mb的限制吗?我正在使用Phonegap1.2和iOS5.解决方法您可以使用带有phonegap插件的原生sqliteDB,您将没有任何限制.在iOS5.1中,Websql被认为是可以随时删除的临时数据…

  3. ios – 领域:如何获取数据库的当前大小

    是否有RealmAPI方法使用RealmSwift作为数据存储来获取我的RealmSwift应用程序的当前数据库大小?

  4. ios – Realm – 无法使用现有主键值创建对象

    我有一个对象有许多狗的人.应用程序有单独的页面,它只显示狗和其他页面显示人的狗我的模型如下我有人存储在Realm中.人有详细页面,我们取,并显示他的狗.如果狗已经存在,我会更新该狗的最新信息并将其添加到人的狗列表中,否则创建新狗,保存并将其添加到人员列表中.这适用于coredata.在尝试用他的狗更新人时,领域会抛出异常无法使用现有主键值创建对象解决方法这里的问题是,即使你正在创建一个全新的Rea

  5. ios – UIWebView中的WebSQL / SQLite数据库的最大大小(phonegap)

    我知道一般来说,Web应用程序的本地存储空间有5MB的限制.本地网页浏览应用程式是否也有这个限制?

  6. ios – Firebase离线存储高级 – 手动同步和进度信息

    >我可以提供一个捆绑数据库–安装App后我可以已经离线查询了Firebase数据?然后我有另一个关于Firebase的主要问题:>JSON存储是伟大的–但是这样我们不关心一个独特的结构,我们必须注意这一点插入总是正确的数据集?我从来没有试图显示实际的进展,但是当您从firebase中检索数据时,始终会在成功检索数据时调用onDataChange方法.https://firebase.google.com/docs/database/android/retrieve-data#read_data_onceC

  7. ios – 如何处理多用户数据库

    我的应用程序就像很多应用程序–它有一个用户输入用户名和密码的登录屏幕,以及登录按钮我的应用程序还使用CoreData来保存大多数用户的业务对象,当然也是用户特定的.我也有一个登出按钮来启用切换用户.这不会发生很多,但仍然是必要的).现在如果不同的用户登录,我需要获取他的具体数据.但是我该如何做呢?

  8. ios – Swift从Firebase数据库中获取特定价值

    我正在尝试从Firebase数据库中获取特定值.我看了一些像谷歌这样的文件,但我做不到.这是数据库的JSON文件:SWIFT代码:我想获得用户的电子邮件价值,而不是每个人.我怎样才能做到这一点?解决方法在您的代码中,快照将包含子值的字典.要访问它们,请将snapshot.value转换为Dictionary,然后访问各个子项是一个快照

  9. ios – Realm Swift:在卸载应用程序后是否可以保留数据库?

    使用realmswift,即使从设备上卸载应用程序,是否可以在设备内存中保留和维护应用程序的领域数据库文件?非常感谢您的帮助.解决方法删除应用程序时,应用程序的所有文件都是剩余的.iOS应用程序是沙盒.这意味着每个应用程序在磁盘中都有自己的空间,并有自己的目录,这些目录充当应用程序及其数据的主页.从iPhone删除应用程序会删除此沙箱,删除与该应用程序关联的所有数据.

  10. ios – 在没有XML的情况下更新sqlite数据库

    我的应用程序需要来自sqlite数据库的数据.它将附带此数据库的一个版本,但我需要定期更新它(很可能每月一次).通常情况下,我一直在通过我设置的一堆网络服务将我的应用程序的其他部分的更新作为XML发送,但我现在正在处理的这个特定数据库非常大(大约20-30MB),而且我当我尝试以这种方式发送时出现超时错误.我尝试将数据库放在我的公司服务器上,然后将其下载到NSData对象中.然后我将该数据对象保存

随机推荐

  1. Angular2 innerHtml删除样式

    我正在使用innerHtml并在我的cms中设置html,响应似乎没问题,如果我这样打印:{{poi.content}}它给了我正确的内容:``但是当我使用[innerHtml]=“poi.content”时,它会给我这个html:当我使用[innerHtml]时,有谁知道为什么它会剥离我的样式Angular2清理动态添加的HTML,样式,……

  2. 为Angular根组件/模块指定@Input()参数

    我有3个根组件,由根AppModule引导.你如何为其中一个组件指定@input()参数?也不由AppModalComponent获取:它是未定义的.据我所知,你不能将@input()传递给bootstraped组件.但您可以使用其他方法来做到这一点–将值作为属性传递.index.html:app.component.ts:

  3. angular-ui-bootstrap – 如何为angular ui-bootstrap tabs指令指定href参数

    我正在使用角度ui-bootstrap库,但我不知道如何为每个选项卡指定自定义href.在角度ui-bootstrap文档中,指定了一个可选参数select(),但我不知道如何使用它来自定义每个选项卡的链接另一种重新定义问题的方法是如何使用带有角度ui-bootstrap选项卡的路由我希望现在还不算太晚,但我今天遇到了同样的问题.你可以通过以下方式实现:1)在控制器中定义选项卡href:2)声明一个函数来改变控制器中的散列:3)使用以下标记:我不确定这是否是最好的方法,我很乐意听取别人的意见.

  4. 离子框架 – 标签内部的ng-click不起作用

    >为什么标签标签内的按钮不起作用?>但是标签外的按钮(登陆)工作正常,为什么?>请帮我解决这个问题.我需要在点击时做出回复按钮workingdemo解决方案就是不要为物品使用标签.而只是使用divHTML

  5. Angular 2:将值传递给路由数据解析

    我正在尝试编写一个DataResolver服务,允许Angular2路由器在初始化组件之前预加载数据.解析器需要调用不同的API端点来获取适合于正在加载的路由的数据.我正在构建一个通用解析器,而不是为我的许多组件中的每个组件设置一个解析器.因此,我想在路由定义中传递指向正确端点的自定义输入.例如,考虑以下路线:app.routes.ts在第一个实例中,解析器需要调用/path/to/resourc

  6. angularjs – 解释ngModel管道,解析器,格式化程序,viewChangeListeners和$watchers的顺序

    换句话说:如果在模型更新之前触发了“ng-change”,我可以理解,但是我很难理解在更新模型之后以及在完成填充更改之前触发函数绑定属性.如果您读到这里:祝贺并感谢您的耐心等待!

  7. 角度5模板形式检测形式有效性状态的变化

    为了拥有一个可以监听其包含的表单的有效性状态的变化的组件并执行某些组件的方法,是reactiveforms的方法吗?

  8. Angular 2 CSV文件下载

    我在springboot应用程序中有我的后端,从那里我返回一个.csv文件WheniamhittingtheURLinbrowsercsvfileisgettingdownloaded.现在我试图从我的角度2应用程序中点击此URL,代码是这样的:零件:服务:我正在下载文件,但它像ActuallyitshouldbeBook.csv请指导我缺少的东西.有一种解决方法,但您需要创建一个页面上的元

  9. angularjs – Angular UI-Grid:过滤后如何获取总项数

    提前致谢:)你应该避免使用jQuery并与API进行交互.首先需要在网格创建事件中保存对API的引用.您应该已经知道总行数.您可以使用以下命令获取可见/已过滤行数:要么您可以使用以下命令获取所选行的数量:

  10. angularjs – 迁移gulp进程以包含typescript

    或者我应该使用tsc作为我的主要构建工具,让它解决依赖关系,创建映射文件并制作捆绑包?

返回
顶部