尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于Playwright与多Agent架构的网站行为流自动化还原工程实践

基于Playwright与多Agent架构的网站行为流自动化还原工程实践 1. 项目概述从“一键还原”到工程化实践最近在折腾自动化测试和爬虫项目时我遇到了一个挺有意思的挑战如何稳定、高效地“克隆”一个目标网站的行为流包括它的页面状态、交互逻辑乃至背后的数据请求。这不仅仅是简单的截图或保存HTML而是要能完整复现用户在网站上的操作路径和状态变化。传统的工具链比如Selenium配合Requests或者单纯的Playwright脚本在面对复杂的SPA单页应用、动态加载、反爬机制时总显得有些力不从心脚本脆弱、维护成本高是常态。于是我开始探索将Playwright与多智能体Multi-Agent的工程思想结合起来构建一套更健壮、更智能的自动化流程。这套流程的目标很明确给定一个目标网站URL系统能自动分析其结构规划操作路径执行交互并最终将整个过程包括中间状态、网络请求、最终渲染完整地“还原”出来形成一个可复现、可审计的报告或脚本。这听起来有点像自动化测试的逆向工程但其应用场景远不止于此比如竞品分析、工作流自动化、甚至为AI训练提供高质量的交互数据。核心的驱动力在于“工程流”。它不是一个简单的脚本而是一套包含角色分工、错误处理、状态管理和质量评估的体系。Playwright提供了强大的浏览器自动化能力而多Agent的架构思想则让我们可以将复杂的“还原”任务分解为分析、决策、执行、验证等多个子任务由不同的“智能体”各司其职协同完成。这大大提升了整个系统的鲁棒性和适应性。2. 核心架构与多Agent角色设计这套工程流的核心在于将“还原网站”这个宏观目标拆解成一系列可被独立管理和执行的微观任务。我们借鉴了软件工程和分布式系统中的Agent概念这里每个Agent并非指必须使用大语言模型驱动的AI智能体而是指一个具有特定职责、能感知环境、做出决策并执行动作的功能模块。它们通过清晰定义的接口和共享状态进行协作。2.1 四大核心Agent的职责解析在我们的设计中主要包含以下四个核心Agent它们构成了一个处理管道Pipeline1. 侦察与分析Agent (Reconnaissance Analysis Agent)这是流程的起点。它的任务是“读懂”目标网站。它接收一个初始URL然后利用Playwright打开页面但并不急于交互。它的工作包括静态结构分析快速扫描页面的DOM树识别关键的结构化元素如导航栏、主内容区、表单、列表等。它会生成一个初步的页面元素地图。动态行为嗅探监听页面加载初期及空闲时触发的网络请求XHR/Fetch分析API端点、请求参数和响应格式。这有助于理解网站的数据获取方式。技术栈推断通过检查全局变量如window.React、window.Vue、特征性的CSS类名或脚本判断网站可能使用的框架React, Vue, Angular等这对后续的元素定位策略有指导意义。初始状态快照保存页面加载后的初始HTML、CSSOM以及完整的网络请求瀑布图。这个快照是后续所有操作的“基线”。实操心得这个Agent的关键是“轻”和“快”。它不应该执行任何可能改变页面状态的操作如点击、滚动。我们通常会将Playwright的page.goto()的waitUntil参数设置为‘networkidle’并配合page.on(‘request’)和page.on(‘response’)事件监听器来捕获网络活动。分析结果会结构化成一份JSON报告传递给下一个Agent。2. 策略规划Agent (Strategy Planning Agent)拿到分析报告后这个Agent负责回答“我们应该如何与这个网站交互”它需要模拟一个真实用户的探索路径。其决策逻辑可以基于规则也可以引入简单的启发式算法或轻量级模型路径生成基于分析报告中的可交互元素按钮、链接、输入框生成一系列可能的操作序列。例如对于一个电商网站典型路径可能是“首页 - 点击某个商品分类 - 滚动浏览商品列表 - 点击进入商品详情页 - 查看商品评价”。风险评估预判某些操作可能带来的风险比如点击一个按钮可能导致弹窗、页面跳转或触发复杂的JS验证。规划时需要准备应对策略如等待弹窗出现后关闭。目标定义明确本次“还原”的深度和广度。是只还原主流程还是需要遍历所有主要标签页策略规划Agent需要根据配置制定一个清晰的、可执行的任务清单。3. 执行与交互Agent (Execution Interaction Agent)这是最“实干”的Agent直接驱动Playwright执行规划好的操作序列。它的核心要求是稳定和容错。健壮的元素定位绝不依赖脆弱的XPath或易变的CSS选择器。优先使用>import { defineConfig, devices } from playwright/test; export default defineConfig({ timeout: 120000, // 单个操作超时设为2分钟给复杂页面足够时间 use: { headless: false, // 开发调试时可设为false生产环境为true viewport: { width: 1920, height: 1080 }, // 固定视口保证一致性 ignoreHTTPSErrors: true, screenshot: only-on-failure, // 协调器会管理自己的截图逻辑这里仅备灾 video: off, // 视频录制开销大通常由我们的状态捕获替代 trace: off, // 同理使用自定义追踪 userAgent: Mozilla/5.0 ... (一个常见的桌面浏览器UA), // 模拟真实用户 }, // 可以配置多个项目来模拟不同浏览器环境 projects: [ { name: chromium, use: { ...devices[Desktop Chrome] } } ], });3.2 侦察分析Agent的深度实现分析Agent的代码核心在于信息收集的全面性和非侵入性。以下是一个简化的示例// src/agents/analyzer/index.ts import { Page, Browser } from playwright; import { AnalysisResult } from ../../core/types; export class AnalyzerAgent { async analyze(page: Page, url: string): PromiseAnalysisResult { const result: AnalysisResult { url, timestamp: new Date().toISOString(), initialDom: , networkActivities: [], interactiveElements: [], techStackHints: [], }; // 1. 导航并等待网络空闲 await page.goto(url, { waitUntil: networkidle, timeout: 60000 }); // 2. 捕获初始DOM清理敏感信息 result.initialDom await this.sanitizeDom(await page.content()); // 3. 监听并记录网络请求在goto之前就开始监听 page.on(request, request { if (this.isRelevantRequest(request)) { result.networkActivities.push({ url: request.url(), method: request.method(), resourceType: request.resourceType(), // 注意不记录postData等可能包含敏感信息的字段 }); } }); // 4. 识别可交互元素使用更稳健的选择器 const buttons await page.locator(button, [rolebutton], a, input, select, textarea).all(); for (const element of buttons.slice(0, 50)) { // 限制数量避免过多 const tag await element.evaluate(el el.tagName.toLowerCase()); const text await element.innerText().catch(() ); const placeholder await element.getAttribute(placeholder); const id await element.getAttribute(id); result.interactiveElements.push({ tag, text: text?.trim(), placeholder, id }); } // 5. 技术栈推断启发式 const techDetect await page.evaluate(() { const hints []; if (window.React) hints.push(React); if (window.Vue) hints.push(Vue); if (window.angular) hints.push(Angular); // 检查特定类名或属性 if (document.querySelector([data-reactroot], [data-reactid])) hints.push(React (by DOM)); return hints; }); result.techStackHints techDetect; return result; } private isRelevantRequest(request: any): boolean { const ignoreTypes [image, stylesheet, font, media]; return !ignoreTypes.includes(request.resourceType()); } private sanitizeDom(html: string): string { // 简易的DOM清理移除脚本、样式、表单值等可根据需要扩展 // 这里可以使用类似cheerio的库进行更精细的操作 return html.replace(/script\b[^]*(?:(?!\/script)[^]*)*\/script/gi, ) .replace(/style\b[^]*(?:(?!\/style)[^]*)*\/style/gi, ); } }3.3 执行交互Agent的容错与状态捕获执行Agent是稳定性的关键。我们必须假设页面环境是不稳定的。// src/agents/executor/index.ts import { Page, Locator } from playwright; import { PlannedAction, StepSnapshot } from ../../core/types; import { Logger } from ../../utils/logger; export class ExecutorAgent { constructor(private page: Page, private logger: Logger) {} async executeAction(action: PlannedAction, stepIndex: number): PromiseStepSnapshot { const snapshot: StepSnapshot { step: stepIndex, action, before: {}, after: {} }; const { selector, actionType, value } action; // 步骤A执行前状态捕获 snapshot.before await this.capturePageState(Step ${stepIndex} - Before); try { const locator this.resolveLocator(selector); await locator.waitFor({ state: visible, timeout: 30000 }); switch (actionType) { case click: await locator.click({ delay: 100 }); // 加入微小延迟模拟真人 break; case fill: await locator.fill(value || ); break; case select: await locator.selectOption(value || ); break; case scroll: await this.page.mouse.wheel(0, parseInt(value || 500)); break; // ... 其他操作类型 } // 步骤B等待操作后的副作用稳定这是避免Flaky测试的核心 await this.waitForStability(); // 步骤C执行后状态捕获 snapshot.after await this.capturePageState(Step ${stepIndex} - After); snapshot.success true; } catch (error) { this.logger.error(Step ${stepIndex} failed: ${error.message}); snapshot.success false; snapshot.error error.message; // 即使失败也尝试捕获失败后的状态 snapshot.after await this.capturePageState(Step ${stepIndex} - Error State); // 根据策略决定是重试、跳过还是终止流程 } return snapshot; } private async capturePageState(label: string) { const state { domHash: , // 可以计算DOM的哈希值用于快速比较 screenshotPath: , networkSinceLastStep: [], }; // 1. 获取并清理DOM const rawHtml await this.page.content(); state.domHash this.computeHash(this.sanitizeDom(rawHtml)); // 2. 截图仅针对视口或特定区域 state.screenshotPath ./outputs/snapshots/${label.replace(/[^a-z0-9]/gi, _)}.png; await this.page.screenshot({ path: state.screenshotPath, fullPage: false }); // 3. 记录此阶段新增的网络请求需要与全局网络监听器配合 // ... 实现略 return state; } private async waitForStability(timeout 10000) { // 实现一个自定义的“稳定”等待例如 // - 等待没有新的网络请求持续1秒 // - 等待某个加载指示器消失 // - 或者简单等待一个固定时间最后手段 await this.page.waitForTimeout(1000); // 基础等待 // 更高级的实现可以监听 networkidle 或特定元素状态 } private resolveLocator(selector: string): Locator { // 智能定位器解析可以支持多种选择器格式 // 例如如果selector以text开头使用 page.getByText if (selector.startsWith(text)) { return this.page.getByText(selector.substring(5)); } // 默认使用CSS选择器 return this.page.locator(selector); } }4. 策略规划Agent的智能决策逻辑规划Agent是这套流程的“大脑”。最简单的实现是基于配置的规则模板。例如针对常见网站类型博客、电商、后台管理系统预定义操作模板。更高级的实现可以引入轻量级的决策逻辑基于元素优先级的探索给“登录按钮”、“搜索框”、“查看更多链接”这类元素赋予更高的操作优先级。广度优先搜索BFS将页面视为图可点击元素是边新页面/状态是节点进行有限深度的遍历。与LLM结合进阶将分析Agent的报告发送给大语言模型如通过API调用GPT-4提示它“作为一个用户请规划探索这个网站的5个关键步骤”。LLM可以生成更接近人类、更语义化的操作序列如“点击右上角的头像然后选择‘个人设置’菜单”。然后我们需要一个翻译层将这些自然语言描述转换回Playwright可执行的定位器和操作指令。// 一个简单的基于规则的规划器示例 // src/agents/planner/rule-based-planner.ts import { AnalysisResult, PlannedAction } from ../../core/types; export class RuleBasedPlanner { generatePlan(analysis: AnalysisResult, config: any): PlannedAction[] { const plan: PlannedAction[] []; const { interactiveElements, url } analysis; // 规则1如果页面有搜索框先执行一次搜索使用配置的关键词 const searchInput interactiveElements.find(el el.tag input (el.placeholder?.toLowerCase().includes(搜索) || el.id?.includes(search))); if (searchInput config.defaultSearchTerm) { plan.push({ selector: input[placeholder*${searchInput.placeholder}], // 简化示例 actionType: fill, value: config.defaultSearchTerm, description: 在搜索框输入“${config.defaultSearchTerm}” }); plan.push({ selector: button[typesubmit], actionType: click, description: 点击搜索按钮 }); } // 规则2点击第一个看起来是主要内容的链接非导航、非页脚 const mainLinks interactiveElements.filter(el el.tag a el.text el.text.length 5); if (mainLinks.length 0) { plan.push({ selector: text${mainLinks[0].text}, actionType: click, description: 点击链接“${mainLinks[0].text}” }); } // 规则3向下滚动以加载更多内容针对无限滚动页面 plan.push({ selector: body, actionType: scroll, value: 2000, description: 向下滚动页面 }); return plan.slice(0, config.maxSteps || 10); // 限制最大步骤数 } }5. 验证、汇编与最终输出所有步骤执行完毕后验证与汇编Agent开始工作。它的任务不仅是打包更是质量守门员。// src/agents/verifier/index.ts import { ExecutionResult, StepSnapshot, FinalReport } from ../../core/types; import * as fs from fs/promises; import * as path from path; export class VerifierAgent { async verifyAndAssemble(result: ExecutionResult, outputDir: string): PromiseFinalReport { const report: FinalReport { summary: { totalSteps: result.snapshots.length, successfulSteps: 0, failedSteps: 0 }, startUrl: result.startUrl, startTime: result.startTime, endTime: new Date().toISOString(), snapshots: [], analysis: result.initialAnalysis, }; for (const snapshot of result.snapshots) { report.snapshots.push({ ...snapshot, // 可以在这里计算前后状态的差异度例如DOM哈希值是否变化 domChanged: snapshot.before.domHash ! snapshot.after?.domHash, }); if (snapshot.success) { report.summary.successfulSteps; } else { report.summary.failedSteps; } } // 生成可读的HTML报告 await this.generateHtmlReport(report, outputDir); // 生成结构化的JSON日志便于其他程序分析 await fs.writeFile( path.join(outputDir, execution-log.json), JSON.stringify(report, null, 2) ); return report; } private async generateHtmlReport(report: FinalReport, outputDir: string) { // 这是一个简化的示例实际可以生成非常丰富的报告 const html !DOCTYPE html html headtitle网站还原报告 - ${report.startUrl}/titlestyle/* 样式省略 *//style/head body h1还原报告/h1 p目标URL: ${report.startUrl}/p p成功率: ${report.summary.successfulSteps}/${report.summary.totalSteps}/p hr ${report.snapshots.map(s div classstep ${s.success ? success : failed} h3步骤 ${s.step}: ${s.action?.description}/h3 p状态: ${s.success ? 成功 : 失败}/p ${s.before.screenshotPath ? img src${path.basename(s.before.screenshotPath)} width400 : } ${s.after?.screenshotPath ? img src${path.basename(s.after.screenshotPath)} width400 : } /div ).join()} /body /html; await fs.writeFile(path.join(outputDir, report.html), html); // 同时需要将截图文件复制到outputDir下 } }6. 常见问题、排查技巧与进阶优化在实际搭建和运行这套工程流时你会遇到不少挑战。以下是我踩过坑后总结的一些核心要点。6.1 元素定位失败稳定性第一生命线这是自动化脚本的头号杀手。我们的执行Agent已经采用了稳健的定位策略但还需要更多防御性编程。问题页面元素加载慢、动态生成、被遮挡、或选择器因前端改动而失效。排查与解决优先使用面向用户的定位器Playwright推荐的page.getByRole(),page.getByText(),page.getByLabel()是首选。它们基于可访问性属性比CSS选择器稳定得多。组合定位不要依赖单一的属性。可以组合角色、文本和邻近元素。例如page.getByRole(button).filter({ hasText: Submit }).and(page.locator(.primary))。自定义测试属性如果目标网站是你可控的强烈建议推动开发团队为关键交互元素添加>// 生成的Playwright脚本示例 import { test, expect } from playwright/test; test(replicated-flow-from-2024-05-27, async ({ page }) { await page.goto(https://target-website.com); await page.getByPlaceholder(搜索商品...).fill(笔记本电脑); await page.getByRole(button, { name: 搜索 }).click(); await page.locator(.product-item:first-child).click(); // ... 更多步骤 });这样你就实现了从“记录”到“生成测试用例”的闭环极大地提升了编写端到端E2E测试的效率。构建这样一套Playwright结合多Agent思想的工程流初期投入确实比写一个直线脚本要大。但当你需要处理数十个、上百个不同结构的网站或者对一个网站进行长期、稳定的监控和回归验证时它的模块化、可维护性和鲁棒性优势就会淋漓尽致地展现出来。它不再是一个脆弱的“脚本”而是一个真正意义上的“工程系统”。
返回列表