Nodejs爬取蚂蜂窝文章的爬虫以及搭建第三方服务器
如题,本项目用Nodejs实现了对蚂蜂窝网站的爬取,并将数据储存到MongoDB中,再以Express作服务器端,Angularjs作前端实现对数据的托管。
本项目Github地址:https://github.com/golmic/mafengwo-spider
本项目线上地址: http://mafengwo.lujq.me
本文介绍其中部分的技术细节。
获取数据
打开蚂蜂窝网站,发现文章部分的数据是用Ajax获取的,包括分页也是,所以查看一下实际的请求路径,为http://www.mafengwo.cn/ajax/ajax_article.php?start=1
所以程序应该向这个php文件发送请求,用Nodejs的话直接http请求也是没问题的,为了代码好看,我使用request库封装一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
function getArticleList(pageNum) { request({ url: "http://www.mafengwo.cn/ajax/ajax_article.php?start=" + pageNum, headers: { 'User-Agent': 'Mozilla/5.0' } }, function(error, response, data) { var res = data.match(/i\\\/\d{7}/g); for (var i = 0; i < 12; i++) { articlesUrl[i] = res[i * 3].substr(3, 7); }; async.each(articlesUrl, getArticle, function(err) { console.log('err: ' + err); }); }); }
|
每页是12篇文章,每篇文字都是(伪)静态页面,正则提取出其中的文章页Url。
对每个Url发送请求,拿到源码。
1 2 3 4 5 6 7 8 9 10
|
function getArticle(urlNumber) { request({ url: "http://www.mafengwo.cn/i/" + urlNumber + ".html", headers: { 'User-Agent': 'Mozilla/5.0' } }, function(error, response, data) {
|
接下来就是处理数据了。
这一段代码较长,但是目的是非常明确的,代码也很清晰。我们需要从这个页面中拿到文章的标题,以及文章的内容。(文章作者以及发布时间由于时间关系我并没有处理,不过也在代码以及数据库种预留了位置,这个同理很容易完成。)
来,我们分析一下这段代码。
1 2 3 4
|
var title, content, creator, created;
|
先是正则获取标题,然后把标题中的特殊符号做一下处理。
然后在实际访问蚂蜂窝网站时发现大多数文章都配有背景音乐,那我也给加上好了。于是这一段代码负责了获取背景音乐的直链地址。
获取文章内容,在写这段代码时发现它的文章是有两种dom结构的,所以分类处理了一下。
这一段代码处理一下图片,第一是文中的图片因为蚂蜂窝给定义了好多样式,并不符合响应式规则,我把与响应式冲突的部分给处理了一下。
然后为了美观,把文章的第一张图片作为列表显示时的特色图片,记录一下Url。
储存数据
事实上整个的任务到此就可以结束了。
1 2 3 4
|
fs.writeFile("html/" + title + ".html", content, function(e) { if (e) throw e; console.log(title); });
|
把每篇文章作为一个静态文件保存。然后遍历一下目录得到文章列表,凭借Nginx对静态资源强大的处理能力,这个网站也算是可以完工了。
出于后期管理文档以及把项目做得高大上点的目的,还是采用NOsql的翘楚MongoDB作为数据库端的解决方案。
1 2 3 4 5 6
|
MongoClient.connect('mongodb://localhost:27017/mean', function(err, db) { assert.equal(null, err); insertArticle(db, title, content, creator, mp3url, imageUrl, created, function() { db.close(); }); });
|
把数据储存到mean数据库中,mean即MongoDB/Expressjs/Angularjs/Nodejs的js全栈实践。
这样数据的储存就完成了。
搭建服务器
目录结构
为了后期维护以及合作开发,服务器端目录的结构与命名规则也需要注意下。
数据结构
为了后期管理员以及作者维护文章的考虑,数据库中不止有Articles一个collection,还有一个users的collection。
结构分别如下:
文章:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
var ArticleSchema = new Schema({ created: { type: Date, default: Date.now }, title: { type: String, default: '', trim: true, required: 'Title cannot be blank' }, content: { type: String, default: '', trim: true }, mp3url:{ type:String }, imageUrl:{ type:String }, creator: { type: String, default: 'golmic', } });
|
用户:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
var UserSchema = new Schema({ firstName: String, lastName: String, email: { type: String,
|
Nodejs驱动下,很容易实现对文章以及用户的CRUD操作。这里只展示了对文章操作的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
exports.list = function(req, res) { Article.find().sort('-created').exec(function(err, articles) { if (err) { return res.status(400).send({ message: getErrorMessage(err) }); } else { for(var i in articles){ articles[i].content=''; }; res.json(articles); } }); }; exports.read = function(req, res) { res.json(req.article); }; exports.update = function(req, res) { var article = req.article; article.title = req.body.title; article.content = req.body.content; article.save(function(err) { if (err) { return res.status(400).send({ message: getErrorMessage(err) }); } else { res.json(article); } }); }; exports.delete = function(req, res) { var article = req.article; article.remove(function(err) { if (err) { return res.status(400).send({ message: getErrorMessage(err) }); } else { res.json(article); } }); };
|
路由规则
首页为文章列表,然后每篇文章有一个url。前端规则很容易,另外为了符合RESTful API的要求,后端需要提供对CRUD操作的API。文章部分路由规则如下:
1 2 3 4 5 6 7 8 9 10
|
module.exports = function(app) { app.route('/api/articles') .get(articles.list) .post(users.requiresLogin, articles.create); app.route('/api/articles/:articleId') .get(articles.read) .put(users.requiresLogin, articles.hasAuthorization, articles.update) .delete(users.requiresLogin, articles.hasAuthorization, articles.delete); app.param('articleId', articles.articleByID); };
|
用户部分同理.
前端路由由Angular控制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
angular.module('articles').config(['$routeProvider', function($routeProvider) { $routeProvider. when('/', { templateUrl: 'articles/views/list-articles.client.view.html' }). when('/articles/create', { templateUrl: 'articles/views/create-article.client.view.html' }). when('/articles/:articleId', { templateUrl: 'articles/views/view-article.client.view.html' }). when('/articles/:articleId/edit', { templateUrl: 'articles/views/edit-article.client.view.html' }); } ]);
|
前端用ngResource模块处理资源位置:
1 2 3 4 5 6 7 8 9 10
|
angular.module('articles').factory('Articles', ['$resource', function($resource) {
|
用户管理
文章作者以及管理员可以修改或者删除文章,逻辑代码见路由部分,实现代码见CRUD部分。
最终效果
其它
其它未尽技术细节请发issue或邮件交流。
全能程序员交流QQ群290551701,群内程序员都是来自,百度、阿里、京东、小米、去哪儿、饿了吗、蓝港等高级程序员 ,拥有丰富的经验。加入我们,直线沟通技术大牛,最佳的学习环境,了解业内的一手的资讯。如果你想结实大牛,那 就加入进来,让大牛带你超神!