Node.js实现简易版"RSSReader"

目的

由于经常会阅读关于C++的博客,譬如Meeting C++!blogrollredditr/cpp等,面对很多信息来源,每天去对应站点读新博客是比较麻烦的事情,试过一些RSS阅读器,不太会用......那就自己动手制作一个吧。

思路

  1. 获取博客主页内容
  2. 取出文章标题及URL
  3. 移除旧文章
  4. 生成新文章列表
  5. 提示用户阅读

开发环境准备

起步

我们将建立一个命令行程序,在对应路径建立myreader目录,进入目录输入:

npm init

根据提示信息输入对应内容,生成package.json结果如下:

{
  "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"
}

在这里我将应用程序入口指定为./bin/AppEntry.js,测试内容如下:

#!/usr/bin/env node
console.log("我的RSS阅读器");

注意在Node.js的命令行程序,可执行程序第一行要这样写才能正确执行。

然后在myreader目录执行:

起步测试

npm link命令相当于注册了myreader,之后就可以使用myreader来执行AppEntry.js了。

获取博客主页内容

使用request来获取博客主页内容,测试的博客为Fluent{C++}:

安装request

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);
});

运行myreader

myreader > index.html

即可得到博客主页的内容。

取出文章的标题和URL

规律分析

打开Fluent{C++},使用Chrome的检查打开分析界面:

fluentcpp

可以看到所有的文章都是用<article>...</article>包裹起来的,而文章的标题和URL存放在<a href="URL">标题</a>中,上一级为<h2 class="entry-title">...</h2>

那么,我们可以取出所有的article,然后查找内容的classentry-title的元素,然后再取出其中的a拿到标题和href属性。

如何实现

使用cheerio来对网页进行处理,cheerio可以采用jQuery的方式对HTML进行操作:

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);
    });
    
});

运行myreader结果如下:

获取文章的标题和URL

封装成为Fetch.js

通过之前的方式,就可以获取博客主页上的文章信息了,将其封装成为Fetch.js
博客的内容大致如此,都是获取文章列表,然后定位到标题,即可取出想要的信息,那么对于特定的博客主页,输入的信息为:

{
    domain: 博客主页
    articleFilter: 文章过滤
    urlFilter: 标题过滤
}

Fetch.js实现如下:

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;

此时的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);
});

移除旧文章

最简单的实现就是记录每次获取到的博客站点最新的文章URL,当获取到文章列表数组时进行遍历,一旦碰到上一次的URL,则后续的文章都是旧文章。

简单起见,采用直接保存JSON对象的方式处理:

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});
};

对于结果的操作实现如下:

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;
        }
    }
};

移除旧文章操作实现如下:

//过滤掉旧文章
function removeOldArticles(last,articles){
    var results = [];
    articles.every(function(article,idx){
        if(article.url === last)
            return false;
        results.push(article);
        return true;
    });
    return results;
};

多博客站点设置

对于每个博客站点,只需要保存相应信息到配置,然后即可使用Fetch.js访问并得到文章列表,设置操作实现如下:

//配置文件的操作
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;
        }
    }
};

整合操作

将设置、获取、移除旧文章操作进行整合后的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在此实现

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){//当所有博客源都遍历完成
            console.log(results);//输出结果
        }
    });
});

通知用户

当获取到所有的新文章之后,需要告知用户有新文章可读及其入口,简单起见将新文章列表生成为HTML,然后弹出系统通知,当用户点击时打开该HTML页面。

生成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来弹出系统通知:

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){});
};

打开HTML页面

使用open

var open = require('open');
function notifyNewArticles(number,location){
    fireNotify({
        title:"新文章可读",
        message:'有'+(number+1)+"篇新文章可读,点击查看.",
        icon:'invalid.png',
        onClick:function(object,option){
            open("file:///"+location);
        }
    });
};

处理结果

当获取了每个博客站点的新文章列表后,还需要处理一下结果,然后用来刷新"最新的文章URL",实现如下:

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, //新文章个数
        last:last //每个站点最新的文章URL
    };
};

流程整合

将原先的单纯输出结果到命令行替换成写入到HTML页面,并通知用户,点击打开页面:

        if(results.length === Object.keys(inputs).length){
            var val = action.mergeArticles(results);//处理结果
            if(val.number > 0){
                result.updateLastArticles(val.last);//刷新最新的文章URL

                var location = path.join(__dirname,'./result.html');
                writer.writeAsHTMLFile(location,results);//写入到HTML文件

                notifier.notifyNewArticles(val.number,location);//通知新文章可读
            }
        }

运行效果

新文章通知
打开后的界面
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容