• NodeJS制作爬虫全过程


    这篇文章主要介绍了NodeJS制作爬虫的全过程,包括项目建立,目标网站分析、使用superagent获取源数据、使用cheerio解析、使用eventproxy来并发抓取每个主题的内容等方面,有需要的小伙伴参考下吧。
     

    今天来学习alsotang的爬虫教程,跟着把CNode简单地爬一遍。

    建立项目craelr-demo
    我们首先建立一个Express项目,然后将app.js的文件内容全部删除,因为我们暂时不需要在Web端展示内容。当然我们也可以在空文件夹下直接 npm install express来使用我们需要的Express功能。

    目标网站分析
    如图,这是CNode首页一部分div标签,我们就是通过这一系列的id、class来定位我们需要的信息。

    使用superagent获取源数据

    superagent就是ajax API来使用的Http库,它的使用方法与jQuery差不多,我们通过它发起get请求,在回调函数中输出结果。

    复制代码代码如下:

    var express = require('express');
    var url = require('url'); //解析操作url
    var superagent = require('superagent'); //这三个外部依赖不要忘记npm install
    var cheerio = require('cheerio');
    var eventproxy = require('eventproxy');
    var targetUrl = 'https://cnodejs.org/';
    superagent.get(targetUrl)
        .end(function (err, res) {
            console.log(res);
        });

    它的res结果为一个包含目标url信息的对象,网站内容主要在其text(string)里。

    使用cheerio解析

    cheerio充当服务器端的jQuery功能,我们先使用它的.load()来载入HTML,再通过CSS selector来筛选元素。

    复制代码代码如下:

    var $ = cheerio.load(res.text);
    //通过CSS selector来筛选数据
    $('#topic_list .topic_title').each(function (idx, element) {
        console.log(element);
    });

    其结果为一个个对象,调用 .each(function(index, element))函数来遍历每一个对象,返回的是HTML DOM Elements。

    输出 console.log($element.attr('title'));的结果为 广州 2014年12月06日 NodeParty 之 UC 场
    之类的标题,输出 console.log($element.attr('href'));的结果为 /topic/545c395becbcb78265856eb2之类的url。再用NodeJS1的url.resolve()函数来补全完整的url。

    复制代码代码如下:

    superagent.get(tUrl)
        .end(function (err, res) {
            if (err) {
                return console.error(err);
            }
            var topicUrls = [];
            var $ = cheerio.load(res.text);
            // 获取首页所有的链接
            $('#topic_list .topic_title').each(function (idx, element) {
                var $element = $(element);
                var href = url.resolve(tUrl, $element.attr('href'));
                console.log(href);
                //topicUrls.push(href);
            });
        });

    使用eventproxy来并发抓取每个主题的内容
    教程上展示了深度嵌套(串行)方法和计数器方法的例子,eventproxy就是使用事件(并行)方法来解决这个问题。当所有的抓取完成后,eventproxy接收到事件消息自动帮你调用处理函数。

    复制代码代码如下:

    //第一步:得到一个 eventproxy 的实例
    var ep = new eventproxy();
    //第二步:定义监听事件的回调函数。
    //after方法为重复监听
    //params: eventname(String) 事件名,times(Number) 监听次数, callback 回调函数
    ep.after('topic_html', topicUrls.length, function(topics){
        // topics 是个数组,包含了 40 次 ep.emit('topic_html', pair) 中的那 40 个 pair
        //.map
        topics = topics.map(function(topicPair){
            //use cheerio
            var topicUrl = topicPair[0];
            var topicHtml = topicPair[1];
            var $ = cheerio.load(topicHtml);
            return ({
                title: $('.topic_full_title').text().trim(),
                href: topicUrl,
                comment1: $('.reply_content').eq(0).text().trim()
            });
        });
        //outcome
        console.log('outcome:');
        console.log(topics);
    });
    //第三步:确定放出事件消息的
    topicUrls.forEach(function (topicUrl) {
        superagent.get(topicUrl)
            .end(function (err, res) {
                console.log('fetch ' + topicUrl + ' successful');
                ep.emit('topic_html', [topicUrl, res.text]);
            });
    });

    结果如下

    扩展练习(挑战)

    获取留言用户名和积分

    在文章页面的源码找到评论的用户class名,classname为reply_author。console.log第一个元素 $('.reply_author').get(0)可以看到,我们需要获取东西都在这里头。

    首先,我们先对一篇文章进行抓取,一次性把需要的都得到即可。

    复制代码代码如下:

    var userHref = url.resolve(tUrl, $('.reply_author').get(0).attribs.href);
    console.log(userHref);
    console.log($('.reply_author').get(0).children[0].data);

    我们可以通过https://cnodejs.org/user/username抓取积分信息

    复制代码代码如下:

    $('.reply_author').each(function (idx, element) {
    var $element = $(element);
    console.log($element.attr('href'));
    });

    在用户信息页面 $('.big').text().trim()即为积分信息。

    使用cheerio的函数.get(0)为获取第一个元素。

    复制代码代码如下:

    var userHref = url.resolve(tUrl, $('.reply_author').get(0).attribs.href);
    console.log(userHref);

    这只是对于单个文章的抓取,对于40个还有需要修改的地方。

    书接上回,我们需要修改程序以达到连续抓取40个页面的内容。也就是说我们需要输出每篇文章的标题、链接、第一条评论、评论用户和论坛积分。

    如图所示,$('.reply_author').eq(0).text().trim();得到的值即为正确的第一条评论的用户。

    {<1>}

    在eventproxy获取评论及用户名内容后,我们需要通过用户名跳到用户界面继续抓取该用户积分

    复制代码代码如下:

    var $ = cheerio.load(topicHtml);
    //此URL为下一步抓取目标URL
    var userHref = 'https://cnodejs.org' + $('.reply_author').eq(0).attr('href');
    userHref = url.resolve(tUrl, userHref);
    var title = $('.topic_full_title').text().trim().replace(/ /g,"");;
    var href = topicUrl;
    var comment1 = $('.reply_content').eq(0).text().trim();
    var author1 = $('.reply_author').eq(0).text().trim();
    //传递参数到下一次并发抓取
    ep.emit('user_html', [userHref, title, href, comment1, author1]);

    在eventproxy这一次中,我们要找到score是放在哪里(class="big")。

    {<2>}

    找到classname就好办了,我们先试着把结果输出一下

    复制代码代码如下:

    var outcome = superagent.get(userUrl)
        .end(function (err, res) {
            if (err) {
                return console.error(err);
            }
            var $ = cheerio.load(res.text);
            var score = $('.big').text().trim();
            console.log(user[1]);
            console.log(user[2]);
            console.log(user[3]);
            console.log(user[4]);
            console.log($('.big').text().trim());
            return ({
                title: user[1],
                href: user[2],
                comment1: user[3],
                author1: user[4],
                score1: score
            });
        });
    });

    运行程序,这段代码得到的结果。

    {<3>}

    但是问题来了,我们在.end()的回调函数中能正确输出结果,但是不能正确的输出outcome。仔细一看,需要输出的outcome是一个Request对象。这是因为粗心犯的错的,.end()函数并不会传递返回值给Request对象,需要将结果返回到上一层(users)。

    复制代码代码如下:

    //find userDetails
    ep.after('user_html', topicUrls.length, function(users){
        users = users.map(function(user){
            var userUrl = user[0];
            var score;
            superagent.get(userUrl)
                .end(function (err, res) {
                    if (err) {
                        return console.error(err);
                    }
                    //console.log(res.text);
                    var $ = cheerio.load(res.text);
                    score = $('.big').text().trim();
                });
            return ({
                title: user[1],
                href: user[2],
                comment1: user[3],
                author1: user[4],
                score1: score
            });
        });

    把users好好地输出发现除了score1其他是正确值。仔细调试发现,程序是先进行了console.log(),然后再进行.map()。更准确地说,在.map()函数内,.get()的回调函数并没有执行完赋值score,return 返回值就进行了。这就是回调函数的异步,而外层的同步操作是不会等待回调函数做完操作的。

    {<4>}

    我的做法就是eventproxy再emit一层消息,伴随着消息把需要的数据一起传递给接收消息操作.after(),只有当消息全部接收完毕,再打印出传递的参数(结果)。

    复制代码代码如下:

    score = $('.big')text().trim();
    //新添加
    ep.emit('got_score', [user[1], user[2], user[3], user[4], score]);
    .....
    ep.after('got_score', 10, function(users){
    console.log(users);
    });

    {<6>}

    这个问题解决了,但score1的数值好像太大了点吧。再一看,原来class='big'有两个,用户的话题收藏也是属于这个class。我们得通过cheerio的.slice( start, [end] )来切取第一个元素,即将score 修改为 score = $('.big').slice(0).eq(0).text().trim();。正确结果如图。

    {<7>}

     
  • 相关阅读:
    Gym
    Gym
    Gym
    LA 3713 宇航员分组
    LA 3211 飞机调度(2—SAT)
    POJ 1050 To The Max
    51nod 1050 循环数组最大子段和
    UVa 11149 矩阵的幂(矩阵倍增法模板题)
    POJ 1236 Network of School
    UVa 11324 最大团(强连通分量缩点)
  • 原文地址:https://www.cnblogs.com/timssd/p/5642212.html
Copyright © 2020-2023  润新知