Кукловод waitForSelector на нескольких селекторах

У меня есть Puppeteer, контролирующий веб-сайт с помощью формы поиска, которая может либо возвращать результат, либо сообщение «Записи не найдены». Как я могу узнать, что было возвращено? waitForSelector, кажется, ждет только одного за раз, в то время как waitForNavigation, похоже, не работает, потому что он возвращается с использованием Ajax. Я использую пробный захват, но его сложно сделать правильно, и он все сильно замедляет.

try {
    await page.waitForSelector(SELECTOR1,{timeout:1000}); 
}
catch(err) { 
    await page.waitForSelector(SELECTOR2);
}

person Jon Wilson    schedule 20.04.2018    source источник


Ответы (9)


Создание любого из элементов существует

Вы можете использовать querySelectorAll и waitFor вместе, чтобы решить эту проблему. Использование всех селекторов с запятой вернет все узлы, соответствующие любому селектору.

await page.waitFor(() => 
  document.querySelectorAll('Selector1, Selector2, Selector3').length
);

Теперь это вернет только true, если есть какой-то элемент, он не вернет, какой селектор соответствовал каким элементам.

person Md. Abu Taher    schedule 21.04.2018
comment
Интересно ... может быть, когда я узнаю, что один из них есть, я смогу использовать свой блок try с очень коротким таймаутом? - person Jon Wilson; 22.04.2018
comment
да. Это правда. - person Md. Abu Taher; 08.02.2019

как насчет использования Promise.race(), как что-то, что я сделал в приведенном ниже фрагменте кода, и не забудьте параметр { visible: true } в методе page.waitForSelector().

public async enterUsername(username:string) : Promise<void> {
    const un = await Promise.race([
        this.page.waitForSelector(selector_1, { timeout: 4000, visible: true })
        .catch(),
        this.page.waitForSelector(selector_2, { timeout: 4000, visible: true })
        .catch(),
    ]);

    await un.focus();
    await un.type(username);
}
person Alferd Nobel    schedule 20.05.2020
comment
Это прекрасное решение. Фактически, если обещание.all (или allSettled) является естественным решением для логики И, то Promise.race следует рассматривать как естественное решение для ИЛИ. - person toyssamurai; 27.07.2020
comment
Дальнейшее расширение его до: let res = await Promise.race ([frame.waitForSelector (.selector1) .then (() = ›{return 1}) .catch (), frame.waitForSelector (.selector0) .then (() = ›{Return 0}) .catch ()]); вы также можете узнать, какой селектор сработал. - person George Y.; 27.07.2021

Воспользовавшись предложением доктора Абу Тахера, я пришел к следующему:

// One of these SELECTORs should appear, we don't know which
await page.waitForFunction((sel) => { 
    return document.querySelectorAll(sel).length;
},{timeout:10000},SELECTOR1 + ", " + SELECTOR2); 

// Now see which one appeared:
try {
    await page.waitForSelector(SELECTOR1,{timeout:10});
}
catch(err) {
    //check for "not found" 
    let ErrMsg = await page.evaluate((sel) => {
        let element = document.querySelector(sel);
        return element? element.innerHTML: null;
    },SELECTOR2);
    if(ErrMsg){
        //SELECTOR2 found
    }else{
        //Neither found, try adjusting timeouts until you never get this...
    }
};
//SELECTOR1 found
person Jon Wilson    schedule 25.04.2018

У меня была аналогичная проблема, и я выбрал это простое решение:

helpers.waitForAnySelector = (page, selectors) => new Promise((resolve, reject) => {
  let hasFound = false
  selectors.forEach(selector => {
    page.waitFor(selector)
      .then(() => {
        if (!hasFound) {
          hasFound = true
          resolve(selector)
        }
      })
      .catch((error) => {
        // console.log('Error while looking up selector ' + selector, error.message)
      })
  })
})

А потом использовать:

const selector = await helpers.waitForAnySelector(page, [
  '#inputSmsCode', 
  '#buttonLogOut'
])

if (selector === '#inputSmsCode') {
  // We need to enter the 2FA sms code. 
} else if (selector === '#buttonLogOut') {
  // We successfully logged in
}
person Gabriel Morin    schedule 19.12.2019

Альтернативным и простым решением было бы подойти к этому с точки зрения CSS. waitForSelector, похоже, следует правилам списка селекторов CSS. Таким образом, вы можете выбрать несколько элементов CSS, просто используя запятую.

try {    
    await page.waitForSelector('.selector1, .selector2',{timeout:1000})
} catch (error) {
    // handle error
}
person Jon Black    schedule 20.10.2020

В кукловоде вы можете просто использовать несколько селекторов, разделенных запятой, вот так:

const foundElement = await page.waitForSelector('.class_1, .class_2');

