2017-12-18 22 views
1

나는 전자 상거래 사이트에서 데이터를 긁어 내기 위해 Node.js를 사용하고있다. 나는 Request을 사용하여 페이지의 DOM을 검색하고 Cheerio을 사용하여 서버 측 DOM 선택을 수행합니다.Javascript for 루프에 서버 측 지연을 추가하는 방법은 무엇입니까?

const cheerio = require('cheerio'); 
const request = require('request'); 

// takes a URL, scrapes the page, and returns an object with the data 
let scrapePage = (url) => { 

    return new Promise((resolve, reject) => { 

     request(url, (error, resp, body) => { 

      if(error){ 
       reject(error); 
      }; 

      let $ = cheerio.load(body); 
      let $url = url; 
      let $price = $('#rt-mainbody > div > div.details > div.Data > div:nth-child(4) > div.description').text(); 

      let obj = { 
       url: $url, 
       price: $price 
      } 

      resolve(obj); 

     }); 

    }); 

}; 

// Runs scrapePage in a loop 
// There is a variable called arrayOfURLs defined elsewhere that contains 100s of URLs 

for(let i = 0; i < arrayOfURLs.length; i++){ 
    scrapePage(arrayOfURLs[i]) 
     .then((obj) => { 
      //write to a file 
     }) 
     .catch((error) => { 
     }) 
}; 

문제는 그 요청을 보내 서버가 가끔 정지의 어떤 종류없이 너무 많은 요청을 보내는 거니까, 내가 믿고있어 빈 데이터를 다시 보내는 것입니다. JS의 비동기 특성으로 인해 루프의 각 반복 사이에 효과적인 지연을 추가하는 방법을 찾는 데 어려움을 겪고 있습니다. setTimeOut 자체가 비동기이기 때문에 동기식으로 setTimeOut을 추가하는 것만으로는 충분하지 않으며 서버에서 실행 중이므로 Window 개체가 없습니다.

편집

위의 코드는 내가 일하고 있어요 무엇의 단순화 된 버전입니다. 전체 코드는 이것이다 : 서버 응답을 보내기 전에 약속 해결하기 위해 당신이 기다리고되지 않습니다처럼

app.js

const fs = require('fs'); 
const path = 'urls.txt'; 
const path2 = 'results.txt'; 
const scraper = require('./scraper'); 

let scrapePage = (url) => { 
    scraper.scrapePage(url) 
     .then((obj) => { 
      // console.log('obj from the scraper with Promises was received'); 
      // console.log(obj); 
      // console.log('writing obj to a file'); 
      fs.appendFile(path2, JSON.stringify(obj) + ', ', (error) => { 
       if(error){ 
        console.log(error); 
       } else { 
        // console.log('Successfully wrote to ' + path2); 
       } 
      }) 
     }) 
     .catch((error) => { 
      console.log('There was an error scraping obj: '); 
      console.log(error); 
     }) 
} 

fs.readFile(path, 'utf8', (err, data) => { 

    if (err){ 
    throw err; 
    }; 

    var urlArray = JSON.parse(data); 

    // this returns an Unexpected Identifier error  
    // const results = await Promise.all(urlArray.map(scrapePage)); 

    // this returns an Unexpected Token Function error 
    // async function scrapePages(){ 
    // const results = await Promise.all(urlArray.map(scrapePage)); 
    // }; 

}); 

scraper.js

const request = require('request'); 
const cheerio = require('cheerio'); 

exports.scrapePage = (url) => { 
    return new Promise((resolve, reject) => { 
     request(url, (error, resp, body) => { 
      if(error){ 
       reject(error); 
      }; 

      let $ = cheerio.load(body); 
      let $url = url; 

      let $price = $('#rt-mainbody > div > div.details > div.itemData > div:nth-child(4) > div.description').text(); 

      let obj = { 
       url: $url, 
       price: $price 
      } 

      resolve(obj); 

     }) 
    }) 
} 
+0

