使用Puppeteer与Puppeteer-Cluster出现“Attempted to use detached Frame”错误
问题描述
基于Express.js构建应用,使用Puppeteer和puppeteer-cluster访问乌克兰测试平台自动完成测试,流程为:
- 进入用户控制台
- 搜索测试
- 进入测试
- 完成测试
但在进入测试并回答若干问题后,出现错误:Attempted to use detached Frame 'D78184744F4CA70F50307B66DDF53250',提示无法获取类为.v-test-questions-title的元素(该元素实际存在)。已尝试修改Puppeteer配置、容器运行、更换浏览器、调整代码、添加等待导航/网络空闲等方法,均未解决,仅部分方案改变错误名称。注意:回答问题时URL不会变化。
相关代码
solveSingleAnswerQuestion方法
async solveSingleAnswerQuestion() { const pageElement = new PageElement(this.page) const question = await pageElement.getContent('.v-test-questions-title') // 错误发生在此处 const variants = await pageElement.getContents( '.v-test-questions-radio-block', '.v-test-questions-checkbox-block' ) console.log({ question, variants }) const answerIndex = await generateResponseIndex( question || '', variants ) console.log({ answerIndex }) const variantsElements = await pageElement.getElements( '.v-test-questions-radio-block', '.v-test-questions-checkbox-block' ) await variantsElements[answerIndex - 1].click() const submitButton = await pageElement.getElement('.v-blue-button-test') await submitButton.click() await waitFor(5000) }
PageElement类
import { cleanString, waitFor } from '@/utils' import type { ElementHandle, Page } from 'puppeteer' export class PageElement { constructor(private readonly page: Page) {} async loadAllElements() { const pageElement = new PageElement(this.page) const button = await pageElement.getElement('.vo-btn-blue') if (button) { await button.click() await waitFor(5000) await this.loadAllElements() } } async getElementsByUniqueSelector(selector: string, fallback?: string) { const elements = await this.getElements(selector, fallback) const filteredElements = ( await Promise.all(elements.map(this.isElementWithSingleClass)) ).filter((element) => element !== null) return filteredElements } async getContentsByUniqueSelector(selector: string, fallback?: string) { const elements = await this.getElementsByUniqueSelector( selector, fallback ) const contents = await Promise.all( elements.map((element) => element.evaluate((el) => el.textContent)) ) return this.cleanContents(contents) } async getContents(selector: string, fallback?: string) { const elements = await this.getElements(selector, fallback) const contents = await Promise.all( elements.map((element) => element.evaluate((el) => el.textContent)) ) return this.cleanContents(contents) } async getElements(selector: string, fallback?: string) { const elements = await this.waitForAndGetElements(selector) if (elements.length > 0) { return elements } if (fallback) { return await this.waitForAndGetElements(fallback) } return [] } async getContent(selector: string, fallback?: string) { const element = await this.getElement(selector, fallback) const content = await element?.evaluate((el) => el.textContent) const cleanedContent = cleanString(content || '') return cleanedContent } async getElement(selector: string, fallback?: string) { const elements = await this.getElements(selector, fallback) return elements[0] } private async waitForAndGetElements(selector: string) { const el = await this.page .waitForSelector(selector, { visible: true, timeout: 5000, }) .catch(() => null) const elContent = await el?.evaluate((el) => el.textContent) console.log({ elContent }) return await this.page.$$(selector) // 错误提示此处无法获取.v-test-questions-title元素 } private async isElementWithSingleClass(element: ElementHandle) { const className = await element.getProperty('className') const classList = (await className.jsonValue()).toString().split(' ') return classList.length > 1 ? null : element } private async cleanContents(contents: (string | null)[]) { return contents.map((content) => cleanString(content || '')) } }
集群初始化代码
import { Cluster } from 'puppeteer-cluster' export const cluster = await Cluster.launch({ concurrency: Cluster.CONCURRENCY_CONTEXT, maxConcurrency: 1000, // monitor: true, puppeteerOptions: { headless: true, dumpio: true, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', ], }, })
服务代码
async solveTestByTitle(testTitle: string) { await this.enterCabinet() const test = new Test(this.page) const pageElement = new PageElement(this.page) await test.startSolvingTestByTitle(testTitle) const question = await pageElement.getContent('.v-test-questions-title') console.log({ questionInService: question }) while (question) { await test.solveSingleAnswerQuestion() } }
解决方向
处理Frame分离问题
Frame分离说明当前页面上下文已失效,测试页面可能通过动态替换Frame或重渲染销毁了原关联上下文。可尝试:- 每次操作前通过
page.mainFrame()获取最新主Frame,替换原操作上下文 - 监听
frameattached/framedetached事件,在Frame变化时重新绑定操作逻辑
- 每次操作前通过
优化元素等待逻辑
当前waitForAndGetElements中两次操作(等待元素+获取元素)存在时间差,可能导致页面变化后获取失败。合并逻辑并增加重试:private async waitForAndGetElements(selector: string, retryCount = 2) { try { await this.page.waitForSelector(selector, { visible: true, timeout: 5000 }); return await this.page.$$(selector); } catch (e) { if (retryCount > 0) { await waitFor(2000); return this.waitForAndGetElements(selector, retryCount - 1); } throw e; } }修复循环逻辑漏洞
原服务代码中question仅初始化一次,会导致无限循环。修改为每次循环重新检查:async solveTestByTitle(testTitle: string) { await this.enterCabinet() const test = new Test(this.page) const pageElement = new PageElement(this.page) await test.startSolvingTestByTitle(testTitle) while (true) { const question = await pageElement.getContent('.v-test-questions-title') if (!question) break; console.log({ questionInService: question }) await test.solveSingleAnswerQuestion() } }调整集群并发数
当前maxConcurrency: 1000过高,易导致浏览器资源耗尽引发上下文异常。降低至10-50区间,观察问题是否缓解。
内容的提问来源于stack exchange,提问作者ivsheva
相关产品推荐
相关产品推荐

