【问题标题】:How can I rewrite this with promises?我如何用承诺重写它?
【发布时间】:2017-01-30 15:16:58
【问题描述】:

我正在为 T 恤网站构建内容抓取工具。

目标是通过一个硬编码的网址进入网站:http://shirts4mike.com

然后我将找到每件 T 恤的所有产品页面,然后创建一个包含其详细信息的对象。然后将其添加到数组中。

当数组中的 T 恤装满时,我将遍历数组并将其记录到 CSV 文件中。

现在,我在请求/响应的时间安排和函数调用方面遇到了一些问题。

如何确保在正确的时间调用 NEXT 函数?我知道它不工作,因为它是异步的。

我怎样才能在正确的时间调用secondScrapelastScraperconvertJson2Csv,以便他们正在使用的变量不会未定义?

我尝试使用诸如 response.end() 之类的东西,但这不起作用。

我假设我需要使用 Promise 才能使其正常工作?并且清晰易读?

有什么想法吗?我的代码如下:

//Modules being used:
var cheerio = require('cheerio');
var request = require('request');
var moment = require('moment');

//hardcoded url
var url = 'http://shirts4mike.com/';

//url for tshirt pages
var urlSet = new Set();

var remainder;
var tshirtArray;


// Load front page of shirts4mike
request(url, function(error, response, html) {
    if(!error && response.statusCode == 200){
        var $ = cheerio.load(html);

    //iterate over links with 'shirt'
        $("a[href*=shirt]").each(function(){
            var a = $(this).attr('href');

            //create new link
            var scrapeLink = url + a;

            //for each new link, go in and find out if there is a submit button. 
            //If there, add it to the set
            request(scrapeLink, function(error,response, html){
                if(!error && response.statusCode == 200) {
                    var $ = cheerio.load(html);

                    //if page has a submit it must be a product page
                    if($('[type=submit]').length !== 0){

                        //add page to set
                        urlSet.add(scrapeLink);

                    } else if(remainder === undefined) {
                        //if not a product page, add it to remainder so it another scrape can be performed.
                        remainder = scrapeLink;                     
                    }
                }
            });
        });     
    }
    //call second scrape for remainder
    secondScrape();
});


function secondScrape() {
    request(remainder, function(error, response, html) {
        if(!error && response.statusCode == 200){
            var $ = cheerio.load(html);

            $("a[href*=shirt]").each(function(){
                var a = $(this).attr('href');

                //create new link
                var scrapeLink = url + a;

                request(scrapeLink, function(error,response, html){
                    if(!error && response.statusCode == 200){

                        var $ = cheerio.load(html);

                        //collect remaining product pages and add to set
                        if($('[type=submit]').length !== 0){
                            urlSet.add(scrapeLink);
                        }
                    }
                });
            });     
        }
    });
    console.log(urlSet);
    //call lastScraper so we can grab data from the set (product pages)
    lastScraper();
};



function lastScraper(){
    //scrape set, product pages
    for(var i = 0; i < urlSet.length; i++){
        var url = urlSet[i];

        request(url, function(error, response, html){
            if(!error && response.statusCode == 200){
                var $ = cheerio.load(html);

                //grab data and store as variables
                var price = $('.price').text();
                var img = $('.shirt-picture').find("img").attr("src");
                var title = $('body').find(".shirt-details > h1").text().slice(4);

                var tshirtObject = {};
                //add values into tshirt object

                tshirtObject.price = price;
                tshirtObject.img = img;
                tshirtObject.title = title;
                tshirtObject.url = url;
                tshirtObject.date = moment().format('MMMM Do YYYY, h:mm:ss a');

                //add the object into the array of tshirts
                tshirtArray.push(tshirtObject); 
            }
        });
    }
    //call function to iterate through tshirt objects in array in order to convert to JSON, then into CSV to be logged
    convertJson2Csv();
};

