1. 项目概述当Playwright遇上MCP网页抓取进入“动口不动手”时代最近在折腾AI驱动的自动化项目发现一个特别有意思的组合Playwright和MCP。如果你还在为写复杂的网页抓取脚本而头疼或者觉得传统的爬虫框架在面对动态渲染、反爬策略时力不从心那这个组合绝对值得你花5分钟了解一下。简单来说它让你能用自然语言告诉AI“帮我抓取这个页面的商品列表”然后AI就能理解你的意图并调用Playwright去执行最后把结构化的数据返回给你。整个过程你几乎不需要写一行传统的定位元素、解析HTML的代码。这背后的核心是MCPModel Context Protocol。你可以把它理解为一个“翻译官”或“中间件”。大语言模型比如Claude、GPT本身并不知道如何操作浏览器但通过MCP我们可以将Playwright这类工具的能力“暴露”给AI。AI接收到你的自然语言指令后会通过MCP协议去调用对应的工具函数比如navigate_to_page,extract_dataPlaywright则负责忠实执行这些底层操作。这样一来抓取逻辑的“思考”部分交给了更擅长理解语义的AI而精准的“执行”部分则交给了强大的Playwright两者强强联合。这个项目非常适合以下几类朋友一是希望快速验证某个数据抓取想法不想在编码上耗费太多时间的产品经理或业务分析师二是需要处理大量异构、动态网页但团队前端自动化经验不足的开发者三是任何对AI应用落地感兴趣想亲手搭建一个“动口不动手”的智能体Agent的极客。接下来我就带你从零开始拆解这套方案的核心思路、详细实现以及我趟过的那些坑。2. 核心思路与架构拆解为什么是Playwright MCP在深入代码之前我们得先搞清楚为什么选这两个技术以及它们是如何协同工作的。这决定了整个项目的稳定性和扩展性。2.1 Playwright的优势与在数据抓取中的角色Playwright是一个由微软开源的浏览器自动化库。在数据抓取领域它相比传统的RequestsBeautifulSoup或者Selenium有几个压倒性优势无头浏览器支持与真实渲染Playwright可以启动真正的Chromium、Firefox或WebKit浏览器包括无头模式能完美执行JavaScript渲染出与用户肉眼所见完全一致的页面。这对于抓取依赖前端框架如React、Vue动态生成内容的网站至关重要。自动等待与稳定性它内置了智能等待机制可以等待元素出现、网络请求完成甚至动画结束再执行下一步操作。这大大减少了因页面加载速度不稳定而导致的脚本失败是我用过最稳定的浏览器自动化工具之一。强大的选择器与录制功能支持CSS、XPath、文本内容等多种定位方式而且DevTools里的录制功能可以快速生成操作脚本为后续的AI指令生成提供了丰富的操作范例。多上下文与隔离可以为每次抓取创建独立的浏览器上下文实现Cookie、本地存储的完全隔离避免数据污染也更容易模拟不同的用户会话。在这个项目中Playwright扮演的是“执行者”和“探索者”的角色。它不负责理解“商品列表”是什么但它能精准地执行“点击这个按钮”、“滚动到页面底部”、“获取这个CSS选择器下的所有文本”等原子操作。2.2 MCP协议连接AI大脑与工具手的关键桥梁MCP即模型上下文协议是Anthropic提出的一种开放协议用于让大语言模型安全、结构化地使用外部工具和资源。你可以把它想象成一套标准的“插座和插头”规范。Server服务器提供工具的一方。在这里我们将Playwright的操作打开浏览器、导航、抓取数据封装成一个个标准的工具函数并启动一个MCP服务器来暴露这些函数。Client客户端使用工具的一方。通常是集成了MCP客户端的AI应用比如Claude Desktop、Cursor IDE或者我们自己写的AI Agent程序。客户端会向服务器查询有哪些工具可用并在需要时调用它们。协议通信Client和Server通过JSON-RPC over stdio标准输入输出或SSE服务器发送事件进行通信。AI模型通过Client发出结构化请求Server执行后返回结果。我们的核心工作就是创建一个MCP Server将Playwright的能力“工具化”。例如我们创建一个名为scrape_website的工具它接受url和instruction自然语言指令两个参数。AI模型收到用户请求后会决定调用这个工具并生成相应的参数。我们的Server收到调用请求后则启动Playwright解析instruction执行相应的浏览器操作来完成任务。2.3 整体架构设计整个系统的数据流是这样的用户在AI客户端如Claude聊天窗口输入“帮我抓取知乎热榜的前10个问题标题和链接。”AI客户端MCP Client识别出这是一个需要调用外部工具的任务。它向已连接的我们的Playwright MCP Server查询可用的工具列表。Server返回工具列表其中包含我们定义的scrape_with_instruction工具及其参数说明。AI模型根据工具描述和用户指令构造一个JSON-RPC请求调用scrape_with_instruction参数为{“url”: “https://www.zhihu.com/hot”, “instruction”: “获取热榜列表中前10个条目的标题文本和其对应的详情页面链接”}。Playwright MCP Server收到请求解析参数。它启动一个Playwright浏览器实例导航到知乎热榜页面。在浏览器中Server需要“理解”instruction。这里有两种策略策略AAI辅助解析Server内部可以调用一个轻量级的本地AI模型或一次远程API调用将指令翻译成具体的Playwright操作步骤序列如定位类名为HotList-list的元素取其下前10个section元素在每个section内提取h2元素的文本作为标题提取a元素的href属性作为链接。策略B预定义模板针对常见指令如“抓取列表”、“提取表格”预定义好操作步骤。本例中我们可以预设对于“获取列表前N个条目的标题和链接”这类指令使用通用的CSS选择器提取模式。Server按照解析出的步骤控制Playwright执行操作并将提取到的数据组装成结构化的JSON格式。Server将结果通过MCP协议返回给AI客户端。AI客户端将结构化的数据呈现给用户或者由AI模型进一步总结、润色后输出。这个架构的关键在于解耦AI负责意图理解和高级规划MCP负责协议转换和工具调度Playwright负责底层精准执行。任何一部分都可以独立升级或替换。3. 环境准备与核心工具链搭建工欲善其事必先利其器。我们先来把开发环境搭好这里我会列出具体的版本和选择理由避免你踩坑。3.1 基础环境配置首先确保你的机器上安装了Node.js (版本18及以上)和Python (版本3.8及以上)。Node.js是运行Playwright和JavaScript版MCP Server的基石而Python生态中有丰富的AI模型库和工具方便我们后续做指令解析。# 检查Node.js和npm版本 node --version npm --version # 检查Python和pip版本 python --version pip --version为什么选择Node.jsPlaywright对Node.js的支持是最原生、最全面的其异步编程模型与浏览器自动化任务高度契合。虽然Playwright也有Python版本但考虑到MCP协议相关的工具和示例在JS/TS生态中更活跃我们优先使用Node.js环境。3.2 创建项目并安装核心依赖我们创建一个新的项目目录并初始化npm项目。mkdir playwright-mcp-scraper cd playwright-mcp-scraper npm init -y接下来安装最核心的三个依赖npm install playwright modelcontextprotocol/sdk axiosplaywright浏览器自动化库本体。modelcontextprotocol/sdkAnthropic官方提供的MCP SDK for Node.js。它提供了构建MCP Server和Client所需的所有类型和工具函数能极大简化开发。axios一个流行的HTTP客户端。如果我们在Server内部采用“策略A”调用AI API来解析指令会用到它。如果你选择纯“策略B”则可以暂时不装。安装浏览器Playwright需要下载它自己管理的浏览器二进制文件以保证环境一致性。npx playwright install chromium这里我推荐只安装Chromium因为它最轻量、兼容性也足够好。如果项目明确需要测试Firefox或WebKit的渲染差异再按需安装。3.3 构建MCP Server骨架MCP Server的核心是实现Server类并定义工具Tool和资源Resource。我们首先创建一个server.js文件搭建基础骨架。// server.js const { Server } require(modelcontextprotocol/sdk/server/index.js); const { StdioServerTransport } require(modelcontextprotocol/sdk/server/stdio.js); const { Tool } require(modelcontextprotocol/sdk/types.js); // 1. 创建Server实例指定名称和版本 const server new Server( { name: playwright-scraper, version: 1.0.0, }, { capabilities: { tools: {}, // 声明我们支持工具 }, } ); // 2. 定义我们的核心工具根据指令抓取网页 const scrapeTool new Tool( scrape_with_instruction, 根据自然语言指令从指定URL抓取结构化数据。, { type: object, properties: { url: { type: string, description: 要抓取的目标网页URL, }, instruction: { type: string, description: 用自然语言描述需要抓取什么数据例如“获取产品价格列表”或“提取文章标题和发布时间”, }, // 可以添加更多参数如等待时间、是否需要截图等 headless: { type: boolean, description: 是否以无头模式运行浏览器不显示界面, default: true, } }, required: [url, instruction], } ); // 3. 设置工具处理函数这里先留空下一节实现 server.setRequestHandler(tools/call, async (request) { // request.params 包含了工具名和调用参数 const { name, arguments: args } request.params; if (name scrape_with_instruction) { // 这里将调用Playwright执行任务 const result await handleScrapeInstruction(args.url, args.instruction, args.headless); return { content: [ { type: text, text: JSON.stringify(result, null, 2), // 将结果格式化为JSON字符串 }, ], }; } throw new Error(Unknown tool: ${name}); }); // 4. 在初始化时向客户端宣告我们提供的工具 server.setRequestHandler(tools/list, async () ({ tools: [scrapeTool], })); // 5. 启动Server使用stdio传输这是与AI客户端通信的标准方式 async function runServer() { const transport new StdioServerTransport(); await server.connect(transport); console.error(Playwright MCP Server is running on stdio...); } runServer().catch((error) { console.error(Server error:, error); process.exit(1); }); // 工具处理函数待实现 async function handleScrapeInstruction(url, instruction, headless true) { // 下一节将在这里填充Playwright逻辑 return { message: Will scrape ${url} with instruction: ${instruction} }; }这个骨架代码已经是一个可运行的MCP Server了。它定义了一个工具并准备好了处理调用的框架。现在我们需要用Playwright的魔力来填充handleScrapeInstruction函数。4. 核心抓取引擎实现让Playwright听懂指令这是整个项目最核心的部分。我们需要在handleScrapeInstruction函数中实现从“自然语言指令”到“Playwright操作序列”的转换并执行抓取。4.1 实现基础的Playwright抓取流程我们先实现一个不考虑复杂指令、只做简单页面访问和全文抓取的版本确保Playwright和MCP能联通。// 在 server.js 顶部引入playwright const { chromium } require(playwright); async function handleScrapeInstruction(url, instruction, headless true) { let browser; let page; try { // 1. 启动浏览器 browser await chromium.launch({ headless }); // 创建一个新的浏览器上下文实现会话隔离 const context await browser.newContext({ viewport: { width: 1920, height: 1080 }, userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 }); page await context.newPage(); // 2. 导航到目标URL并等待网络空闲状态 console.error(Navigating to ${url}); await page.goto(url, { waitUntil: networkidle }); // 3. 简单的指令解析演示如果指令包含“截图”就截图 if (instruction.toLowerCase().includes(screenshot)) { const screenshotBuffer await page.screenshot({ fullPage: true }); // 注意MCP协议目前主要传递文本。对于二进制数据可以转base64或保存到文件返回路径。 return { success: true, message: Screenshot taken., data: { screenshot: screenshotBuffer.toString(base64) } }; } // 4. 默认行为获取页面标题和整个body的粗略文本 const title await page.title(); const bodyText await page.textContent(body); return { success: true, url, instruction, data: { title, // 只取前1000字符预览避免返回数据过大 bodyTextPreview: bodyText.substring(0, 1000) (bodyText.length 1000 ? ... : ) } }; } catch (error) { console.error(Scraping error:, error); return { success: false, error: error.message }; } finally { // 5. 确保浏览器被关闭释放资源 if (browser) { await browser.close(); } } }现在我们的Server已经具备了最基础的能力。你可以用以下方式测试它启动Servernode server.js它会在标准输入输出上等待连接。我们需要一个MCP Client来测试。一个简单的方法是使用MCP SDK自带的测试工具或者更直观的使用支持MCP的AI客户端如配置了该Server的Claude Desktop。4.2 进阶实现指令解析与智能抓取基础版本只是“听令”打开网页。要实现“智能抓取”核心是解析instruction参数。这里我们探讨两种实战策略。策略A集成轻量级AI进行指令解析推荐用于复杂场景我们可以在Server内部将用户的自然语言指令转换为一个具体的“操作清单”。这个清单可以描述为一系列步骤例如[{action: scroll, selector: footer}, {action: get_elements, selector: .product-item, limit: 5, extract: {title: .title, price: .price}}]为了实现这个转换我们需要一个文本理解模型。为了速度和成本我们可以使用本地的小模型或者调用大模型的API。这里以调用OpenAI的GPT-3.5 API为例你需要准备OPENAI_API_KEY// 在文件顶部引入axios如果之前没装需要 npm install axios const axios require(axios); async function parseInstructionToPlan(instruction, url) { const prompt 你是一个网页抓取专家。请将用户的抓取指令转化为一个具体的JSON操作序列。 目标网页URL: ${url} 用户指令: ${instruction} 操作类型包括 1. navigate: 导航到某个URL如果指令包含新链接。 2. click: 点击某个元素。需要提供selector。 3. scroll: 滚动页面。可以指定方向或目标selector。 4. wait: 等待。可以指定时间毫秒或等待某个selector出现。 5. extract: 提取数据。需要提供目标selector以及要提取的字段映射字段名: 子selector或属性名。 6. screenshot: 截图。 请只输出一个合法的JSON数组每个元素是一个操作对象。 示例 [ {action: scroll, selector: footer}, {action: wait, milliseconds: 1000}, {action: extract, selector: .article-list li, limit: 10, fields: {title: h2, link: ahref}} ] ; try { const response await axios.post( https://api.openai.com/v1/chat/completions, { model: gpt-3.5-turbo, messages: [{ role: user, content: prompt }], temperature: 0.1, // 低温度保证输出格式稳定 }, { headers: { Authorization: Bearer ${process.env.OPENAI_API_KEY}, Content-Type: application/json, }, } ); const content response.data.choices[0].message.content; // 从返回的文本中解析出JSON数组 const jsonMatch content.match(/\[[\s\S]*\]/); if (jsonMatch) { return JSON.parse(jsonMatch[0]); } throw new Error(Failed to parse AI response as JSON); } catch (error) { console.error(AI parsing error:, error); // 解析失败时返回一个保守的默认计划只提取页面所有文本 return [{ action: extract, selector: body, fields: { fullText: _text } }]; } }然后我们需要升级handleScrapeInstruction函数来执行这个“操作计划”async function handleScrapeInstruction(url, instruction, headless true) { let browser; let page; try { browser await chromium.launch({ headless }); const context await browser.newContext(); page await context.newPage(); await page.goto(url, { waitUntil: networkidle }); // 解析指令为操作计划 const plan await parseInstructionToPlan(instruction, url); console.error(Execution plan:, JSON.stringify(plan)); const results []; // 执行计划中的每个操作 for (const step of plan) { await executeStep(page, step, results); } return { success: true, url, instruction, plan, // 返回执行计划便于调试 data: results }; } catch (error) { console.error(Scraping error:, error); return { success: false, error: error.message }; } finally { if (browser) await browser.close(); } } // 执行单个步骤的函数 async function executeStep(page, step, results) { switch (step.action) { case click: if (step.selector) { await page.click(step.selector, { timeout: 5000 }); await page.waitForLoadState(networkidle); } break; case scroll: if (step.selector) { // 滚动到特定元素 await page.locator(step.selector).scrollIntoViewIfNeeded(); } else if (step.direction bottom) { // 滚动到底部 await page.evaluate(() window.scrollTo(0, document.body.scrollHeight)); } if (step.milliseconds) { await page.waitForTimeout(step.milliseconds); } break; case wait: if (step.selector) { await page.waitForSelector(step.selector, { timeout: 10000 }); } else if (step.milliseconds) { await page.waitForTimeout(step.milliseconds); } break; case extract: if (step.selector) { const elements await page.locator(step.selector).all(); const limit step.limit || elements.length; for (let i 0; i Math.min(elements.length, limit); i) { const element elements[i]; const item {}; for (const [fieldName, fieldSpec] of Object.entries(step.fields || {})) { if (fieldSpec _text) { // 提取元素文本 item[fieldName] await element.textContent(); } else if (typeof fieldSpec string fieldSpec.includes()) { // 提取属性如 ahref const [subSelector, attr] fieldSpec.split(); const targetElem subSelector ? element.locator(subSelector).first() : element; item[fieldName] await targetElem.getAttribute(attr); } else if (typeof fieldSpec string) { // 提取子元素的文本 item[fieldName] await element.locator(fieldSpec).first().textContent(); } } results.push(item); } } break; case screenshot: const screenshotBuffer await page.screenshot({ fullPage: step.fullPage }); results.push({ screenshot: screenshotBuffer.toString(base64) }); break; default: console.warn(Unknown action: ${step.action}); } }策略B基于预定义规则和选择器的快速解析如果觉得调用AI API有延迟、成本或隐私顾虑或者你的抓取目标相对固定可以采用规则匹配的方式。例如我们可以内置一些常见指令模式function parseInstructionByRule(instruction) { const lowerInstruction instruction.toLowerCase(); // 规则1抓取列表 if (lowerInstruction.includes(list) || lowerInstruction.includes(前)) { const match lowerInstruction.match(/前\s*(\d)/); const limit match ? parseInt(match[1], 10) : 10; // 这里需要根据网站结构预设或动态探测选择器这是一个难点 // 可以尝试常见的选择器模式如 .list-item, li, article, tr return { action: extract_list, itemSelector: .list-item, li, article, tr, // 尝试性选择器 fields: { text: _text }, limit }; } // 规则2抓取表格 else if (lowerInstruction.includes(table)) { return { action: extract_table, tableSelector: table }; } // 默认规则提取所有段落文本 else { return { action: extract_text, selector: p, h1, h2, h3, h4 }; } }规则引擎的优点是速度快、零成本但灵活性和准确性远不如AI解析。在实际项目中我通常采用“混合策略”先尝试用规则匹配常见、明确的指令对于规则无法覆盖的复杂指令再降级到使用AI解析。这样能在成本和效果间取得平衡。5. 完整代码整合与配置我们将上述所有模块整合到一个完整的、可运行的server.js文件中。同时为了便于使用我们还需要一个配置文件来管理API密钥等敏感信息。项目结构playwright-mcp-scraper/ ├── server.js # 主服务器文件 ├── config.json # 配置文件需自行创建不要提交到git ├── package.json └── node_modules/config.json{ openaiApiKey: 你的OpenAI API密钥, defaultHeadless: true, userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ... }完整的 server.jsconst { Server } require(modelcontextprotocol/sdk/server/index.js); const { StdioServerTransport } require(modelcontextprotocol/sdk/server/stdio.js); const { Tool } require(modelcontextprotocol/sdk/types.js); const { chromium } require(playwright); const axios require(axios); const config require(./config.json); // 加载配置 const server new Server( { name: playwright-mcp-scraper, version: 1.0.0 }, { capabilities: { tools: {} } } ); // 定义工具 const scrapeTool new Tool( scrape_with_instruction, 根据自然语言指令从指定URL抓取结构化数据。支持点击、滚动、等待、提取等多种操作。, { type: object, properties: { url: { type: string, description: 目标网页URL }, instruction: { type: string, description: 自然语言指令如“获取前10条新闻的标题和链接” }, headless: { type: boolean, description: 无头模式, default: config.defaultHeadless }, timeout: { type: number, description: 全局超时时间毫秒, default: 30000 } }, required: [url, instruction], } ); // AI解析指令函数 async function parseInstructionToPlan(instruction, url) { // 此处省略与上一节代码相同。实际使用时请将上一节的 parseInstructionToPlan 函数复制过来。 // 注意这里需要读取 config.openaiApiKey } // 执行步骤函数 async function executeStep(page, step, results) { // 此处省略与上一节代码相同。实际使用时请将上一节的 executeStep 函数复制过来。 } // 主处理函数 async function handleScrapeInstruction(url, instruction, headless true, timeout 30000) { let browser; let page; try { browser await chromium.launch({ headless }); const context await browser.newContext({ viewport: { width: 1920, height: 1080 }, userAgent: config.userAgent }); page await context.newPage(); // 设置页面超时 page.setDefaultTimeout(timeout); await page.goto(url, { waitUntil: networkidle, timeout }); const plan await parseInstructionToPlan(instruction, url); console.error(Execution plan:, JSON.stringify(plan)); const results []; for (const step of plan) { await executeStep(page, step, results); } return { success: true, url, instruction, plan, data: results }; } catch (error) { console.error(Scraping error:, error); return { success: false, error: error.message }; } finally { if (browser) await browser.close(); } } // 设置MCP Server处理器 server.setRequestHandler(tools/list, async () ({ tools: [scrapeTool], })); server.setRequestHandler(tools/call, async (request) { const { name, arguments: args } request.params; if (name scrape_with_instruction) { const result await handleScrapeInstruction( args.url, args.instruction, args.headless ! undefined ? args.headless : config.defaultHeadless, args.timeout ); return { content: [{ type: text, text: JSON.stringify(result, null, 2) }], }; } throw new Error(Unknown tool: ${name}); }); // 启动服务器 async function runServer() { const transport new StdioServerTransport(); await server.connect(transport); console.error(Playwright MCP Scraper Server is running...); } runServer().catch((error) { console.error(Server fatal error:, error); process.exit(1); });运行与测试创建config.json文件并填入你的OpenAI API密钥。安装依赖npm install启动服务器node server.js服务器现在正在标准输入输出上等待连接。你需要一个MCP客户端来调用它。6. 连接AI客户端在Claude Desktop中实际使用让我们的Server发挥价值的最后一步是让它被AI助手调用。这里以Claude Desktop为例展示如何配置。找到Claude Desktop的配置目录macOS:~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:%APPDATA%\Claude\claude_desktop_config.jsonLinux:~/.config/Claude/claude_desktop_config.json编辑配置文件在mcpServers部分添加我们的服务器配置。{ mcpServers: { playwright-scraper: { command: node, args: [ /ABSOLUTE/PATH/TO/YOUR/PROJECT/playwright-mcp-scraper/server.js ], env: { OPENAI_API_KEY: 你的OpenAI API密钥 // 也可以在这里设置环境变量 } } } }注意command也可以是npmargs可以是[run, start]前提是你在package.json中配置了scripts: { start: node server.js }。关键是必须使用绝对路径。重启Claude Desktop。开始对话重启后在Claude的聊天框中你可以直接说“使用scrape_with_instruction工具抓取豆瓣电影Top250第一页的电影名称和评分URL是 https://movie.douban.com/top250”。Claude会识别出可用的工具并生成类似下面的调用请求你看不到这个过程最终将结构化的JSON结果返回并展示给你。7. 实战优化与避坑指南在实际部署和使用中你会遇到各种问题。以下是我从多次实践中总结出的关键点和避坑技巧。7.1 性能与稳定性优化浏览器实例复用频繁启动关闭浏览器开销巨大。可以修改Server使用一个浏览器实例池。对于每个请求从池中取出一个上下文Context来创建新页面用完后关闭页面但保留上下文实现会话隔离和性能提升。设置合理的超时与等待networkidle等待虽然稳但某些页面可能有长期活跃的连接如WebSocket。需要设置全局超时如30秒并为关键操作如waitForSelector单独设置更短的超时避免单个页面卡死整个流程。处理反爬机制User-Agent使用常见的浏览器UA并定期更新。指纹识别Playwright启动的浏览器有默认指纹。高级反爬会检测。可以通过browser.newContext()时传入viewport,locale,timezoneId等参数进行一定程度的伪装但无法完全模拟真实用户环境的复杂性。频率限制在Server端实现请求队列和延迟控制避免对同一域名短时间发起大量请求。错误处理与重试网络波动、元素加载失败很常见。在executeStep函数中为每个操作特别是click和waitForSelector添加try-catch并实现简单的重试逻辑如重试2次能显著提升鲁棒性。7.2 指令解析的准确性提升提供网站结构提示在调用AI解析指令时可以将目标页面的部分关键HTML结构或已知的选择器作为上下文提供给AI。例如先让Playwright获取页面body的innerHTML的前几千个字符连同指令一起发送给AI。这能极大提高AI生成的选择器的准确性。后处理与验证AI生成的选择器可能无效。在执行提取操作前可以用page.locator(selector).count()检查元素是否存在。如果不存在可以尝试一些备用的通用选择器或者将错误信息反馈给用户/AI进行下一轮交互修正。定义清晰的指令范式引导用户使用更结构化的指令比如“以表格形式提取”、“提取所有图片的src属性”、“点击‘加载更多’按钮直到不再出现新内容然后提取全部列表”。在工具描述中给出明确示例能减少歧义。7.3 安全与资源管理输入验证与沙箱务必对用户输入的url进行验证防止恶意URL如file://协议或SSRF攻击。可以考虑将浏览器运行在独立的Docker容器或安全沙箱中。资源限制限制单个任务的最大运行时间、最大内存使用和可访问的域名。防止恶意指令导致无限循环或访问内部网络。敏感信息API密钥等绝对不要硬编码在代码中。使用config.json或环境变量管理并将config.json加入.gitignore。7.4 扩展性设计更多工具除了抓取可以暴露更多Playwright能力作为独立工具如screenshot_page: 对页面截图。run_script: 在页面上下文中执行自定义JavaScript代码。get_network_logs: 获取页面加载过程中的网络请求信息对于分析API接口非常有用。会话管理实现create_session和close_session工具允许跨多个工具调用保持同一个浏览器会话如登录状态。结果缓存对于相同的URL和指令可以将结果缓存一段时间减少重复抓取提升响应速度。8. 常见问题排查与调试技巧即使按照步骤操作也难免会遇到问题。这里列出一些常见错误和解决方法。问题1启动Server后Claude Desktop无法连接或找不到工具。检查点Claude Desktop配置文件中args的路径是否是绝对路径运行node server.js命令时是否在项目目录下控制台是否打印了启动日志检查Claude Desktop的日志文件通常在配置目录同级看是否有连接错误信息。确保package.json中依赖已正确安装 (node_modules存在)。问题2AI解析指令时返回错误或非JSON内容。检查点OPENAI_API_KEY环境变量或配置文件是否正确设置且有效尝试在parseInstructionToPlan函数中打印AI API的请求和响应查看返回的原始内容。调整提示词Prompt使其更严格地要求输出JSON。可以增加“你必须只输出JSON数组不要有任何其他解释文字”这样的指令。考虑使用更高性能的模型如gpt-4或专门微调过的模型来提升解析准确率。问题3Playwright执行时元素找不到或操作失败。调试技巧将headless参数设为false让浏览器窗口显示出来直观地看到脚本执行到了哪一步页面状态如何。在关键步骤前后添加page.screenshot({ path: debug-step1.png })将截图保存到本地查看。使用page.evaluate(() debugger)在浏览器开发者工具中暂停脚本手动检查DOM。检查AI生成的选择器是否合理。可以在无头模式下用page.locator(selector).count()验证元素数量。问题4抓取速度慢。优化方向启用浏览器实例/上下文复用。评估是否需要等待networkidle有时domcontentloaded就足够了。减少不必要的操作步骤AI生成的计划可能包含冗余的滚动或等待。考虑并行处理多个不相关的抓取任务需要更复杂的Server架构。问题5返回给AI客户端的数据量太大导致响应缓慢或截断。解决方案在提取数据时通过limit参数限制条数。对长文本进行截断只返回摘要或前N个字符。对于大量数据可以考虑让Server将结果保存到临时文件或数据库只返回一个文件ID或查询链接给客户端。这个Playwright MCP实战项目将强大的浏览器自动化能力与AI的自然语言理解能力结合为网页数据抓取打开了一扇新的大门。它降低了自动化任务的技术门槛将繁琐的编码工作转化为清晰的指令描述。虽然目前指令解析的准确率和复杂网站的抓取成功率还有提升空间但其代表的方向——用自然语言驱动复杂工具——无疑是未来人机交互和自动化的重要趋势。你可以基于这个基础框架根据具体的业务场景进行深化和定制例如集成登录认证、处理验证码、对接数据库等构建出真正属于你的智能数据助手。
基于Playwright与MCP协议实现AI驱动的智能网页抓取
发布时间:2026/6/30 18:19:42
1. 项目概述当Playwright遇上MCP网页抓取进入“动口不动手”时代最近在折腾AI驱动的自动化项目发现一个特别有意思的组合Playwright和MCP。如果你还在为写复杂的网页抓取脚本而头疼或者觉得传统的爬虫框架在面对动态渲染、反爬策略时力不从心那这个组合绝对值得你花5分钟了解一下。简单来说它让你能用自然语言告诉AI“帮我抓取这个页面的商品列表”然后AI就能理解你的意图并调用Playwright去执行最后把结构化的数据返回给你。整个过程你几乎不需要写一行传统的定位元素、解析HTML的代码。这背后的核心是MCPModel Context Protocol。你可以把它理解为一个“翻译官”或“中间件”。大语言模型比如Claude、GPT本身并不知道如何操作浏览器但通过MCP我们可以将Playwright这类工具的能力“暴露”给AI。AI接收到你的自然语言指令后会通过MCP协议去调用对应的工具函数比如navigate_to_page,extract_dataPlaywright则负责忠实执行这些底层操作。这样一来抓取逻辑的“思考”部分交给了更擅长理解语义的AI而精准的“执行”部分则交给了强大的Playwright两者强强联合。这个项目非常适合以下几类朋友一是希望快速验证某个数据抓取想法不想在编码上耗费太多时间的产品经理或业务分析师二是需要处理大量异构、动态网页但团队前端自动化经验不足的开发者三是任何对AI应用落地感兴趣想亲手搭建一个“动口不动手”的智能体Agent的极客。接下来我就带你从零开始拆解这套方案的核心思路、详细实现以及我趟过的那些坑。2. 核心思路与架构拆解为什么是Playwright MCP在深入代码之前我们得先搞清楚为什么选这两个技术以及它们是如何协同工作的。这决定了整个项目的稳定性和扩展性。2.1 Playwright的优势与在数据抓取中的角色Playwright是一个由微软开源的浏览器自动化库。在数据抓取领域它相比传统的RequestsBeautifulSoup或者Selenium有几个压倒性优势无头浏览器支持与真实渲染Playwright可以启动真正的Chromium、Firefox或WebKit浏览器包括无头模式能完美执行JavaScript渲染出与用户肉眼所见完全一致的页面。这对于抓取依赖前端框架如React、Vue动态生成内容的网站至关重要。自动等待与稳定性它内置了智能等待机制可以等待元素出现、网络请求完成甚至动画结束再执行下一步操作。这大大减少了因页面加载速度不稳定而导致的脚本失败是我用过最稳定的浏览器自动化工具之一。强大的选择器与录制功能支持CSS、XPath、文本内容等多种定位方式而且DevTools里的录制功能可以快速生成操作脚本为后续的AI指令生成提供了丰富的操作范例。多上下文与隔离可以为每次抓取创建独立的浏览器上下文实现Cookie、本地存储的完全隔离避免数据污染也更容易模拟不同的用户会话。在这个项目中Playwright扮演的是“执行者”和“探索者”的角色。它不负责理解“商品列表”是什么但它能精准地执行“点击这个按钮”、“滚动到页面底部”、“获取这个CSS选择器下的所有文本”等原子操作。2.2 MCP协议连接AI大脑与工具手的关键桥梁MCP即模型上下文协议是Anthropic提出的一种开放协议用于让大语言模型安全、结构化地使用外部工具和资源。你可以把它想象成一套标准的“插座和插头”规范。Server服务器提供工具的一方。在这里我们将Playwright的操作打开浏览器、导航、抓取数据封装成一个个标准的工具函数并启动一个MCP服务器来暴露这些函数。Client客户端使用工具的一方。通常是集成了MCP客户端的AI应用比如Claude Desktop、Cursor IDE或者我们自己写的AI Agent程序。客户端会向服务器查询有哪些工具可用并在需要时调用它们。协议通信Client和Server通过JSON-RPC over stdio标准输入输出或SSE服务器发送事件进行通信。AI模型通过Client发出结构化请求Server执行后返回结果。我们的核心工作就是创建一个MCP Server将Playwright的能力“工具化”。例如我们创建一个名为scrape_website的工具它接受url和instruction自然语言指令两个参数。AI模型收到用户请求后会决定调用这个工具并生成相应的参数。我们的Server收到调用请求后则启动Playwright解析instruction执行相应的浏览器操作来完成任务。2.3 整体架构设计整个系统的数据流是这样的用户在AI客户端如Claude聊天窗口输入“帮我抓取知乎热榜的前10个问题标题和链接。”AI客户端MCP Client识别出这是一个需要调用外部工具的任务。它向已连接的我们的Playwright MCP Server查询可用的工具列表。Server返回工具列表其中包含我们定义的scrape_with_instruction工具及其参数说明。AI模型根据工具描述和用户指令构造一个JSON-RPC请求调用scrape_with_instruction参数为{“url”: “https://www.zhihu.com/hot”, “instruction”: “获取热榜列表中前10个条目的标题文本和其对应的详情页面链接”}。Playwright MCP Server收到请求解析参数。它启动一个Playwright浏览器实例导航到知乎热榜页面。在浏览器中Server需要“理解”instruction。这里有两种策略策略AAI辅助解析Server内部可以调用一个轻量级的本地AI模型或一次远程API调用将指令翻译成具体的Playwright操作步骤序列如定位类名为HotList-list的元素取其下前10个section元素在每个section内提取h2元素的文本作为标题提取a元素的href属性作为链接。策略B预定义模板针对常见指令如“抓取列表”、“提取表格”预定义好操作步骤。本例中我们可以预设对于“获取列表前N个条目的标题和链接”这类指令使用通用的CSS选择器提取模式。Server按照解析出的步骤控制Playwright执行操作并将提取到的数据组装成结构化的JSON格式。Server将结果通过MCP协议返回给AI客户端。AI客户端将结构化的数据呈现给用户或者由AI模型进一步总结、润色后输出。这个架构的关键在于解耦AI负责意图理解和高级规划MCP负责协议转换和工具调度Playwright负责底层精准执行。任何一部分都可以独立升级或替换。3. 环境准备与核心工具链搭建工欲善其事必先利其器。我们先来把开发环境搭好这里我会列出具体的版本和选择理由避免你踩坑。3.1 基础环境配置首先确保你的机器上安装了Node.js (版本18及以上)和Python (版本3.8及以上)。Node.js是运行Playwright和JavaScript版MCP Server的基石而Python生态中有丰富的AI模型库和工具方便我们后续做指令解析。# 检查Node.js和npm版本 node --version npm --version # 检查Python和pip版本 python --version pip --version为什么选择Node.jsPlaywright对Node.js的支持是最原生、最全面的其异步编程模型与浏览器自动化任务高度契合。虽然Playwright也有Python版本但考虑到MCP协议相关的工具和示例在JS/TS生态中更活跃我们优先使用Node.js环境。3.2 创建项目并安装核心依赖我们创建一个新的项目目录并初始化npm项目。mkdir playwright-mcp-scraper cd playwright-mcp-scraper npm init -y接下来安装最核心的三个依赖npm install playwright modelcontextprotocol/sdk axiosplaywright浏览器自动化库本体。modelcontextprotocol/sdkAnthropic官方提供的MCP SDK for Node.js。它提供了构建MCP Server和Client所需的所有类型和工具函数能极大简化开发。axios一个流行的HTTP客户端。如果我们在Server内部采用“策略A”调用AI API来解析指令会用到它。如果你选择纯“策略B”则可以暂时不装。安装浏览器Playwright需要下载它自己管理的浏览器二进制文件以保证环境一致性。npx playwright install chromium这里我推荐只安装Chromium因为它最轻量、兼容性也足够好。如果项目明确需要测试Firefox或WebKit的渲染差异再按需安装。3.3 构建MCP Server骨架MCP Server的核心是实现Server类并定义工具Tool和资源Resource。我们首先创建一个server.js文件搭建基础骨架。// server.js const { Server } require(modelcontextprotocol/sdk/server/index.js); const { StdioServerTransport } require(modelcontextprotocol/sdk/server/stdio.js); const { Tool } require(modelcontextprotocol/sdk/types.js); // 1. 创建Server实例指定名称和版本 const server new Server( { name: playwright-scraper, version: 1.0.0, }, { capabilities: { tools: {}, // 声明我们支持工具 }, } ); // 2. 定义我们的核心工具根据指令抓取网页 const scrapeTool new Tool( scrape_with_instruction, 根据自然语言指令从指定URL抓取结构化数据。, { type: object, properties: { url: { type: string, description: 要抓取的目标网页URL, }, instruction: { type: string, description: 用自然语言描述需要抓取什么数据例如“获取产品价格列表”或“提取文章标题和发布时间”, }, // 可以添加更多参数如等待时间、是否需要截图等 headless: { type: boolean, description: 是否以无头模式运行浏览器不显示界面, default: true, } }, required: [url, instruction], } ); // 3. 设置工具处理函数这里先留空下一节实现 server.setRequestHandler(tools/call, async (request) { // request.params 包含了工具名和调用参数 const { name, arguments: args } request.params; if (name scrape_with_instruction) { // 这里将调用Playwright执行任务 const result await handleScrapeInstruction(args.url, args.instruction, args.headless); return { content: [ { type: text, text: JSON.stringify(result, null, 2), // 将结果格式化为JSON字符串 }, ], }; } throw new Error(Unknown tool: ${name}); }); // 4. 在初始化时向客户端宣告我们提供的工具 server.setRequestHandler(tools/list, async () ({ tools: [scrapeTool], })); // 5. 启动Server使用stdio传输这是与AI客户端通信的标准方式 async function runServer() { const transport new StdioServerTransport(); await server.connect(transport); console.error(Playwright MCP Server is running on stdio...); } runServer().catch((error) { console.error(Server error:, error); process.exit(1); }); // 工具处理函数待实现 async function handleScrapeInstruction(url, instruction, headless true) { // 下一节将在这里填充Playwright逻辑 return { message: Will scrape ${url} with instruction: ${instruction} }; }这个骨架代码已经是一个可运行的MCP Server了。它定义了一个工具并准备好了处理调用的框架。现在我们需要用Playwright的魔力来填充handleScrapeInstruction函数。4. 核心抓取引擎实现让Playwright听懂指令这是整个项目最核心的部分。我们需要在handleScrapeInstruction函数中实现从“自然语言指令”到“Playwright操作序列”的转换并执行抓取。4.1 实现基础的Playwright抓取流程我们先实现一个不考虑复杂指令、只做简单页面访问和全文抓取的版本确保Playwright和MCP能联通。// 在 server.js 顶部引入playwright const { chromium } require(playwright); async function handleScrapeInstruction(url, instruction, headless true) { let browser; let page; try { // 1. 启动浏览器 browser await chromium.launch({ headless }); // 创建一个新的浏览器上下文实现会话隔离 const context await browser.newContext({ viewport: { width: 1920, height: 1080 }, userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 }); page await context.newPage(); // 2. 导航到目标URL并等待网络空闲状态 console.error(Navigating to ${url}); await page.goto(url, { waitUntil: networkidle }); // 3. 简单的指令解析演示如果指令包含“截图”就截图 if (instruction.toLowerCase().includes(screenshot)) { const screenshotBuffer await page.screenshot({ fullPage: true }); // 注意MCP协议目前主要传递文本。对于二进制数据可以转base64或保存到文件返回路径。 return { success: true, message: Screenshot taken., data: { screenshot: screenshotBuffer.toString(base64) } }; } // 4. 默认行为获取页面标题和整个body的粗略文本 const title await page.title(); const bodyText await page.textContent(body); return { success: true, url, instruction, data: { title, // 只取前1000字符预览避免返回数据过大 bodyTextPreview: bodyText.substring(0, 1000) (bodyText.length 1000 ? ... : ) } }; } catch (error) { console.error(Scraping error:, error); return { success: false, error: error.message }; } finally { // 5. 确保浏览器被关闭释放资源 if (browser) { await browser.close(); } } }现在我们的Server已经具备了最基础的能力。你可以用以下方式测试它启动Servernode server.js它会在标准输入输出上等待连接。我们需要一个MCP Client来测试。一个简单的方法是使用MCP SDK自带的测试工具或者更直观的使用支持MCP的AI客户端如配置了该Server的Claude Desktop。4.2 进阶实现指令解析与智能抓取基础版本只是“听令”打开网页。要实现“智能抓取”核心是解析instruction参数。这里我们探讨两种实战策略。策略A集成轻量级AI进行指令解析推荐用于复杂场景我们可以在Server内部将用户的自然语言指令转换为一个具体的“操作清单”。这个清单可以描述为一系列步骤例如[{action: scroll, selector: footer}, {action: get_elements, selector: .product-item, limit: 5, extract: {title: .title, price: .price}}]为了实现这个转换我们需要一个文本理解模型。为了速度和成本我们可以使用本地的小模型或者调用大模型的API。这里以调用OpenAI的GPT-3.5 API为例你需要准备OPENAI_API_KEY// 在文件顶部引入axios如果之前没装需要 npm install axios const axios require(axios); async function parseInstructionToPlan(instruction, url) { const prompt 你是一个网页抓取专家。请将用户的抓取指令转化为一个具体的JSON操作序列。 目标网页URL: ${url} 用户指令: ${instruction} 操作类型包括 1. navigate: 导航到某个URL如果指令包含新链接。 2. click: 点击某个元素。需要提供selector。 3. scroll: 滚动页面。可以指定方向或目标selector。 4. wait: 等待。可以指定时间毫秒或等待某个selector出现。 5. extract: 提取数据。需要提供目标selector以及要提取的字段映射字段名: 子selector或属性名。 6. screenshot: 截图。 请只输出一个合法的JSON数组每个元素是一个操作对象。 示例 [ {action: scroll, selector: footer}, {action: wait, milliseconds: 1000}, {action: extract, selector: .article-list li, limit: 10, fields: {title: h2, link: ahref}} ] ; try { const response await axios.post( https://api.openai.com/v1/chat/completions, { model: gpt-3.5-turbo, messages: [{ role: user, content: prompt }], temperature: 0.1, // 低温度保证输出格式稳定 }, { headers: { Authorization: Bearer ${process.env.OPENAI_API_KEY}, Content-Type: application/json, }, } ); const content response.data.choices[0].message.content; // 从返回的文本中解析出JSON数组 const jsonMatch content.match(/\[[\s\S]*\]/); if (jsonMatch) { return JSON.parse(jsonMatch[0]); } throw new Error(Failed to parse AI response as JSON); } catch (error) { console.error(AI parsing error:, error); // 解析失败时返回一个保守的默认计划只提取页面所有文本 return [{ action: extract, selector: body, fields: { fullText: _text } }]; } }然后我们需要升级handleScrapeInstruction函数来执行这个“操作计划”async function handleScrapeInstruction(url, instruction, headless true) { let browser; let page; try { browser await chromium.launch({ headless }); const context await browser.newContext(); page await context.newPage(); await page.goto(url, { waitUntil: networkidle }); // 解析指令为操作计划 const plan await parseInstructionToPlan(instruction, url); console.error(Execution plan:, JSON.stringify(plan)); const results []; // 执行计划中的每个操作 for (const step of plan) { await executeStep(page, step, results); } return { success: true, url, instruction, plan, // 返回执行计划便于调试 data: results }; } catch (error) { console.error(Scraping error:, error); return { success: false, error: error.message }; } finally { if (browser) await browser.close(); } } // 执行单个步骤的函数 async function executeStep(page, step, results) { switch (step.action) { case click: if (step.selector) { await page.click(step.selector, { timeout: 5000 }); await page.waitForLoadState(networkidle); } break; case scroll: if (step.selector) { // 滚动到特定元素 await page.locator(step.selector).scrollIntoViewIfNeeded(); } else if (step.direction bottom) { // 滚动到底部 await page.evaluate(() window.scrollTo(0, document.body.scrollHeight)); } if (step.milliseconds) { await page.waitForTimeout(step.milliseconds); } break; case wait: if (step.selector) { await page.waitForSelector(step.selector, { timeout: 10000 }); } else if (step.milliseconds) { await page.waitForTimeout(step.milliseconds); } break; case extract: if (step.selector) { const elements await page.locator(step.selector).all(); const limit step.limit || elements.length; for (let i 0; i Math.min(elements.length, limit); i) { const element elements[i]; const item {}; for (const [fieldName, fieldSpec] of Object.entries(step.fields || {})) { if (fieldSpec _text) { // 提取元素文本 item[fieldName] await element.textContent(); } else if (typeof fieldSpec string fieldSpec.includes()) { // 提取属性如 ahref const [subSelector, attr] fieldSpec.split(); const targetElem subSelector ? element.locator(subSelector).first() : element; item[fieldName] await targetElem.getAttribute(attr); } else if (typeof fieldSpec string) { // 提取子元素的文本 item[fieldName] await element.locator(fieldSpec).first().textContent(); } } results.push(item); } } break; case screenshot: const screenshotBuffer await page.screenshot({ fullPage: step.fullPage }); results.push({ screenshot: screenshotBuffer.toString(base64) }); break; default: console.warn(Unknown action: ${step.action}); } }策略B基于预定义规则和选择器的快速解析如果觉得调用AI API有延迟、成本或隐私顾虑或者你的抓取目标相对固定可以采用规则匹配的方式。例如我们可以内置一些常见指令模式function parseInstructionByRule(instruction) { const lowerInstruction instruction.toLowerCase(); // 规则1抓取列表 if (lowerInstruction.includes(list) || lowerInstruction.includes(前)) { const match lowerInstruction.match(/前\s*(\d)/); const limit match ? parseInt(match[1], 10) : 10; // 这里需要根据网站结构预设或动态探测选择器这是一个难点 // 可以尝试常见的选择器模式如 .list-item, li, article, tr return { action: extract_list, itemSelector: .list-item, li, article, tr, // 尝试性选择器 fields: { text: _text }, limit }; } // 规则2抓取表格 else if (lowerInstruction.includes(table)) { return { action: extract_table, tableSelector: table }; } // 默认规则提取所有段落文本 else { return { action: extract_text, selector: p, h1, h2, h3, h4 }; } }规则引擎的优点是速度快、零成本但灵活性和准确性远不如AI解析。在实际项目中我通常采用“混合策略”先尝试用规则匹配常见、明确的指令对于规则无法覆盖的复杂指令再降级到使用AI解析。这样能在成本和效果间取得平衡。5. 完整代码整合与配置我们将上述所有模块整合到一个完整的、可运行的server.js文件中。同时为了便于使用我们还需要一个配置文件来管理API密钥等敏感信息。项目结构playwright-mcp-scraper/ ├── server.js # 主服务器文件 ├── config.json # 配置文件需自行创建不要提交到git ├── package.json └── node_modules/config.json{ openaiApiKey: 你的OpenAI API密钥, defaultHeadless: true, userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ... }完整的 server.jsconst { Server } require(modelcontextprotocol/sdk/server/index.js); const { StdioServerTransport } require(modelcontextprotocol/sdk/server/stdio.js); const { Tool } require(modelcontextprotocol/sdk/types.js); const { chromium } require(playwright); const axios require(axios); const config require(./config.json); // 加载配置 const server new Server( { name: playwright-mcp-scraper, version: 1.0.0 }, { capabilities: { tools: {} } } ); // 定义工具 const scrapeTool new Tool( scrape_with_instruction, 根据自然语言指令从指定URL抓取结构化数据。支持点击、滚动、等待、提取等多种操作。, { type: object, properties: { url: { type: string, description: 目标网页URL }, instruction: { type: string, description: 自然语言指令如“获取前10条新闻的标题和链接” }, headless: { type: boolean, description: 无头模式, default: config.defaultHeadless }, timeout: { type: number, description: 全局超时时间毫秒, default: 30000 } }, required: [url, instruction], } ); // AI解析指令函数 async function parseInstructionToPlan(instruction, url) { // 此处省略与上一节代码相同。实际使用时请将上一节的 parseInstructionToPlan 函数复制过来。 // 注意这里需要读取 config.openaiApiKey } // 执行步骤函数 async function executeStep(page, step, results) { // 此处省略与上一节代码相同。实际使用时请将上一节的 executeStep 函数复制过来。 } // 主处理函数 async function handleScrapeInstruction(url, instruction, headless true, timeout 30000) { let browser; let page; try { browser await chromium.launch({ headless }); const context await browser.newContext({ viewport: { width: 1920, height: 1080 }, userAgent: config.userAgent }); page await context.newPage(); // 设置页面超时 page.setDefaultTimeout(timeout); await page.goto(url, { waitUntil: networkidle, timeout }); const plan await parseInstructionToPlan(instruction, url); console.error(Execution plan:, JSON.stringify(plan)); const results []; for (const step of plan) { await executeStep(page, step, results); } return { success: true, url, instruction, plan, data: results }; } catch (error) { console.error(Scraping error:, error); return { success: false, error: error.message }; } finally { if (browser) await browser.close(); } } // 设置MCP Server处理器 server.setRequestHandler(tools/list, async () ({ tools: [scrapeTool], })); server.setRequestHandler(tools/call, async (request) { const { name, arguments: args } request.params; if (name scrape_with_instruction) { const result await handleScrapeInstruction( args.url, args.instruction, args.headless ! undefined ? args.headless : config.defaultHeadless, args.timeout ); return { content: [{ type: text, text: JSON.stringify(result, null, 2) }], }; } throw new Error(Unknown tool: ${name}); }); // 启动服务器 async function runServer() { const transport new StdioServerTransport(); await server.connect(transport); console.error(Playwright MCP Scraper Server is running...); } runServer().catch((error) { console.error(Server fatal error:, error); process.exit(1); });运行与测试创建config.json文件并填入你的OpenAI API密钥。安装依赖npm install启动服务器node server.js服务器现在正在标准输入输出上等待连接。你需要一个MCP客户端来调用它。6. 连接AI客户端在Claude Desktop中实际使用让我们的Server发挥价值的最后一步是让它被AI助手调用。这里以Claude Desktop为例展示如何配置。找到Claude Desktop的配置目录macOS:~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:%APPDATA%\Claude\claude_desktop_config.jsonLinux:~/.config/Claude/claude_desktop_config.json编辑配置文件在mcpServers部分添加我们的服务器配置。{ mcpServers: { playwright-scraper: { command: node, args: [ /ABSOLUTE/PATH/TO/YOUR/PROJECT/playwright-mcp-scraper/server.js ], env: { OPENAI_API_KEY: 你的OpenAI API密钥 // 也可以在这里设置环境变量 } } } }注意command也可以是npmargs可以是[run, start]前提是你在package.json中配置了scripts: { start: node server.js }。关键是必须使用绝对路径。重启Claude Desktop。开始对话重启后在Claude的聊天框中你可以直接说“使用scrape_with_instruction工具抓取豆瓣电影Top250第一页的电影名称和评分URL是 https://movie.douban.com/top250”。Claude会识别出可用的工具并生成类似下面的调用请求你看不到这个过程最终将结构化的JSON结果返回并展示给你。7. 实战优化与避坑指南在实际部署和使用中你会遇到各种问题。以下是我从多次实践中总结出的关键点和避坑技巧。7.1 性能与稳定性优化浏览器实例复用频繁启动关闭浏览器开销巨大。可以修改Server使用一个浏览器实例池。对于每个请求从池中取出一个上下文Context来创建新页面用完后关闭页面但保留上下文实现会话隔离和性能提升。设置合理的超时与等待networkidle等待虽然稳但某些页面可能有长期活跃的连接如WebSocket。需要设置全局超时如30秒并为关键操作如waitForSelector单独设置更短的超时避免单个页面卡死整个流程。处理反爬机制User-Agent使用常见的浏览器UA并定期更新。指纹识别Playwright启动的浏览器有默认指纹。高级反爬会检测。可以通过browser.newContext()时传入viewport,locale,timezoneId等参数进行一定程度的伪装但无法完全模拟真实用户环境的复杂性。频率限制在Server端实现请求队列和延迟控制避免对同一域名短时间发起大量请求。错误处理与重试网络波动、元素加载失败很常见。在executeStep函数中为每个操作特别是click和waitForSelector添加try-catch并实现简单的重试逻辑如重试2次能显著提升鲁棒性。7.2 指令解析的准确性提升提供网站结构提示在调用AI解析指令时可以将目标页面的部分关键HTML结构或已知的选择器作为上下文提供给AI。例如先让Playwright获取页面body的innerHTML的前几千个字符连同指令一起发送给AI。这能极大提高AI生成的选择器的准确性。后处理与验证AI生成的选择器可能无效。在执行提取操作前可以用page.locator(selector).count()检查元素是否存在。如果不存在可以尝试一些备用的通用选择器或者将错误信息反馈给用户/AI进行下一轮交互修正。定义清晰的指令范式引导用户使用更结构化的指令比如“以表格形式提取”、“提取所有图片的src属性”、“点击‘加载更多’按钮直到不再出现新内容然后提取全部列表”。在工具描述中给出明确示例能减少歧义。7.3 安全与资源管理输入验证与沙箱务必对用户输入的url进行验证防止恶意URL如file://协议或SSRF攻击。可以考虑将浏览器运行在独立的Docker容器或安全沙箱中。资源限制限制单个任务的最大运行时间、最大内存使用和可访问的域名。防止恶意指令导致无限循环或访问内部网络。敏感信息API密钥等绝对不要硬编码在代码中。使用config.json或环境变量管理并将config.json加入.gitignore。7.4 扩展性设计更多工具除了抓取可以暴露更多Playwright能力作为独立工具如screenshot_page: 对页面截图。run_script: 在页面上下文中执行自定义JavaScript代码。get_network_logs: 获取页面加载过程中的网络请求信息对于分析API接口非常有用。会话管理实现create_session和close_session工具允许跨多个工具调用保持同一个浏览器会话如登录状态。结果缓存对于相同的URL和指令可以将结果缓存一段时间减少重复抓取提升响应速度。8. 常见问题排查与调试技巧即使按照步骤操作也难免会遇到问题。这里列出一些常见错误和解决方法。问题1启动Server后Claude Desktop无法连接或找不到工具。检查点Claude Desktop配置文件中args的路径是否是绝对路径运行node server.js命令时是否在项目目录下控制台是否打印了启动日志检查Claude Desktop的日志文件通常在配置目录同级看是否有连接错误信息。确保package.json中依赖已正确安装 (node_modules存在)。问题2AI解析指令时返回错误或非JSON内容。检查点OPENAI_API_KEY环境变量或配置文件是否正确设置且有效尝试在parseInstructionToPlan函数中打印AI API的请求和响应查看返回的原始内容。调整提示词Prompt使其更严格地要求输出JSON。可以增加“你必须只输出JSON数组不要有任何其他解释文字”这样的指令。考虑使用更高性能的模型如gpt-4或专门微调过的模型来提升解析准确率。问题3Playwright执行时元素找不到或操作失败。调试技巧将headless参数设为false让浏览器窗口显示出来直观地看到脚本执行到了哪一步页面状态如何。在关键步骤前后添加page.screenshot({ path: debug-step1.png })将截图保存到本地查看。使用page.evaluate(() debugger)在浏览器开发者工具中暂停脚本手动检查DOM。检查AI生成的选择器是否合理。可以在无头模式下用page.locator(selector).count()验证元素数量。问题4抓取速度慢。优化方向启用浏览器实例/上下文复用。评估是否需要等待networkidle有时domcontentloaded就足够了。减少不必要的操作步骤AI生成的计划可能包含冗余的滚动或等待。考虑并行处理多个不相关的抓取任务需要更复杂的Server架构。问题5返回给AI客户端的数据量太大导致响应缓慢或截断。解决方案在提取数据时通过limit参数限制条数。对长文本进行截断只返回摘要或前N个字符。对于大量数据可以考虑让Server将结果保存到临时文件或数据库只返回一个文件ID或查询链接给客户端。这个Playwright MCP实战项目将强大的浏览器自动化能力与AI的自然语言理解能力结合为网页数据抓取打开了一扇新的大门。它降低了自动化任务的技术门槛将繁琐的编码工作转化为清晰的指令描述。虽然目前指令解析的准确率和复杂网站的抓取成功率还有提升空间但其代表的方向——用自然语言驱动复杂工具——无疑是未来人机交互和自动化的重要趋势。你可以基于这个基础框架根据具体的业务场景进行深化和定制例如集成登录认证、处理验证码、对接数据库等构建出真正属于你的智能数据助手。