如何控制承诺链流

时间:2016-12-14 17:34:40

标签: node.js promise

我正在尝试创建一个节点js web scraper。这个刮刀的整体操作是:

  1. 从数据库中获取URL数组。回报承诺。
  2. 从数据库发送请求到URL并刮取数据。返回承诺
  3. 将抓取的数据插入数据库。
  4. 我希望能够像这样编写我的步骤。

    getUrls()
      .then(scrapeData)
      .then(insertData);
    

    但是,我发现为了做到这一点,我必须等待每个网址中的所有数据在步骤2中解析(使用promise.all),以便继续进行下一个链接事件。

    这可能会造成问题,因为我可能会向数千个URL发送请求,如果在promise.all期间失败,则所有收集的数据都会丢失。

    我更愿意让每个功能都这样运行:

    getUrls() //grab array of all urls (could be thousands)
      .then(scrapeData) // for each url scrape data and immediately proceed to chained function
      .then(insertData);
    

    简而言之,是否有一种程序方法可以在等待数据时迭代承诺和控制链?

    我的代码:

    var express = require('express');
    var app = express();
    var request = require('request');
    var cheerio = require('cheerio');
    
    app.get('/', (req, res) => {
    
        var sql = require("mssql");
    
        // config for your database
        var config = {
            user: '',
            password: '',
            server: '',
            database: '',
            options: {
                encrypt: false // Use this if you're on Windows Azure 
            }
        } 
    
        const getSkus = () => {
            var promise = new Promise((resolve, reject) => {
                sql.connect(config, (err) => {
    
                    if (err) console.log(err);
    
                    // create Request object
                    var request = new sql.Request();
    
                    // query to the database and get the records
                    request.query('SELECT URL FROM PRODUCTS, (err, recordset) => {
    
                        if (err) {
                            console.log("There was an error executing the SQL statement: " + err)
                            reject(err);
                        } else{
                        resolve(recordset);
                        }
                    });
                });
             });
            return promise;
        }
    
        const urlGen = (skus) => {
            var base_url = 'http://somesite.com/search/?q='
            var urls = [];
    
            skus.forEach((sku) =>{
                let code = sku.Code;
                let mpn = sku.MPN;
                let url = base_url + mpn;
                urls.push(url);
            });
            return urls;
        }
    
        const makeRequests = (urls) => {
            var promises = [];
    
            urls.forEach((url) => {
                var promise = new Promise((resolve, reject) => {
                    request(url, (err, response, html) => {
                        if(!err && response.statusCode == 200){
                                //do scraping here
                                }
                                catch(err){
                                    reject(err);
                                    console.log('Error occured during data scraping:');
                                }
                                resolve(jsontemp);
                            }
                            else{
                                reject(err);
                            }
                    });
                });
                promises.push(promise);
            });
    
            return Promise.all(promises);
        }
    
        getSkus()
            .then(urlGen)
            .then(makeRequests)
            .catch((e) => console.log(e));
    
    });
    
    var server = app.listen(5000, function () {
        console.log('Server is running..');
    });
    

2 个答案:

答案 0 :(得分:1)

如您所知,您建议的计划:

getUrls()
  .then(scrapeData)
  .then(insertData);

旨在在继续下一步之前在整个阵列上运行该过程的每个步骤。但是,听起来您希望尽快处理每个URL,而不是等待所有URL完成每个步骤。

  

然而,我发现为了做到这一点,我必须等待所有   来自每个网址的数据,以便在步骤2中解析(使用promise.all)   为了进入下一个链式事件。

是的,这就是上面代码的目的。

  

这可能会造成问题,因为我可能会发送请求   成千上万的URL,如果一个在promise.all期间失败,则所有数据   聚集然后失去了。

如果你希望promise链式迭代继续进行,即使有错误,那么你必须在迭代中本地捕获错误,这样它们就不会自动向上传播,这会阻止promise链。即使发生某些错误,这也允许整个迭代完成。或者,您可以使用通常称为Promise.all()的{​​{1}}替代等待所有承诺(无论是拒绝还是已解决),然后返回所有结果。你可以看到an implementation of settle works here是怎样的,虽然这个概念类似于我下面的代码所示。

如果我正确理解您的代码,settle()是一个异步函数,它从数据库返回一个skus列表,而getSkus()是一个同步函数,它只将skus处理成URL。因此,每一项都是单一的操作,实际上不能分解成碎片,因此我们将从操作的开始开始。

因此,你可以这样做:

getURLs()

在此实现中,const Promise = require('bluebird'); const request = Promise.promisifyAll(require('request'), {multiArgs: true}); Promise.map(getSkus().then(getURLs), function(url) { // This will only ever return a promise that resolves (all rejections are caught locally) // so that Promise.map() will not stop when an error occurs, but will // process all URLs return request.getAsync(url).then(scrapeData).then(insertData).catch(function(err) { // log the error, but conciously let the promise iteration continue (without err) console.err(err); // put error in the results in case caller wants to see all errors return err; }); }, {concurrency: 10}).then(function(results) { // results will be an array of whatever insertData returns // of for any step in the iteration that had an error, it will be // some type of Error object }); scrapeData()适用于处理它们在此处传递的参数。

这使用Bluebird来宣传insertData()模块并使用一些并发控制来迭代你的URL数组(以防止启动大量的同时请求),尽管你可以通过自己编写更多代码来使用标准的ES6承诺控制并发并宣传request()模块。

仅使用ES6标准承诺(并且没有任何并发​​控制来限制一次传入的请求数量),您可以这样做:

request()

实现自己的并发控制需要更多代码(这就是我在第一个例子中使用Bluebird的原因,因为它内置了它)。

答案 1 :(得分:-2)

秘诀是在上一次执行后执行下一次request调用(假设它发出HTTP请求)。

使用本机ES6 Promise实现

一种可能的实现方式是:

function getUrls () {
  var urls = ['http://google.com', 'http://amazon.com', 'http://microsoft.com']
  var bodies = []

  // Initial value
  var promise = Promise.resolve()

  urls.forEach(function (url) {
    // We assign a new promise that will resolve only after the previous one has finished
    promise = promise.then(function () {
      return request(url)
    }).then(function (body) {
      bodies.push(body)
    })
  })

  // Then we return a promise that is the result of all fetched urls
  return promise.then(function () {
    return bodies
  })
}

但是,我建议你使用bluebird模块,它有一些非常方便的方法来处理promises集合。

使用bluebird承诺实施

那实际上只是:

var Promise = require('bluebird')
function getUrls () {
  var urls = ['http://google.com', 'http://amazon.com', 'http://microsoft.com']
  return Promise.resolve(urls).map(function (url) {
    return request(url)
  })
}

如果您确实希望该操作可以重复,则可以进行一些修改。

var Promise = require('bluebird')

function getUrls () {
  var urls = ['http://google.com', 'http://amazon.com', 'http://microsoft.com']
  return Promise.resolve(urls).map(function (url) {
    return retriableRequest(url, 3)
  })
}

function retriableRequest (url, retries) {
  return request(url).catch(function (error) {
    if (retries <= 0) throw error

    return retriableRequest(url, retries - 1)
  })
}