Node.js實(shí)現(xiàn)簡(jiǎn)易版"RSSReader"

目的

由于經(jīng)常會(huì)閱讀關(guān)于C++的博客迷帜,譬如Meeting C++!blogroll迹鹅、 redditr/cpp等担孔,面對(duì)很多信息來(lái)源江锨,每天去對(duì)應(yīng)站點(diǎn)讀新博客是比較麻煩的事情,試過(guò)一些RSS閱讀器糕篇,不太會(huì)用......那就自己動(dòng)手制作一個(gè)吧啄育。

思路

  1. 獲取博客主頁(yè)內(nèi)容
  2. 取出文章標(biāo)題及URL
  3. 移除舊文章
  4. 生成新文章列表
  5. 提示用戶閱讀

開(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í)行:

起步測(cè)試

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)分析界面:

fluentcpp

可以看到所有的文章都是用<article>...</article>包裹起來(lái)的饰迹,而文章的標(biāo)題和URL存放在<a href="URL">標(biāo)題</a>中,上一級(jí)為<h2 class="entry-title">...</h2>余舶。

那么啊鸭,我們可以取出所有的article,然后查找內(nèi)容的classentry-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é)果如下:

獲取文章的標(biāo)題和URL

封裝成為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);//通知新文章可讀
            }
        }

運(yùn)行效果

新文章通知
打開(kāi)后的界面
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市渐逃,隨后出現(xiàn)的幾起案子够掠,更是在濱河造成了極大的恐慌,老刑警劉巖茄菊,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疯潭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡面殖,警方通過(guò)查閱死者的電腦和手機(jī)竖哩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)脊僚,“玉大人期丰,你說(shuō)我怎么就攤上這事〕蕴簦” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵街立,是天一觀的道長(zhǎng)舶衬。 經(jīng)常有香客問(wèn)我,道長(zhǎng)赎离,這世上最難降的妖魔是什么逛犹? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上虽画,老公的妹妹穿的比我還像新娘舞蔽。我一直安慰自己,他們只是感情好码撰,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布渗柿。 她就那樣靜靜地躺著,像睡著了一般脖岛。 火紅的嫁衣襯著肌膚如雪朵栖。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天柴梆,我揣著相機(jī)與錄音陨溅,去河邊找鬼。 笑死绍在,一個(gè)胖子當(dāng)著我的面吹牛门扇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播偿渡,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼臼寄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了卸察?” 一聲冷哼從身側(cè)響起脯厨,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎坑质,沒(méi)想到半個(gè)月后合武,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涡扼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年稼跳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吃沪。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡汤善,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出票彪,到底是詐尸還是另有隱情红淡,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布降铸,位于F島的核電站在旱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏推掸。R本人自食惡果不足惜桶蝎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一驻仅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧登渣,春花似錦噪服、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至竹揍,卻和暖如春敬飒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背芬位。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工无拗, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人昧碉。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓英染,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親被饿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子四康,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容