這一節咱們只作兩件事,第一是創建相應的爬蟲系統,從網頁連接上提取合適的信息,第二則是將這些信息儲存在數據庫中,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中構建一個合理的展現模塊並與數據庫通訊。