【问题讨论】:

  • 一个简单的转换为 promises 仍然会给你留下一个低效的过程,在这个过程中重新访问页面。一个好的范例是允许每个页面最多访问一次。

标签: javascript node.js web-scraping promise


【解决方案1】:

您正确地将 Promise 确定为解决时间问题的方法。

为了获得可用的承诺,您需要承诺request(或采用 HTTP 库,其方法返回承诺)。

您可以通过 Promise 解决时间问题,但您也可以借此机会改进整体范式。您可以编写一个递归调用自身的函数,而不是几乎相同的第一/第二/第三阶段的离散函数。写得正确,这将确保目标站点中的每个页面最多被访问一次;基于整体性能和目标服务器的负载,应避免重新访问。

//Modules being used:
var Promise = require('path/to/bluebird');
var cheerio = require('cheerio');
var moment = require('moment');

// Promisify `request` to make `request.getAsync()` available.
// Ref: http://stackoverflow.com/questions/28308131/how-do-you-properly-promisify-request
var request = Promise.promisify(require('request'));
Promise.promisifyAll(request);

//hardcoded url
var url = 'http://shirts4mike.com/';

var urlSet = new Set();
var tshirtArray = [];

var maxLevels = 3; // limit the recursion to this number of levels.

function scrapePage(url_, levelCounter) {
    // Bale out if :
    //   a) the target url_ has been visited already,
    //   b) maxLevels has been reached.
    if(urlSet.has(url_) || levelCounter >= maxLevels) {
        return Promise.resolve();
    }
    urlSet.add(url_);

    return request.getAsync(url_).then(function(response, html) {
        var $;
        if(response.statusCode !== 200) {
            throw new Error('statusCode was not 200'); // will be caught below
        }
        $ = cheerio.load(html);
        if($('[type=submit]').length > 0) {
            // yay, it's a product page.
            tshirtArray.push({
                price: $('.price').text(),
                img: $('.shirt-picture').find("img").attr("src"),
                title: $('body').find(".shirt-details > h1").text().slice(4),
                url: url_,
                date: moment().format('MMMM Do YYYY, h:mm:ss a')
            });
        }
        // find any shirt links on page represented by $, visit each link in turn, and scrape.
        return Promise.all($("a[href*=shirt]").map(function(link) {
            return scrapePage(link.href, levelCounter + 1);
        }).get());
    }).catch(function(e) {
        // ensure "success" even if scraping threw an error.
        console.log(e);
        return null;
    });
}

scrapePage(url, 0).then(convertJson2Csv);

如您所见,递归解决方案:

  • 避免重复代码,
  • 将根据需要向下钻取尽可能多的级别 - 由变量 maxLevels 确定。

注意:这仍然不是一个好的解决方案。这里有一个隐含的假设,就像在原始代码中一样,所有的衬衫页面都可以从站点的主页访问,仅通过“衬衫”链接。如果可以通过例如“clothing”>“shirts”访问衬衫,那么上面的代码将找不到任何衬衫。

