Puppeteer中如何同步执行for循环?解决循环异步异常问题
Puppeteer循环无法同步执行的问题修复方案
我正在编写一个Puppeteer脚本,流程为:登录页面后进入商品列表页,获取带有[data-pid]属性的元素列表,逐个点击列表中的链接,跳转后检查是否存在#hunt > img元素,若存在则点击,随后返回上一页。
当前脚本中的for循环本该同步执行,但第一次循环正常,从第二次开始循环会立即执行所有剩余迭代,不再等待每一步完成。现有代码如下:
import puppeteer from 'puppeteer'; (async () => { const browser = await puppeteer.launch({headless:false}); const page = await browser.newPage(); // Login await page.goto('https://www.acherryontop.com/user/log_in', {waitUntil: 'domcontentloaded'}); let loginButton = await page.waitForSelector('#action2'); await page.type('#email', 'email@email.com'); await page.type('#password', 'password'); await loginButton.click(); await page.waitForNavigation(); // Start await page.goto('https://www.acherryontop.com/shop/company/stamperia_intl', {waitUntil: 'domcontentloaded'}); let elemList = await page.$$('[data-pid]'); for (let i = 0; i <= elemList.length; i++) { let pointsExist = !! await page.$('#hunt_progress_number'); if (pointsExist) { let currentItem = elemList[i]; // This has to be wrapped in a try or it errors on second iteration try { await currentItem.click({waitUntil: 'domcontentloaded'}); } catch { console.log('Failed to click'); } // If element is here, click on it try { let point = await page.$('#hunt > img'); await point.click({waitUntil: 'domcontentloaded'}); } catch { console.log('Point not found, going back...'); } // Go back and click on the next item await page.goBack({waitUntil: 'domcontentloaded'}); } } })();
问题根源
核心问题是:从商品列表页跳转再返回后,之前通过page.$$()获取的elemList中的元素句柄已失效。页面导航会重置DOM环境,旧元素引用不再指向当前页面的真实元素,导致后续循环的点击操作要么报错要么无响应,看起来像是循环“跳过等待直接执行”。
另外循环条件i <= elemList.length会导致最后一次迭代时elemList[i]为undefined,也是报错的诱因,应改为i < elemList.length。
修复方案
不要提前缓存所有元素句柄,每次循环开始时重新获取当前页面的目标元素,确保操作的是有效DOM节点。修改后的代码如下:
import puppeteer from 'puppeteer'; (async () => { const browser = await puppeteer.launch({headless: false}); const page = await browser.newPage(); // 登录流程 await page.goto('https://www.acherryontop.com/user/log_in', {waitUntil: 'domcontentloaded'}); const loginButton = await page.waitForSelector('#action2'); await page.type('#email', 'email@email.com'); await page.type('#password', 'password'); await loginButton.click(); await page.waitForNavigation(); // 进入商品列表页 await page.goto('https://www.acherryontop.com/shop/company/stamperia_intl', {waitUntil: 'domcontentloaded'}); // 获取商品总数,避免每次循环重复查询 const itemCount = await page.$$eval('[data-pid]', elems => elems.length); for (let i = 0; i < itemCount; i++) { const pointsExist = !!await page.$('#hunt_progress_number'); if (pointsExist) { try { // 每次通过:nth-of-type选择器定位当前元素,确保是当前页面的有效节点 await page.click(`[data-pid]:nth-of-type(${i + 1})`, {waitUntil: 'domcontentloaded'}); } catch (err) { console.log('点击商品失败:', err.message); // 点击失败后返回列表页,继续下一次循环 await page.goBack({waitUntil: 'domcontentloaded'}); continue; } // 检查并点击目标元素,使用waitForSelector确保元素存在 try { const pointElem = await page.waitForSelector('#hunt > img', {timeout: 3000}); await pointElem.click({waitUntil: 'domcontentloaded'}); } catch { console.log('未找到目标元素,返回列表页...'); } // 返回列表页并等待加载完成 await page.goBack({waitUntil: 'domcontentloaded'}); // 等待当前索引的商品元素重新渲染,确保下一次点击有效 await page.waitForSelector(`[data-pid]:nth-of-type(${i + 1})`); } } // 执行完毕关闭浏览器 await browser.close(); })();
关键修改点
- 避免元素句柄失效:不再提前缓存
elemList,每次循环通过:nth-of-type选择器定位当前元素,保证操作的是当前页面的有效DOM节点。 - 修正循环边界:将
i <= elemList.length改为i < itemCount,避免越界访问。 - 强化错误处理:点击商品失败后,通过
continue跳过当前迭代,防止后续逻辑出错。 - 可靠的元素等待:用
page.waitForSelector替代page.$,设置超时时间确保元素存在后再操作,避免无限等待。 - 列表页渲染等待:返回列表页后,等待目标元素重新渲染,提升下一次点击的可靠性。
内容的提问来源于stack exchange,提问作者Zach Wulf
相关产品推荐
相关产品推荐

