蚂蜂窝爬虫

2015-09-12 Golmic 更多博文 » 博客 » GitHub »

Nodejs 爬虫

原文链接 http://code.lujq.me/2015/09/12/%E8%9A%82%E8%9C%82%E7%AA%9D%E7%88%AC%E8%99%AB/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


Nodejs爬取蚂蜂窝文章的爬虫以及搭建第三方服务器 <!--more--> 如题,本项目用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库封装一下。

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发送请求,拿到源码。

function getArticle(urlNumber) {
    request({
        url: "http://www.mafengwo.cn/i/" + urlNumber + ".html",
        headers: {
            'User-Agent': 'Mozilla/5.0'
        }
    }, function(error, response, data) {
        //处理数据
    });
};

接下来就是处理数据了。

这一段代码较长,但是目的是非常明确的,代码也很清晰。我们需要从这个页面中拿到文章的标题,以及文章的内容。(文章作者以及发布时间由于时间关系我并没有处理,不过也在代码以及数据库种预留了位置,这个同理很容易完成。) 来,我们分析一下这段代码。

var title, content, creator, created;
/*获取标题*/
title = data.match(/<h1.*>\s*.+\s*<\/h1>/).toString().replace(/\s*/g, "").replace(/$/g, "").replace(/\//g, "|").match(/>.+</).toString();
title = title.substring(1, title.length - 1);

先是正则获取标题,然后把标题中的特殊符号做一下处理。

/*如果有背景音乐就获取背景音乐*/
if (data.indexOf("music_url") < data.indexOf('music_auto_play')) {
    mp3url = data.substring(data.indexOf("music_url"), data.indexOf('music_auto_play'));
} else {
    mp3url = data.substring(data.indexOf("music_url"), data.indexOf('is_new_note'));
};
mp3url = mp3url.match(/http.+\.mp3/);
if (mp3url) {
    mp3url = mp3url.toString();
    content = '<audio src="' + mp3url + '" autoplay="autoplay" loop="loop"></audio>';
};

然后在实际访问蚂蜂窝网站时发现大多数文章都配有背景音乐,那我也给加上好了。于是这一段代码负责了获取背景音乐的直链地址。

/*获取文章内容,发现有两种类型,分别适配*/
if (data.indexOf('a_con_text cont') != -1) {
    content += data.substring(data.indexOf("a_con_text cont") + 296, data.indexOf('integral') - 12);
} else {
    content += data.substring(data.indexOf("ginfo_kw_hotel") + 16, data.indexOf('vc_total') - 19);
};

获取文章内容,在写这段代码时发现它的文章是有两种dom结构的,所以分类处理了一下。

/*移除它给图片定义的父标签宽度以便响应式*/
content = content.replace(/width:\d*px/g, "");
/*把文中˚∫圖片作為在列表中顯示時的圖片*/
/*有的第一張圖片是表情.....處理一下..*/
imageUrl = data.match(/http.*\.(jpeg|png|jpg)"/).toString();
imageUrl = imageUrl.substring(0, imageUrl.indexOf('"'));

这一段代码处理一下图片,第一是文中的图片因为蚂蜂窝给定义了好多样式,并不符合响应式规则,我把与响应式冲突的部分给处理了一下。 然后为了美观,把文章的第一张图片作为列表显示时的特色图片,记录一下Url。

储存数据

事实上整个的任务到此就可以结束了。

fs.writeFile("html/" + title + ".html", content, function(e) {
    if (e) throw e;
    console.log(title);
});

把每篇文章作为一个静态文件保存。然后遍历一下目录得到文章列表,凭借Nginx对静态资源强大的处理能力,这个网站也算是可以完工了。 出于后期管理文档以及把项目做得高大上点的目的,还是采用NOsql的翘楚MongoDB作为数据库端的解决方案。

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。 结构分别如下: 文章:

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

用户:

var UserSchema = new Schema({
    firstName: String,
    lastName: String,
    email: {
        type: String,
        // Validate the email format
        match: [/.+\@.+\..+/, "Please fill a valid email address"]
    },
    username: {
        type: String,
        // Set a unique 'username' index
        unique: true,
        // Validate 'username' value existance
        required: 'Username is required',
        // Trim the 'username' field
        trim: true
    },
    password: {
        type: String,
        // Validate the 'password' value length
        validate: [

            function(password) {
                return password && password.length > 6;
            }, 'Password should be longer'
        ]
    },
    salt: {
        type: String
    },
    provider: {
        type: String,
        // Validate 'provider' value existance
        required: 'Provider is required'
    },
    providerId: String,
    providerData: {},
    created: {
        type: Date,
        // Create a default 'created' value
        default: Date.now
    }
});

Nodejs驱动下,很容易实现对文章以及用户的CRUD操作。这里只展示了对文章操作的代码。

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。文章部分路由规则如下:

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控制:

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模块处理资源位置:

angular.module('articles').factory('Articles', ['$resource', function($resource) {
    // Use the '$resource' service to return an article '$resource' object
    return $resource('api/articles/:articleId', {
        articleId: '@_id'
    }, {
        update: {
            method: 'PUT'
        }
    });
}]);

用户管理

文章作者以及管理员可以修改或者删除文章,逻辑代码见路由部分,实现代码见CRUD部分。

最终效果

其它

其它未尽技术细节请发issue或邮件交流。