Узел: перебирать строки файла, обрабатывать каждую асинхронно, ждать каждого результата?

#javascript #node.js #asynchronous #async-await

#javascript #node.js #асинхронный #async-ожидание

Вопрос:

У меня есть файл с именем и информацией о дате рождения. Для каждой строки в файле мне нужно отправить данные в веб-форму и посмотреть, какой результат я получу. Я использую Node и Puppeteer (без головы), а также readline для чтения файла.

Код отлично работает для небольших файлов, но когда я запускаю его с полными 5000 именами или даже с несколькими сотнями, я получаю сотни безголовых экземпляров Chromium, что ставит мою машину на колени и, возможно, создает запутанные ошибки тайм-аута.

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

Есть мысли?

 const puppeteer = require('puppeteer');
const fs = require('fs');
const readline = require('readline');
const BALLOT_TRACK_URL = 'https://www.example.com/ballottracking.aspx';
const VOTER_FILE = 'MailBallotsTT.tab';
const VOTER_FILE_SMALL = 'MailBallotsTTSmall.tab';
const COUNTY = 'Example County';


checkBallot = (async ( fName, lName, dob, county ) => {
  /* Initiate the Puppeteer browser */
  const browser = await puppeteer.launch({headless:true });
  const page = await browser.newPage();

  await page.goto( BALLOT_TRACK_URL, { waitUntil: 'networkidle0' });


  // fill out the form
  await page.type('#ctl00_ContentPlaceHolder1_FirstNameText', fName );
  await page.type('#ctl00_ContentPlaceHolder1_LastNameText', lName );
  await page.type('#ctl00_ContentPlaceHolder1_DateOfBirthText', dob );
  await page.type('#ctl00_ContentPlaceHolder1_CountyDropDown', county );

  let pageData = await page.content();

  // Extract the results from the page
  try {
  submitSelector = 'input[name="ctl00$ContentPlaceHolder1$RetrieveButton"]';
  tableSelector  = '#ctl00_ContentPlaceHolder1_ResultPanel > div > div > div > table > tbody > tr:nth-child(3) > td:nth-child(7) > div';

  foundSubmitSelector = await page.waitForSelector(submitSelector, { timeout: 5000 } );
  clickResult = await page.click( submitSelector );
  foundTable = await page.waitForSelector(tableSelector, { timeout: 5000 } )

  let data = await page.evaluate( ( theSelector ) => {
    let text = document.querySelector( theSelector ).innerHTML.replaceAll('<br>', '').trim();
    /* Returning an object filled with the scraped data */
    return {
      text
    }
  }, tableSelector );
  return data;
} catch (error) {
  return {
    text: error.message
  }
} finally {
  browser.close();
}
});

const mainFunction =  () => {

  const readInterface = readline.createInterface({
    input: fs.createReadStream( VOTER_FILE_SMALL ),
    output: null,
    console: false
  });
  
  readInterface.on('line', async(line) => {
    split = line.split( 't' );
    fName = split[0];
    lName = split[1];
    dob = split[2];
    checkResult = await checkBallot( fName, lName, dob, COUNTY );
    console.log( line   't'   checkResult.text );
    to = await new Promise(resolve => setTimeout(resolve, 5000));
  });

};

mainFunction();
  

Комментарии:

1. Как использовать синхронную строку чтения в файле и заставить основную функцию читать и обрабатывать только одну строку из файла за раз? По завершении каждой строки основная функция снова вызывает себя с таймаутом 0. Когда основная функция, наконец, достигает конца всех строк, она может завершиться без вызова самой себя. Если вы хотите перекрыть, скажем, 5 вызовов, вы могли бы начать с вызова основной функции 5 раз. Каждый раз, когда одна из них завершается, она снова вызывает функцию main для чтения файла и обработки следующей строки.

Ответ №1:

Вот некоторый код, который реализует мое предложение в комментарии. Я использовал setTimeout для представления имеющегося у вас асинхронного кода, но в принципе подход должен быть легко адаптируемым:

 // Source file testReadFileSync.js

const fs = require( "fs" );

const data = fs.readFileSync( "./inputfile.txt", { encoding: "utf-8" } );
const lines = data.split( "n" );
console.log( `There are ${lines.length} lines to process`);

var currentLine = 0;

function main(){
    // Check if we have processed all the lines
    if (currentLine == lines.length ) return;

    // get the current line number, then increment it;
    let lineNum = currentLine  ;
    
    // Process the line
    asyncProcess( lineNum,  lines[ lineNum ] )

}


function asyncProcess( lineNum, line ){
    
    console.log( `Start processing line[${lineNum}]` );

    let delayMS = getRandomInt( 100 ) * 10
    setTimeout( function(){
        

        // ------------------------------------------------
        // this function represents all the async processing 
        // for one line.
        // After async has finished, we call main again
        // ------------------------------------------------

        console.log( `Finished processing line[${lineNum}]` );
        main();

    }, delayMS )

}

function getRandomInt(max) {
    return Math.floor(Math.random() * Math.floor(max));
}

// Start four parallel async processes, each of which will process one line at a time
main();
main();
main();
main();
  

Комментарии:

1. Спасибо! Я попробую. Похоже, что это приведет к считыванию всего файла в память, но это всего лишь около 5 Тыс. строк, так что все должно быть в порядке. Спасибо, что нашли время предложить пример кода — я дам вам знать, как это происходит.

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