我正在尝试创建一个节点js web scraper。这个刮刀的整体操作是:
我希望能够像这样编写我的步骤。
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..');
});
答案 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请求)。
一种可能的实现方式是:
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集合。
那实际上只是:
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)
})
}