目的
由于經(jīng)常會(huì)閱讀關(guān)于C++的博客迷帜,譬如Meeting C++!的blogroll迹鹅、 reddit的r/cpp等担孔,面對(duì)很多信息來(lái)源江锨,每天去對(duì)應(yīng)站點(diǎn)讀新博客是比較麻煩的事情,試過(guò)一些RSS閱讀器糕篇,不太會(huì)用......那就自己動(dòng)手制作一個(gè)吧啄育。
思路
- 獲取博客主頁(yè)內(nèi)容
- 取出文章標(biāo)題及URL
- 移除舊文章
- 生成新文章列表
- 提示用戶閱讀
開(kāi)發(fā)環(huán)境準(zhǔn)備
起步
我們將建立一個(gè)命令行程序,在對(duì)應(yīng)路徑建立myreader
目錄娩缰,進(jìn)入目錄輸入:
npm init
根據(jù)提示信息輸入對(duì)應(yīng)內(nèi)容灸撰,生成package.json
結(jié)果如下:
{
"name": "myreader",
"version": "0.0.1",
"description": "simple RSS-like reader",
"main": "./bin/AppEntry.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"blog",
"rss",
"reader"
],
"author": "liff.engineer@gmail.com",
"license": "ISC"
}
在這里我將應(yīng)用程序入口指定為./bin/AppEntry.js
,測(cè)試內(nèi)容如下:
#!/usr/bin/env node
console.log("我的RSS閱讀器");
注意在Node.js的命令行程序拼坎,可執(zhí)行程序第一行要這樣寫(xiě)才能正確執(zhí)行浮毯。
然后在myreader
目錄執(zhí)行:
npm link
命令相當(dāng)于注冊(cè)了myreader
,之后就可以使用myreader
來(lái)執(zhí)行AppEntry.js
了泰鸡。
獲取博客主頁(yè)內(nèi)容
使用request來(lái)獲取博客主頁(yè)內(nèi)容债蓝,測(cè)試的博客為Fluent{C++}:
安裝request
庫(kù)
npm install --save request
修改AppEntry.js
#!/usr/bin/env node
var request = require('request');
var domain = "http://www.fluentcpp.com/";
request(domain,function(error,response,body){
if(error){
console.log(error);
}
if(response.statusCode != 200){
console.log(response);
}
console.log(body);
});
運(yùn)行myreader
:
myreader > index.html
即可得到博客主頁(yè)的內(nèi)容。
取出文章的標(biāo)題和URL
規(guī)律分析
打開(kāi)Fluent{C++}盛龄,使用Chrome的檢查打開(kāi)分析界面:
可以看到所有的文章都是用<article>...</article>
包裹起來(lái)的饰迹,而文章的標(biāo)題和URL存放在<a href="URL">標(biāo)題</a>
中,上一級(jí)為<h2 class="entry-title">...</h2>
余舶。
那么啊鸭,我們可以取出所有的article
,然后查找內(nèi)容的class
為entry-title
的元素匿值,然后再取出其中的a
拿到標(biāo)題和href
屬性赠制。
如何實(shí)現(xiàn)
使用cheerio來(lái)對(duì)網(wǎng)頁(yè)進(jìn)行處理,cheerio
可以采用jQuery的方式對(duì)HTML
進(jìn)行操作:
npm install --save cheerio
#!/usr/bin/env node
var request = require('request');
var cheerio = require('cheerio');
var domain = "http://www.fluentcpp.com/";
request(domain,function(error,response,body){
if(error){
console.log(error);
}
if(response.statusCode != 200){
console.log(response);
}
var $ = cheerio.load(body);
$('article').each(function(i,e){
var entry = $(e).find('.entry-title').find('a');
var article = {
title:entry.text(),
url:entry.attr('href')
};
console.log(article);
});
});
運(yùn)行myreader
結(jié)果如下:
封裝成為Fetch.js
通過(guò)之前的方式,就可以獲取博客主頁(yè)上的文章信息了挟憔,將其封裝成為Fetch.js
钟些。
博客的內(nèi)容大致如此,都是獲取文章列表绊谭,然后定位到標(biāo)題政恍,即可取出想要的信息,那么對(duì)于特定的博客主頁(yè),輸入的信息為:
{
domain: 博客主頁(yè)
articleFilter: 文章過(guò)濾
urlFilter: 標(biāo)題過(guò)濾
}
Fetch.js
實(shí)現(xiàn)如下:
var request = require('request');
var cheerio = require('cheerio');
function fetchArticles(input,callback){
request(input.domain,function(error,response,body){
if(error){
console.log(error);
}
if(response.statusCode != 200){
console.log(response);
}
var articles = [];
var $ = cheerio.load(body);
var urlFilters = input.urlFilter.split(" ");
$(input.articleFilter).each(function(i,e){
var entry = $(e);
urlFilters.forEach(function(item,index,array){
if(item)
entry = entry.find(item);
});
entry = entry.find('a');
var article = {
title:entry.text(),
url:entry.attr('href')
};
articles.push(article);
});
callback(articles);
});
};
exports.fetchArticles = fetchArticles;
此時(shí)的AppEntry.js
為:
#!/usr/bin/env node
var fetch = require("./Fetch.js");
var input = {
domain:"http://www.fluentcpp.com/",
articleFilter:"article",
urlFilter:".entry-title"
};
fetch.fetchArticles(input,function(articles){
console.log(articles);
});
移除舊文章
最簡(jiǎn)單的實(shí)現(xiàn)就是記錄每次獲取到的博客站點(diǎn)最新的文章URL,當(dāng)獲取到文章列表數(shù)組時(shí)進(jìn)行遍歷,一旦碰到上一次的URL恩伺,則后續(xù)的文章都是舊文章鹤树。
簡(jiǎn)單起見(jiàn)铣焊,采用直接保存JSON對(duì)象的方式處理:
var jsonfile = require('jsonfile');
//讀取JSON文件
function loadJSONFile(file){
if(!fs.existsSync(file))
return {};
return jsonfile.readFileSync(file);
};
//保存JSON文件
function saveJSONFile(file,json){
jsonfile.writeFileSync(file,json,{spaces:4});
};
對(duì)于結(jié)果的操作實(shí)現(xiàn)如下:
const fs = require('fs');
const path = require('path');
function result(location){
return {
lastFile:path.join(location,"./last.json"),//最新的文章文件位置
lastArticles:function(){
return loadJSONFile(this.lastFile);
},
lastArticle:function(domain){//查詢某博客最新的文章
var vals = this.lastArticles();
if(!vals.hasOwnProperty(domain))
return "";
return vals[domain];
},
updateLastArticle:function(domain,url){//更新某博客最新的文章
var vals = this.lastArticles();
vals[domain] = url;
saveJSONFile(this.lastFile,vals);
return vals;
},
updateLastArticles:function(articles){//更新最新的文章
var vals = this.lastArticles();
articles.forEach(function(article,index,array){
vals[article.domain] = article.url;
});
saveJSONFile(this.lastFile,vals);
return vals;
}
}
};
移除舊文章操作實(shí)現(xiàn)如下:
//過(guò)濾掉舊文章
function removeOldArticles(last,articles){
var results = [];
articles.every(function(article,idx){
if(article.url === last)
return false;
results.push(article);
return true;
});
return results;
};
多博客站點(diǎn)設(shè)置
對(duì)于每個(gè)博客站點(diǎn),只需要保存相應(yīng)信息到配置罕伯,然后即可使用Fetch.js
訪問(wèn)并得到文章列表曲伊,設(shè)置操作實(shí)現(xiàn)如下:
//配置文件的操作
function setting(location){
return {
file:path.join(location,'./setting.json'),
inputs:function(){ //獲取博客源列表
var set = loadJSONFile(this.file);
if(set.hasOwnProperty('inputs'))
return set.inputs;
return {};
},
append:function(input){//追加新博客源
var result = loadJSONFile(this.file);
if(!result.hasOwnProperty('inputs'))
result.inputs = new Map();
result.inputs[input.domain] = input;
saveJSONFile(this.file,result);
return result.inputs;
}
}
};
整合操作
將設(shè)置、獲取追他、移除舊文章操作進(jìn)行整合后的AppEntry.js
:
#!/usr/bin/env node
const path = require('path');
var result = require('../src/Result.js').result(__dirname);
var setting = require('../src/Setting.js').setting(__dirname);
var fetch = require('../src/Fetch.js');
var action = require('../src/action.js');//removeOldArticles在此實(shí)現(xiàn)
console.log('配置文件位于:'+setting.file);
var inputs = setting.inputs();
var results = [];
Object.keys(inputs).forEach(function(domain){
fetch.fetchArticles(inputs[domain],function(articles){
//得到文章列表并移除舊文章
var vals = action.removeOldArticles(result.lastArticle(domain),articles);
results.push({domain,results:vals});
if(results.length === Object.keys(inputs).length){//當(dāng)所有博客源都遍歷完成
console.log(results);//輸出結(jié)果
}
});
});
通知用戶
當(dāng)獲取到所有的新文章之后坟募,需要告知用戶有新文章可讀及其入口,簡(jiǎn)單起見(jiàn)將新文章列表生成為HTML邑狸,然后彈出系統(tǒng)通知懈糯,當(dāng)用戶點(diǎn)擊時(shí)打開(kāi)該HTML頁(yè)面。
生成HTML
function writeAsHTMLFile(file,articles){
var content = '<div class="articles">\n';
articles.forEach(function(item,index,array){
item.results.forEach(function(article,idx,obj){
content+='<article>\n';
content+='<a href="'+article.url+'">'+article.title+'</a>\n';
content+='</article>\n';
});
});
content+='</div>\n';
fs.writeFileSync(file,content);
};
彈出通知
這里使用node-notifier來(lái)彈出系統(tǒng)通知:
const notifier = require('node-notifier');
const path = require('path');
function fireNotify(NotifyInput){
notifier.notify({
title:NotifyInput.title,
message:NotifyInput.message,
icon:path.join(__dirname,NotifyInput.icon),
sound:true,
wait:true
});
notifier.on('click',NotifyInput.onClick? NotifyInput.onClick:function(object,options){});
notifier.on('timeout',NotifyInput.onTimeout? NotifyInput.onTimeout:function(object,options){});
};
打開(kāi)HTML頁(yè)面
使用open:
var open = require('open');
function notifyNewArticles(number,location){
fireNotify({
title:"新文章可讀",
message:'有'+(number+1)+"篇新文章可讀,點(diǎn)擊查看.",
icon:'invalid.png',
onClick:function(object,option){
open("file:///"+location);
}
});
};
處理結(jié)果
當(dāng)獲取了每個(gè)博客站點(diǎn)的新文章列表后单雾,還需要處理一下結(jié)果赚哗,然后用來(lái)刷新"最新的文章URL",實(shí)現(xiàn)如下:
function mergeArticles(articles){
var last = [];
var number = 0;
articles.forEach(function(item,index,array){
if(item.results.length >0){
last.push({
domain:item.domain,
url:item.results[0].url
});
number+=item.results.length;
}
});
return {
number:number, //新文章個(gè)數(shù)
last:last //每個(gè)站點(diǎn)最新的文章URL
};
};
流程整合
將原先的單純輸出結(jié)果到命令行替換成寫(xiě)入到HTML頁(yè)面硅堆,并通知用戶屿储,點(diǎn)擊打開(kāi)頁(yè)面:
if(results.length === Object.keys(inputs).length){
var val = action.mergeArticles(results);//處理結(jié)果
if(val.number > 0){
result.updateLastArticles(val.last);//刷新最新的文章URL
var location = path.join(__dirname,'./result.html');
writer.writeAsHTMLFile(location,results);//寫(xiě)入到HTML文件
notifier.notifyNewArticles(val.number,location);//通知新文章可讀
}
}