【讨论】:

    【解决方案2】:

    您可以使用async 模块的waterfall 方法,它可以让您顺利解决此问题。

    我只是尝试用这个模块来做你的代码

    希望这对你有用

    瀑布格式

    async.waterfall([
      function(callback) {
        callback(null, previousvalue);
      },
      function(previousvalue, callback) {}
    ], function(err, result) { //Final callback
    
    });
    

    var async = require('async');
    var cheerio = require('cheerio');
    var request = require('request');
    var moment = require('moment');
    
    //hardcoded url
    var url = 'http://shirts4mike.com/';
    
    //url for tshirt pages
    var urlSet = new Set();
    
    var remainder;
    var tshirtArray = [];
    
    
    async.waterfall([
      function(callback) {
        // Load front page of shirts4mike
        request(url, function(error, response, html) {
          if (!error && response.statusCode == 200) {
            var $ = cheerio.load(html);
    
            //iterate over links with 'shirt'
            $("a[href*=shirt]").each(function() {
              var a = $(this).attr('href');
    
              //create new link
              var scrapeLink = url + a;
    
              //for each new link, go in and find out if there is a submit button. 
              //If there, add it to the set
              request(scrapeLink, function(error, response, html) {
                if (!error && response.statusCode == 200) {
                  var $ = cheerio.load(html);
    
                  //if page has a submit it must be a product page
                  if ($('[type=submit]').length !== 0) {
    
                    //add page to set
                    urlSet.add(scrapeLink);
                    callback(null, true);
    
                  } else if (remainder === undefined) {
                    //if not a product page, add it to remainder so it another scrape can be performed.
                    remainder = scrapeLink;
                    callback(nul, true);
                  }
                }
              });
            });
          }
          //call second scrape for remainder
          // secondScrape();
        });
      },
      function(previousvalue, callback) {
        request(remainder, function(error, response, html) {
          if (!error && response.statusCode == 200) {
            var $ = cheerio.load(html);
    
            $("a[href*=shirt]").each(function() {
              var a = $(this).attr('href');
    
              //create new link
              var scrapeLink = url + a;
    
              request(scrapeLink, function(error, response, html) {
                if (!error && response.statusCode == 200) {
    
                  var $ = cheerio.load(html);
    
                  //collect remaining product pages and add to set
                  if ($('[type=submit]').length !== 0) {
                    urlSet.add(scrapeLink);
                  }
                  callback(null, true);
                }
              });
            });
          }
        });
        console.log(urlSet);
        //call lastScraper so we can grab data from the set (product pages)
      },
      function(previousvalue, callback) {
        //scrape set, product pages
        for (var i = 0; i < urlSet.length; i++) {
          var url = urlSet[i];
    
          request(url, function(error, response, html) {
            if (!error && response.statusCode == 200) {
              var $ = cheerio.load(html);
    
              //grab data and store as variables
              var price = $('.price').text();
              var img = $('.shirt-picture').find("img").attr("src");
              var title = $('body').find(".shirt-details > h1").text().slice(4);
    
              var tshirtObject = {};
              //add values into tshirt object
    
              tshirtObject.price = price;
              tshirtObject.img = img;
              tshirtObject.title = title;
              tshirtObject.url = url;
              tshirtObject.date = moment().format('MMMM Do YYYY, h:mm:ss a');
    
              //add the object into the array of tshirts
              tshirtArray.push(tshirtObject);
            }
          });
        }
      }
    ], function(err, result) {
      //call function to iterate through tshirt objects in array in order to convert to JSON, then into CSV to be logged
      convertJson2Csv();
    });
    

    【讨论】:

      【解决方案3】:

      您可以使用此示例来转换代码示例的其余部分。

      promise = new Promise((resolve, reject) => ( 
          request("http://shirts4mike.com/", 
          (err, response, html) => (response.statusCode == 200 ? resolve(html): reject(err))
      )));
      
      
      promise.then(html => {
          var $ = cheerio.load(html);
          // continue
      });
      

      【讨论】:

      • 我会为每个请求创建一个新的承诺吗?
      • 是的,这就是路要走。
      • 我认为您将能够重构一个函数,该函数接受一个 URL 并返回一个承诺,并为每个对 URL 的请求调用一次该函数。
      【解决方案4】:

      有一个名为 request-promise 的 npm 模块。

      简单地说:

      var rp = require("request-promise");
      

      在任何你提出请求的地方,你都可以使用 request-promise 进行切换。

      例如:

      rp(url)
      .then(function(value){
        //do whatever
      })
      .catch(function(err){
        console.log(err)
      })
      

      【讨论】:

        猜你喜欢
        • 2015-10-03
        • 1970-01-01
        • 2015-10-28
        • 1970-01-01
        • 1970-01-01
        • 2016-03-19
        • 2019-02-08
        • 1970-01-01
        • 2019-02-04
        相关资源
        最近更新 更多