Возвращенный элемент будет elementHandle первого элемента, найденного на странице.

Затем, если вы хотите узнать, какой элемент был найден, вы можете получить имя класса следующим образом:

const className = await page.evaluate(el => el.className, foundElement);

в вашем случае должен работать код, подобный этому:

const foundElement = await page.waitForSelector([SELECTOR1,SELECTOR2].join(','));
const responseMsg = await page.evaluate(el => el.innerText, foundElement);
if (responseMsg == "No records found"){ // Your code here }
person Andyba    schedule 19.02.2021

Объединив некоторые элементы сверху во вспомогательный метод, я создал команду, которая позволяет мне создавать несколько возможных результатов выбора и обрабатывать первое решение.

/**
 * @typedef {import('puppeteer').ElementHandle} PuppeteerElementHandle
 * @typedef {import('puppeteer').Page} PuppeteerPage
 */

/** Description of the function
  @callback OutcomeHandler
  @async
  @param {PuppeteerElementHandle} element matched element
  @returns {Promise<*>} can return anything, will be sent to handlePossibleOutcomes
*/

/**
 * @typedef {Object} PossibleOutcome
 * @property {string} selector The selector to trigger this outcome
 * @property {OutcomeHandler} handler handler will be called if selector is present
 */

/**
 * Waits for a number of selectors (Outcomes) on a Puppeteer page, and calls the handler on first to appear,
 * Outcome Handlers should be ordered by preference, as if multiple are present, only the first occuring handler
 * will be called.
 * @param {PuppeteerPage} page Puppeteer page object
 * @param {[PossibleOutcome]} outcomes each possible selector, and the handler you'd like called.
 * @returns {Promise<*>} returns the result from outcome handler
 */
async function handlePossibleOutcomes(page, outcomes)
{
  var outcomeSelectors = outcomes.map(outcome => {
    return outcome.selector;
  }).join(', ');
  return page.waitFor(outcomeSelectors)
  .then(_ => {
    let awaitables = [];
    outcomes.forEach(outcome => {
      let await = page.$(outcome.selector)
      .then(element => {
        if (element) {
          return [outcome, element];
        }
        return null;
      });
      awaitables.push(await);
    });
    return Promise.all(awaitables);
  })
  .then(checked => {
    let found = null;
    checked.forEach(check => {
      if(!check) return;
      if(found) return;
      let outcome = check[0];
      let element = check[1];
      let p = outcome.handler(element);
      found = p;
    });
    return found;
  });
}

Чтобы использовать его, вам просто нужно вызвать и предоставить массив возможных результатов и их селекторов / обработчиков:

 await handlePossibleOutcomes(page, [
    {
      selector: '#headerNavUserButton',
      handler: element => {
        console.log('Logged in',element);
        loggedIn = true;
        return true;
      }
    },
    {
      selector: '#email-login-password_error',
      handler: element => {
        console.log('password error',element);
        return false;
      }
    }
  ]).then(result => {
    if (result) {
      console.log('Logged in!',result);
    } else {
      console.log('Failed :(');
    }
  })
person BadPirate    schedule 21.08.2019

Еще один шаг, используя Promise.race(), обернув его и просто проверив индекс для дальнейшей логики:

// Typescript
export async function racePromises(promises: Promise<any>[]): Promise<number> {
  const indexedPromises: Array<Promise<number>> = promises.map((promise, index) => new Promise<number>((resolve) => promise.then(() => resolve(index))));
  return Promise.race(indexedPromises);
}
// Javascript
export async function racePromises(promises) {
  const indexedPromises = promises.map((promise, index) => new Promise((resolve) => promise.then(() => resolve(index))));
  return Promise.race(indexedPromises);
}

Использование:

const navOutcome = await racePromises([
  page.waitForSelector('SELECTOR1'),
  page.waitForSelector('SELECTOR2')
]);
if (navigationOutcome === 0) {
  //logic for 'SELECTOR1'
} else if (navigationOutcome === 1) {
  //logic for 'SELECTOR2'
}


person LeOn - Han Li    schedule 11.10.2020

Методы кукловода могут вызывать ошибки, если они не могут выполнить запрос. Например, page.waitForSelector (selector [, options]) может завершиться ошибкой, если селектор не соответствует ни одному узлу в течение заданного периода времени.

Для определенных типов ошибок Puppeteer использует определенные классы ошибок. Эти классы доступны через require ('puppeteer / Errors').

Список поддерживаемых классов:

TimeoutError

Пример обработки ошибки тайм-аута:

const {TimeoutError} = require('puppeteer/Errors');

// ...

try {
  await page.waitForSelector('.foo');
} catch (e) {
  if (e instanceof TimeoutError) {
    // Do something if this is a timeout.
  }
}
person C Alonso C Ortega    schedule 17.08.2018