多个选择器上的 Puppeteer waitForSelector
Puppeteer waitForSelector on multiple selectors
我让 Puppeteer 控制一个网站,该网站有一个查找表单,可以 return 结果或 "No records found" 消息。我怎么知道哪个是 returned?
waitForSelector 似乎一次只等待一个,而 waitForNavigation 似乎不起作用,因为它是 returned 使用 Ajax。
我正在使用 try catch,但很难正确处理并且会减慢一切。
try {
await page.waitForSelector(SELECTOR1,{timeout:1000});
}
catch(err) {
await page.waitForSelector(SELECTOR2);
}
使任何元素存在
你可以使用querySelectorAll
和waitForFunction
一起解决这个问题。使用带逗号的所有选择器将 return 所有与任何选择器匹配的节点。
await page.waitForFunction(() =>
document.querySelectorAll('Selector1, Selector2, Selector3').length
);
现在这只会 return true
如果有一些元素,它不会 return 哪个选择器匹配哪些元素。
根据 Md. Abu Taher 的建议,我得到了这个:
// 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
如果无法满足请求,Puppeteer 方法可能会抛出错误。例如,如果选择器在给定时间范围内不匹配任何节点,page.waitForSelector(selector[ options]) 可能会失败。
对于某些类型的错误,Puppeteer 使用特定的错误 类。这些 类 可通过 require('puppeteer/Errors').
获得
支持列表类:
超时错误处理示例:
const {TimeoutError} = require('puppeteer/Errors');
// ...
try {
await page.waitForSelector('.foo');
} catch (e) {
if (e instanceof TimeoutError) {
// Do something if this is a timeout.
}
}
将上面的一些元素组合到一个辅助方法中,我构建了一个命令,允许我创建多个可能的选择器结果并处理第一个要解析的结果。
/**
* @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 :(');
}
})
我遇到了类似的问题并寻求这个简单的解决方案:
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
}
如何像我在下面的代码片段中那样使用 Promise.race()
,并且不要忘记 page.waitForSelector()
方法中的 { visible: true }
选项。
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);
}
进一步使用 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'
}
另一种简单的解决方案是从更 CSS 的角度来解决这个问题。 waitForSelector
似乎跟在 CSS selector list rules 之后。所以基本上你可以 select 多个 CSS 元素,只需使用逗号。
try {
await page.waitForSelector('.selector1, .selector2',{timeout:1000})
} catch (error) {
// handle error
}
在 puppeteer 中,您可以像这样简单地使用以逗号分隔的多个选择器:
const foundElement = await page.waitForSelector('.class_1, .class_2');
返回的元素将是在页面中找到的第一个元素的元素句柄。
接下来,如果您想知道找到了哪个元素,您可以像这样获取 class 名称:
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 }
我刚开始使用 Puppeteer,遇到了同样的问题,因此我想制作一个自定义函数来实现相同的 use-case。
函数如下:
async function waitForMySelectors(selectors, page){
for (let i = 0; i < selectors.length; i++) {
await page.waitForSelector(selectors[i]);
}
}
函数中的第一个参数接收选择器数组,第二个参数是我们在其中执行等待过程的页面。
调用函数如下例:
var SelectorsArray = ['#username', '#password'];
await waitForMySelectors(SelectorsArray, page);
虽然我还没有对它进行任何测试,但它似乎可以正常工作。
我让 Puppeteer 控制一个网站,该网站有一个查找表单,可以 return 结果或 "No records found" 消息。我怎么知道哪个是 returned? waitForSelector 似乎一次只等待一个,而 waitForNavigation 似乎不起作用,因为它是 returned 使用 Ajax。 我正在使用 try catch,但很难正确处理并且会减慢一切。
try {
await page.waitForSelector(SELECTOR1,{timeout:1000});
}
catch(err) {
await page.waitForSelector(SELECTOR2);
}
使任何元素存在
你可以使用querySelectorAll
和waitForFunction
一起解决这个问题。使用带逗号的所有选择器将 return 所有与任何选择器匹配的节点。
await page.waitForFunction(() =>
document.querySelectorAll('Selector1, Selector2, Selector3').length
);
现在这只会 return true
如果有一些元素,它不会 return 哪个选择器匹配哪些元素。
根据 Md. Abu Taher 的建议,我得到了这个:
// 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
如果无法满足请求,Puppeteer 方法可能会抛出错误。例如,如果选择器在给定时间范围内不匹配任何节点,page.waitForSelector(selector[ options]) 可能会失败。
对于某些类型的错误,Puppeteer 使用特定的错误 类。这些 类 可通过 require('puppeteer/Errors').
获得支持列表类:
超时错误处理示例:
const {TimeoutError} = require('puppeteer/Errors');
// ...
try {
await page.waitForSelector('.foo');
} catch (e) {
if (e instanceof TimeoutError) {
// Do something if this is a timeout.
}
}
将上面的一些元素组合到一个辅助方法中,我构建了一个命令,允许我创建多个可能的选择器结果并处理第一个要解析的结果。
/**
* @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 :(');
}
})
我遇到了类似的问题并寻求这个简单的解决方案:
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
}
如何像我在下面的代码片段中那样使用 Promise.race()
,并且不要忘记 page.waitForSelector()
方法中的 { visible: true }
选项。
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);
}
进一步使用 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'
}
另一种简单的解决方案是从更 CSS 的角度来解决这个问题。 waitForSelector
似乎跟在 CSS selector list rules 之后。所以基本上你可以 select 多个 CSS 元素,只需使用逗号。
try {
await page.waitForSelector('.selector1, .selector2',{timeout:1000})
} catch (error) {
// handle error
}
在 puppeteer 中,您可以像这样简单地使用以逗号分隔的多个选择器:
const foundElement = await page.waitForSelector('.class_1, .class_2');
返回的元素将是在页面中找到的第一个元素的元素句柄。
接下来,如果您想知道找到了哪个元素,您可以像这样获取 class 名称:
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 }
我刚开始使用 Puppeteer,遇到了同样的问题,因此我想制作一个自定义函数来实现相同的 use-case。
函数如下:
async function waitForMySelectors(selectors, page){
for (let i = 0; i < selectors.length; i++) {
await page.waitForSelector(selectors[i]);
}
}
函数中的第一个参数接收选择器数组,第二个参数是我们在其中执行等待过程的页面。
调用函数如下例:
var SelectorsArray = ['#username', '#password'];
await waitForMySelectors(SelectorsArray, page);
虽然我还没有对它进行任何测试,但它似乎可以正常工作。