[sleep()의 JavaScript 버전은 무엇입니까?] (https://stackoverflow.com/questions/951021/what-is-the-javascript-version-of-sleep) –

+0

당신이 의지 할 수없는 복제본 주어진 시간 후에 데이터가있을 것이라고 가정합니다. 대신 콜백 함수를 사용하여 접근을 시도하십시오. – Ctznkane525

+1

이 빈 데이터 문제를 조사해보고 최소한 헤더와 응답 코드를 기록하여 오류의 위치를 ​​파악할 수 있습니다. 추측하고있는 것처럼 보이며 오류가 무엇인지 알기도 전에 변경해야하는 이유는 무엇입니까? –

답변

2

나에게 보인다. async/await을 사용하여 for 루프를 완전히 제거 할 수 있습니다.

const results = await Promise.all(arrayOfURLs.map(scrapePage)); 
+0

작은 코드를 수정했습니다. 나는 이미'.then '로 해결하겠다는 약속을 기다리고있다. 'scrapePage' 메쏘드는 Promise를 반환하고,'.then'로 해결할 때까지 기다렸다가 그 결과를 파일에 씁니다. – fuzzybabybunny

+0

@fuzzybabybunny 그래, 각 긁힌 자국을 처리하는 데는 문제가 없지만, HTTP 응답을 반환하기 전에 해결할 모든 약속을 기다리는 것이 아니라, 서버가 "공백"을 반환 할 가능성이 가장 높습니다. 응답은 스크래핑이 끝나기 전에 * 반환되기 때문에 응답합니다. – James

+0

실제 코드를 포함하도록 원래 게시물을 수정했습니다. 나는'Promise.all'을 기다리고 당신의 비트를 추가했지만 오류가 발생했습니다 - 그것들은 업데이트 된 코드에 설명되어 있습니다. 나는 텍스트 파일에서 URL을 읽기 위해 실행하는 메소드의 콜백 함수에서'scrapePage' 함수를 실행 중입니다. – fuzzybabybunny

1

활성 연결이 x 개 이상인 경우 throttle을 사용할 수 있습니다. 또는 초 당 x 양 이상을 원하면 throttlePeriod을 사용할 수 있습니다.

페일 객체 여기

const Fail = function(details){this.details=details;}; 
const max10 = throttle(10)(scrapePage);//max 10 active connections 
//const fivePerSecond = throttlePeriod(2,1000)(scrapePage); //start no more than 2 per second 
Promise.all(
    arrayOfURLs.map(
    url => 
     max10(url) 
     .catch(err=>new Fail([err,url])) 
) 
) 
.then(
    results =>{ 
    successes = 
     results.filter(
     result=>(result&&result.constructor)!==Fail 
    ); 
    failed = 
     results.filter(
     result=>(result&&result.constructor)===Fail 
    ) 
    } 
); 
1
const cheerio = require('cheerio'); 
const request = require('request'); 
let scrapePage = (url) => { 

return new Promise((resolve, reject) => { 

    request(url, (error, resp, body) => { 

     if(error){ 
      reject(error); 
      return; 
     }; 

     if(!body) { 
      reject('Empty Body'); 
      return; 
     } 


     let $ = cheerio.load(body); 

     let $url = url; 
     let $price = $('#rt-mainbody > div > div.details > div.Data > div:nth-child(4) > div.description').text(); 

     let obj = { 
      url: $url, 
      price: $price 
     } 

     resolve(obj); 

    }); 

}); 
}; 

function processUrl(url){ 
scrapePage(url) 
    .then((obj) => { 
     //write to a file 
     if(i < arrayOfURLs.length) 
      processUrl(arrayOfURLs.pop()) 
    }) 
    .catch((error) => { 
     arrayOfURLs.unshift(url); 
     if(i < arrayOfURLs.length) // put this in finally block 
      processUrl(arrayOfURLs.pop()) 
    }) 
}; 
processUrl(arrayOfURLs.pop()); 

우리가 큐로 및 경우 arrayOfUrls 배열을 사용할 수 있습니다에게 단 하나의 요청이 실패 할 경우 그래서 당신이 오류를 잡을 수 귀하의 해결 핸들러를 호출하지 반환 않습니다 Promise.all 사용 우리는 오류 또는 빈 페이지를 받았다. 우리는 배열에서이 URL을 다시 푸시한다. 그렇게하면 모든 URL을 동기식으로 처리 할 수 ​​있습니다.

+1

이것은 훌륭한 6+ 솔루션처럼 보입니다. 나는 인덱스 대신'# array.pop'을 사용하는 것을 고려할 것이다. – pguardiario