zoukankan      html  css  js  c++  java
  • 蚂蜂窝爬虫

    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;
    /*获取标题*/
    title = data.match(/<h1.*>s*.+s*</h1>/).toString().replace(/s*/g, "").replace(/$/g, "").replace(///g, "|").match(/>.+</).toString();
    title = title.substring(1, title.length - 1);

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /*如果有背景音乐就获取背景音乐*/
    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>';
    };

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

    1
    2
    3
    4
    5
    6
    /*获取文章内容,发现有两种类型,分别适配*/
    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结构的,所以分类处理了一下。

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

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

    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) {
    // Use the '$resource' service to return an article '$resource' object
    return $resource('api/articles/:articleId', {
    articleId: '@_id'
    }, {
    update: {
    method: 'PUT'
    }
    });
    }]);

    用户管理

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

    最终效果


    其它

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

    全能程序员交流QQ群290551701,群内程序员都是来自,百度、阿里、京东、小米、去哪儿、饿了吗、蓝港等高级程序员 ,拥有丰富的经验。加入我们,直线沟通技术大牛,最佳的学习环境,了解业内的一手的资讯。如果你想结实大牛,那 就加入进来,让大牛带你超神!

  • 相关阅读:
    XML操作
    C# 相等(==,Equals)
    VS插件 选项卡竖排列
    springcloud版本号
    @EnableDiscoveryClient和@EnableEurekaClient的区别
    在mvn构建时直接将项目jar包做成服务第三方工具
    java引用的强制转型
    java基础类型的byte为长度
    springboot访问服务器本地静态文件的方法
    nginx反向代理和负载均衡的基本配置
  • 原文地址:https://www.cnblogs.com/starliu/p/4864151.html
Copyright © 2011-2022 走看看