Синхронные множественные запросы в цикле foor с Node js

Я начинаю с Javascript, и мне нужна помощь, чтобы понять, как сделать этот код синхронным во время цикла for. По сути, я делаю несколько POST-запросов внутри для циклов, а затем я очищаю данные с помощью библиотеки X-Ray, и, наконец, я сохраняю результат в базе данных Mongo. Вывод в порядке, но он поступает неупорядоченным образом и неожиданно зависает, и мне приходится принудительно закрывать клавишей Ctrl+C. Это моя функция:

  function getdata() {
  const startYear = 1996;
  const currentYear = 1998; // new Date().getFullYear()

for (let i = startYear; i <= currentYear; i++) {
for (let j = 1; j <= 12; j++) {
  if (i === startYear) {
    j = 12;
  }

  // Form to be sent
  const form = {
    year: `${i}`,
    month: `${j}`,
    day: '01',
  };

  const formData = querystring.stringify(form);
  const contentLength = formData.length;

  // Make HTTP Request
  request({
    headers: {
      'Content-Length': contentLength,
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    uri: 'https://www.ipma.pt/pt/geofisica/sismologia/',
    body: formData,
    method: 'POST',
  }, (err, res, html) => {

    if (!err && res.statusCode === 200) {

      // Scrapping data with X-Ray
      x(html, '#divID0 > table > tr', {
        date: '.block90w',
        lat: 'td:nth-child(2)',
        lon: 'td:nth-child(3)',
        prof: 'td:nth-child(4)',
        mag: 'td:nth-child(5)',
        local: 'td:nth-child(6)',
        degree: 'td:nth-child(7)',
      })((error, obj) => {

        const result = {
          date: obj.date,
          lat: obj.lat.replace(',', '.'),
          lon: obj.lon.replace(',', '.'),
          prof: obj.prof == '-' ? null : obj.prof.replace(',', '.'),
          mag: obj.mag.replace(',', '.'),
          local: obj.local,
          degree: obj.degree,
        };

        // console.log(result);

        upsertEarthquake(result); // save to DB

      });

    }


  });

  }
  }
  }

Я предполагаю, что должен использовать обещания или обратные вызовы, но я не могу понять, как это сделать, и я уже пытался использовать асинхронное ожидание, но безуспешно. Если вам нужна дополнительная информация, пожалуйста, скажите мне, спасибо.

3 ответа

Решение

Вы вызываете запрос внутри цикла.

Асинхронные функции - это функции, в которых получение результата (AKA, получение ответа в функции обратного вызова) выполняется после завершения логики основного потока.

Таким образом, если у нас есть это:

for (var i = 0; i < 12; i++) {
    request({
        data: i
    }, function(error, data) {
        // This is the request result, inside a callback function
    });
}

Логика будет бежать за 12 requests перед вызовом обратных вызовов, поэтому обратные вызовы будут суммироваться и вызываться после выполнения всего основного цикла.

Не вдаваясь в подробности о генераторах ES6 (поскольку я думаю, что это немного усложняет работу, а также лучше изучать на низком уровне происходящее), вам нужно назвать request, дождитесь вызова его функции обратного вызова и вызовите следующий request, Как это сделать? Есть много способов, но я обычно так:

var i= 0;
function callNext() {
    if (i>= 12) {
        requestEnded();
    } else {
        request({
            data: i++ // Increment the counter as we are not inside a for loop that increments it
        }, function(error, data) {
            // Do something with the data, and also check if an error was received and act accordingly, which is very much possible when talking about internet requests
            console.log(error, data);
            // Call the next request inside the callback, so we are sure that the next request is ran just after this request has ended
            callNext();
        })
    }
}
callNext();

requestEnded() {
    console.log("Yay");
}

Здесь вы видите логику. У вас есть функция с именем callNext который сделает следующий звонок или звонок requestEnded если звонки больше не нужны.

когда request называется внутри callNext, он будет ожидать получения обратного вызова (что произойдет асинхронно, когда-нибудь в будущем), обработает полученные данные, а затем внутри обратного вызова вы скажете ему повторить вызов callNext,

Вместо зацикливания вы можете создать массив, используя начальный и конечный год, затем сопоставить его с конфигурацией для вашего запроса, а затем сопоставить результат этого с тем, что возвращает рентген (рентгеновский снимок возвращает обещание, так что нет необходимости в обратном вызове). Затем поместите результат очистки в mongodb, используя функцию, которая возвращает обещание.

Если что-то отвергает, создайте Fail введите объект и разрешите с этим объектом.

Запустите все запросы, рентген и монго параллельно с помощью Promise.all, но ограничьте количество активных запросов с помощью газа.

Вот как это будет выглядеть в коде:

//you can get library containing throttle here:
//  https://github.com/amsterdamharu/lib/blob/master/src/index.js
const lib = require('lib');
const Fail = function(details){this.details=details;};
const isFail = o=>(o&&o.constructor)===Fail;
const max10 = lib.throttle(10);
const range = lib.range;
const createYearMonth = (startYear,endYear)=>
  range(startYear,endYear)
  .reduce(
    (acc,year)=>
      acc.concat(
        range(1,12).map(month=>({year,month}))
      )
    ,[]
  );
const toRequestConfigs = yearMonths =>
  yearMonths.map(
    yearMonth=>{
      const formData = querystring.stringify(yearMonth);
      return {
        headers: {
          'Content-Length': formData.length,
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        uri: 'https://www.ipma.pt/pt/geofisica/sismologia/',
        body: formData,
        method: 'POST',
      };
    }
  );
const scrape = html =>
  x(
    html, 
    '#divID0 > table > tr', 
    {
      date: '.block90w',
      lat: 'td:nth-child(2)',
      lon: 'td:nth-child(3)',
      prof: 'td:nth-child(4)',
      mag: 'td:nth-child(5)',
      local: 'td:nth-child(6)',
      degree: 'td:nth-child(7)'
    }
  );
const requestAsPromise = config =>
  new Promise(
    (resolve,reject)=>
      request(
        config,
        (err,res,html)=>
          (!err && res.statusCode === 200) 
            //x-ray returns a promise:
            // https://github.com/matthewmueller/x-ray#xraythencb
            ? resolve(html)
            : reject(err)
      )
  );
const someMongoStuff = scrapeResult =>
  //do mongo stuff and return promise
  scrapeResult;
const getData = (startYear,endYear) =>
  Promise.all(
    toRequestConfigs(
      createYearMonth(startYear,endYear)
    )
    .map(
      config=>
        //maximum 10 active requests
        max10(requestAsPromise)(config)
        .then(scrape)
        .then(someMongoStuff)
        .catch(//if something goes wrong create a Fail type object
          err => new Fail([err,config.body])
        )
    )
  )
//how to use:
getData(1980,1982)
.then(//will always resolve unless toRequestConfigs or createYearMonth throws
  result=>{
    //items that were successfull
    const successes = result.filter(item=>!isFail(item));
    //items that failed
    const failed = result.filter(isFail);
  }
)

Что часто случается с очисткой, так это то, что целевой сайт не позволяет вам делать более x запросов в течение какого-либо периода и начинать заносить в черный список ваш IP и отказываться от услуги, если вы пройдете через это.

Допустим, вы хотите ограничить до 10 запросов в 5 секунд, тогда вы можете изменить приведенный выше код на:

const max10 = lib.throttlePeriod(10,5000);

Остальная часть кода такая же

У тебя есть sync for...loop с async methods внутри это проблема.

Чистый способ исправить это с

ES2017 async/await синтаксис

Предполагая, что вы хотите остановить каждую итерацию после upsertEarthquake(result) Вы должны изменить свой код на что-то вроде этого.

function async getdata() {
    const startYear = 1996;
    const currentYear = 1998; // new Date().getFullYear()

    for (let i = startYear; i <= currentYear; i++) {
        for (let j = 1; j <= 12; j++) {
            if (i === startYear)
                j = 12; 

            // Form to be sent
            const form = {
                year: `${i}`,
                month: `${j}`,
                day: '01',
            };

            const formData = querystring.stringify(form);
            const contentLength = formData.length;
            //Make HTTP Request
            await new Promise((next, reject)=> { 
                request({
                    headers: {
                        'Content-Length': contentLength,
                        'Content-Type': 'application/x-www-form-urlencoded',
                    },
                    uri: 'https://www.ipma.pt/pt/geofisica/sismologia/',
                    body: formData,
                    method: 'POST',
                }, (err, res, html) => {
                    if (err || res.statusCode !== 200)
                        return next() //If there is an error jump to the next

                    //Scrapping data with X-Ray
                    x(html, '#divID0 > table > tr', {
                        date: '.block90w',
                        lat: 'td:nth-child(2)',
                        lon: 'td:nth-child(3)',
                        prof: 'td:nth-child(4)',
                        mag: 'td:nth-child(5)',
                        local: 'td:nth-child(6)',
                        degree: 'td:nth-child(7)',
                    })((error, obj) => {
                        const result = {
                            date: obj.date,
                            lat: obj.lat.replace(',', '.'),
                            lon: obj.lon.replace(',', '.'),
                            prof: obj.prof == '-' ? null : obj.prof.replace(',', '.'),
                            mag: obj.mag.replace(',', '.'),
                            local: obj.local,
                            degree: obj.degree,
                        }
                        //console.log(result);
                        upsertEarthquake(result); // save to DB
                        next() //This makes jump to the next for... iteration
                    })

                }) 
            }
        }
    }
}

Я предполагаю что upsertEarthquake является асинхронной функцией или является типом огня и забыть.

Если есть ошибка, вы можете использовать next(), но если вы хотите разорвать цикл, используйте reject()

if (err || res.statusCode !== 200)
    return reject(err)
Другие вопросы по тегам