【问题标题】:How to make multiple nested http requests with promises and async/await while web scraping如何在 Web 抓取时使用 Promise 和 async/await 发出多个嵌套的 http 请求
【发布时间】:2020-07-27 13:56:30
【问题描述】:

下面我有一个 Node.js 函数,它向不同的 url 发出一系列请求,然后对于每个 url,我使用 Cheerio 网络抓取库循环遍历 dom 上的元素并创建一个子数组。在每个请求结束时(在子数组已满之后),我想将该数组的内容推送到一个更大的数组,该数组超出了请求范围。

我尝试的方法似乎不起作用。看起来我无法从 .then 块内访问“allPlayers”。

function readPlayers(teamUrls){

    const allPlayers = [];

    teamUrls.forEach((teamUrl, i) => { 

        const options = {
            gzip: true,
            uri: teamUrl,
            Connection: 'keep-alive',
            transform: function (body) {
                return cheerio.load(body);
            }
        };

        request(options)
        .then(($) => {

            const team = [];

                $('tbody').children('tr').each(function(j, element){            

                     const playerName = $(element).children('td').eq(1).children('span').eq(1).find('a').text().trim();

                     const player = { 'playerName': playerName };

                     team.push(player);

                 });

            allPlayers.push(team);

        }).catch(err => console.log("error: " + err)) );

    });

}

所以我想知道重写此代码以使请求工作并用结果填充外部数组 (allPlayers) 的最佳方法。

我曾考虑尝试将整个请求直接推送到外部数组中,但无济于事。

在这个例子中,我使用 request-promise 来发出请求。

我研究过使用 Promise.map,我认为它适合这种情况。然后我会返回整个请求(我认为),但我不完全理解在这种情况下我在做什么......或者它是否会起作用。

任何人都可以解释这种情况下的范围,为什么我不能像我正在尝试的那样做。

非常感谢

【问题讨论】:

  • 我不明白你为什么在 then 语句中使用美元符号 - .then(($) => {
  • 这只是从cheerio web scraper 请求返回的DOM ..(我认为jquery 就是这样做的,这就是文档的方式)。它可以很容易地重命名为“DOM”或其他名称
  • 这里是request request-promise?因为原来的request lib 没有返回承诺
  • 您绝对可以访问闭包内的allPlayers(在then 块内)。您可能只是试图过早地消耗假定的结果。
  • forEach 替换为map,而不是推送只是返回结果并将Promise.all 放在所有内容的前面。

标签: javascript node.js http promise


【解决方案1】:

您必须记住,当您使用异步函数时,您无法返回到同步代码执行。

这是您可以做到的方法之一。它将并行获取所有玩家:

async function readPlayers(teamUrls) {
   const playerPromises = teamUrls.map((teamUrl, i) => {
    const options = {
      gzip: true,
      uri: teamUrl,
      Connection: 'keep-alive',
      transform: function(body) {
        return cheerio.load(body);
      }
    };
    return request(options)
  });

  const players = await Promise.all(playerPromises);
  return players.reduce((allPlayers, $) =>{
    const team = [];
    $('tbody').children('tr').each(function(j, element) {
      const playerName = $(element).children('td').eq(1).children('span').eq(1).find('a').text().trim();
      const player = { playerName: playerName };
      team.push(player);
    });
    allPlayers.push(team);
    return allPlayers;
  },[])
}

您可以使用await readPlayers(array)readPlayers(array).then(allteamplayers=>{...}) 来使用它

注意:在当前代码中,它将是一个二维数组,[[{p1:p1}..]、[{p2:p2}..]] 等

【讨论】:

  • 很好的答案,如果同步代码是嵌套的,它是否仍然有效(就像上面 Elias 的答案?)
  • 是的,它会工作没问题。我个人喜欢我的异步调用和处理器(或业务逻辑)不同。很容易重构。您可以在其他函数中取出对其他函数和处理器的 http 调用。您的处理器可以是具有多种类型处理器的类。如果你将这两者隔离开来,它们就会变得原子和独立。
  • 所以逻辑上的分离是fetchPlayers()getPlayerName()
  • 部分同意 Aritra 的观点,关注点分离是一种设计原则,可以更轻松地重构、扩展和测试代码。也许一个好的重构是在 processPlayers 函数中提取您的 .then 回调。
  • 完全正确 ^ @EliasFaraone
【解决方案2】:

如果您使用 forEach,每个回调都将异步运行,您将无法等待它们。你可以把它换成一个 for 循环,把你的 Promise 收集到一个数组中,然后等待所有的 Promise 完成:

async function readPlayers(teamUrls) {
  const allPlayers = [];
  const allPromises = [];

  for (var i = 0; i < teamUrls.length; i++) {
    var teamUrl = teamUrls[i];

    const options = {
      gzip: true,
      uri: teamUrl,
      Connection: "keep-alive",
      transform: function(body) {
        return cheerio.load(body);
      }
    };

    allPromises.push(
      request(options)
        .then($ => {
          const team = [];
          $("tbody")
            .children("tr")
            .each(function(j, element) {
              const playerName = $(element)
                .children("td")
                .eq(1)
                .children("span")
                .eq(1)
                .find("a")
                .text()
                .trim();
              const player = { playerName: playerName };
              team.push(player);
            });

          allPlayers.push(team);
        })
        .catch(err => console.log("error: " + err))
    );

    // wait untill all the promises resolve
    await Promise.all(allPromises);

    console.log(allPlayers);

    return allPlayers;
  }
}

然后你就可以通过等待你的函数来获取所有玩家:

var allPlayers = await readPlayers(teamUrls);

【讨论】:

  • 当然,当您在函数中使用 await 时,您需要使其异步。 forEach 接受一个 callBack 函数,但总是会调用 callBack(element) 之类的函数而不等待它!所以callBack在自身内部是同步的,但作为一个整体仍然会异步运行,这意味着调用forEach循环的代码在这些callBacks完成之前继续。
  • 所以,回顾一下,forEach 执行“callBack(element)”,但您想要“await callBack(element)”。为此,最简单的方法是改用“for”循环,您可以在其中指定如何调用该回调
  • 欣赏
猜你喜欢
  • 2018-07-05
  • 2018-01-22
  • 1970-01-01
  • 2019-11-23
  • 2013-08-03
  • 1970-01-01
  • 1970-01-01
  • 2018-03-23
  • 2020-03-01
相关资源
最近更新 更多