Puppeteer vs Selenium:网页抓取与自动化测试工具选型全解析
发布时间:2026/9/16 21:02:25来源:尧图网络
上周有个做电商数据的朋友找我救火他们要定期从一个老旧的会员管理系统里抓报表数据页面全是 JavaScript 渲染直接上 Requests 什么都拿不到。折腾了两天后他抛给我一个经典问题Puppeteer 和 Selenium 到底该用哪个这个问题我其实被问过不下十次每次都得从头给他掰扯一遍原理和适用场景。网页抓取工具选型这件事说难不难但如果不把底层逻辑摸清楚真等到代码写了一半再换方案那才叫痛苦。这篇文章我就把这两大工具从原理、实战到踩坑完整讲透给正在纠结选型的你一个可以直接抄作业的答案。1. 先聊清楚Puppeteer 和 Selenium 到底各自是什么很多新手一上来就急着对比 API 好不好写、代码好不好调但其实选型的第一关是搞清楚这两个工具的底层身份。它们是两种哲学两种技术路径不是简单换个语法就能互通的。1.1 PuppeteerChrome 亲儿子Node 选手的利器Puppeteer 是 Google Chrome 团队官方出的 Node.js 库它最大的特点是直接通过 Chrome DevTools 协议CDP和 Chromium 浏览器通信。CDP 是什么简单说就是 Chrome 留出来的原生控制通道能让你操作系统里那台浏览器几乎是“内窥镜级别”的操作——页面跳转、DOM 操作、网络请求拦截、截图、生成 PDF、甚至模拟设备指纹全都覆盖。我第一次用 Puppeteer 的感受就是这玩意太通透了。你不用在浏览器外面再套一层驱动因为它本身就是浏览器亲爹给的钥匙。launch()一调它甚至能帮你自动下载一个 Chromium 内核不用你管环境里的浏览器是不是最新版。但代价也很明显Puppeteer 只支持 Chromium 系浏览器。Firefox 虽然出了实验性支持但距离生产可用还有距离。如果你的项目目标是跑 Chrome、Edge现在 Edge 也是 Chromium 内核那没问题可一旦要兼容 Safari、老版本浏览器Puppeteer 就有点力不从心了。1.2 Selenium跨浏览器自动化的老牌霸主Selenium 是自动化测试领域的老爷车从 2004 年就开始活跃现在绝大多数大厂的 Web UI 自动化测试脚本都基于它。它不是直接操作浏览器而是通过 WebDriver 协议和浏览器驱动比如 chromedriver、geckodriver通信。WebDriver 是一个行业标准各大浏览器厂商都主动去实现这个协议接口所以 Selenium 能横跨 Chrome、Firefox、Safari、Edge 甚至 IE。Selenium 支持的语言非常多Java、Python、C#、Ruby、JavaScript 都有官方绑定。这也意味着如果你在 Python 或 Java 的技术栈里做网页抓取Selenium 是更顺手的选项。我身边不少做爬虫的老哥主力语言是 Python所以哪怕 Puppeteer 在性能上更有优势他们最后还是会选 Selenium不为别的就因为不用为了一个抓取脚本去单独维护一个 Node 服务。1.3 核心原理的差异CDP 与 WebDriver 是完全相反的路子这里我做个生活化的类比。CDP 就像你把钥匙直接给了最信任的朋友他进门后想怎么动你的东西都行动作细腻、效率高但前提是你家是这种特定品牌的锁。WebDriver 则更像中介模式你通过中介驱动去给房主浏览器传话中介严格遵守一套标准化的通信协议所以不管房主是谁只要他认得中介你就能跟他对话。中介效率自然没有直达那么高但胜在兼容性好。这个原理差异直接决定了两者在几个关键性能上的差别能力项PuppeteerSelenium操作深度可直接拦截请求、修改响应、控制网络拦截请求需要配合代理工具能力受限浏览器范围Chromium / Chrome 最丝滑全主流浏览器执行速度一般更快因为通信链路短稍慢WebDriver 协议有多层转发API 复杂度相对简洁Promise 风格较传统API 多、历史包袱重别小看通信链路长短这个事。我实际跑过一个 500 个商品页的采集任务同样的流程、同样的等待逻辑Puppeteer 比 Selenium 快大约 25% 到 30%。如果你的抓取量是几万级、几十万级这个差距足以让你加班几小时。2. 选型的第一课看清你项目到底要干嘛很多人一上来就问“哪个更好”这其实是个伪命题。选型永远先看约束条件。我把这些年接触过的项目需求总结了三个核心维度你拿这三个问题去套自己的项目答案基本就浮出水面了。2.1 先看任务类型数据抓取还是回归测试如果任务是批量抓取数据、模拟点击路径、验证业务流这两者都能干但侧重点不同。Puppeteer 在网络请求拦截、响应分析和性能优化上有天然优势适合做重数据的抓取任务。Selenium 在断言、测试报告、多浏览器矩阵上生态更成熟适合做自动化测试框架尤其当团队有“回归测试 爬虫”双重需求时Selenium 一套代码两头用很香。我建议你问自己一个问题你的代码里是不是大量使用了“验证”“期望结果”“断言失败”这些逻辑如果是你干的更像测试的活Selenium 的生态能帮你省下很多写框架的时间。如果你的核心诉求是“给我拿到数据、存到库里”那 Puppeteer 的拦截和处理速度更有优势。2.2 再看团队技术栈Node 还是 Python/Java就算 Puppeteer 性能再好你团队里都是 Python 工程师我也不会建议你去搞 Node 服务。维护成本这种东西账面上看不到但项目上线后每天都在扣分。如果你的技术栈是 PythonSelenium BeautifulSoup/parsel 的组合非常顺手解析、数据处理、入库全在一个脚本里搞定。如果是 Node 技术栈那 Puppeteer 就是不二之选它的 async/await 流程控制和 Node 天生的异步 IO 配合写起来相当舒服。还有一个折中方案如果你的核心爬虫必须用 Python但又要用 Puppeteer 的能力那就把 Puppeteer 包装成一个微服务用 HTTP 接口暴露几个页面操作能力Python 那边负责调用。我这么干过能两全但要接受额外维护一套服务的成本。2.3 部署环境和数据规模同样影响决策你有没有想过抓取脚本跑在什么环境如果跑在轻量 Docker 容器里Puppeteer 的 Chromium 依赖会比 Selenium 多一些系统库Dockerfile 要费点心思。但 Selenium 这边你需要保证 WebDriver 和浏览器版本严格匹配容器里版本一旦升级就可能导致线上任务挂掉。再看看数据规模。小批量页面、偶尔抓一次选哪个都无所谓按你顺手来。但如果你要做的是大规模并发抓取那 Puppeteer 的浏览器实例复用、请求拦截和轻量级启动特性会让你省下不少服务器资源。数据量百万级以上的项目我几乎没见过用 Selenium 当主力跑爬虫的基本都是 Puppeteer 或者干脆上 Playwright。3. 实战对比同样的活两边分别怎么干理论聊完直接上实操对比。我挑了四个典型场景用同一需求在两边写出实现方式你看完就明白二者的操作风格差距在哪里。3.1 打开页面与等待元素两套思路两种写法先看 Puppeteer 的写法const puppeteer require(puppeteer); (async () { const browser await puppeteer.launch({ headless: new, args: [--no-sandbox, --disable-setuid-sandbox] }); const page await browser.newPage(); await page.goto(https://example.com/login, { waitUntil: networkidle2, timeout: 30000 }); await page.waitForSelector(#username, { timeout: 10000 }); console.log(页面就绪); await browser.close(); })();再看 SeleniumPython 版的写法from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver webdriver.Chrome(serviceService(ChromeDriverManager().install())) driver.get(https://example.com/login) try: element WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, username)) ) print(页面就绪) finally: driver.quit()注意几个细节。Puppeteer 的waitUntil: networkidle2表示等所有网络请求基本结束才继续这在抓取单页应用时很管用。Selenium 的WebDriverWait则是一种显式轮询先定义一个最长等待时间再等某个条件成立。从写法上你看得出来Puppeteer 的等待条件是内置在 API 里的非常顺滑Selenium 里你需要组合WebDriverWaitexpected_conditions来做多了一层心智负担。还有一个实际感受Puppeteer 中可以很优雅地链式调用.click()、.type()Promise 风格天然不会写出“回调地狱”。Selenium 用 Python 写其实也挺直观但如果是 Java 绑定就比较啰嗦。3.2 模拟点击输入从按钮到填表表单交互是网页抓取里最常见的操作。Puppeteer 里模拟用户输入可以用page.type()或者page.keyboard.type()await page.type(#username, admin); await page.type(#password, password123); await page.click(button[typesubmit]); await Promise.all([ page.waitForNavigation({ waitUntil: networkidle2 }), ]);这里有个很关键的点waitForNavigation必须和触发跳转的page.click()并行放入Promise.all中否则会竞态。如果先 click 再 wait跳转可能已经开始了等待会超时。这个坑我踩过现在都养成习惯写成Promise.all。Selenium 里是这个写法driver.find_element(By.ID, username).send_keys(admin) driver.find_element(By.ID, password).send_keys(password123) driver.find_element(By.CSS_SELECTOR, button[typesubmit]).click() WebDriverWait(driver, 10).until( EC.url_changes(driver.current_url) )Selenium 的 WebDriver 协议下click()会等待当前动作完成所以不存在 Puppeteer 那种竞态问题。但 Selenium 的弱点是智能等待少你得自己判断“点击后页面变化”这个条件。用EC.url_changes、EC.staleness_of这些条件可以做到但需要你显式声明。3.3 最难的那类下拉框divulli 组合怎么破很多网站的下拉框不是原生select而是用divulli组合模拟的。原生 select 用 Select 类一行搞定但这种模拟下拉框你真的要像真实用户一样“点开菜单再选一个条目”。先用 Selenium 写一次完整定位from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.keys import Keys # 点击 div 触发器展开下拉菜单 trigger driver.find_element(By.CSS_SELECTOR, div.custom-select-trigger) trigger.click() # 关键等待 li 列表真正渲染出来 options_list WebDriverWait(driver, 10).until( EC.presence_of_all_elements_located( (By.CSS_SELECTOR, ul.custom-select-options li.custom-option) ) ) # 遍历选项按文本匹配目标项 target None for option in options_list: if option.text.strip() 目标选项: target option break if target is None: raise RuntimeError(f未找到选项: 目标选项) target.click()这里的核心逻辑是三步点开、等待、遍历匹配。很多初学者会栽在第二步以为 click 之后 li 立刻就存在。实际上这类组件大多有动画过渡或者异步渲染不加显式等待直接find_elements大概率拿到空列表。如果 li 在 hover 时才显示还要用ActionChains(driver).move_to_element(trigger).perform()先悬停再点击。这类下拉框的触发方式五花八门写之前先在 DevTools 里确认事件触发类型是 click 还是 mouseover。同理Puppeteer 的实现// 点击触发器 await page.click(div.custom-select-trigger); // 等待菜单可见 await page.waitForSelector(ul.custom-select-options li.custom-option, { visible: true, timeout: 10000 }); const options await page.$$(ul.custom-select-options li.custom-option); let clicked false; for (const option of options) { const text await option.evaluate((el) el.textContent.trim()); if (text 目标选项) { await option.click(); clicked true; break; } } if (!clicked) { throw new Error(未找到目标选项); }看到没Puppeteer 里page.waitForSelector的visible: true参数特别香它只等元素真实可见而不是仅仅存在于 DOM 里这恰好对得上模拟下拉框的渲染特点。3.4 元素枚举与定位元数据让抓取更健壮有个场景大家可能遇到过页面结构经常小改版今天 CSS 类名还是.old-class明天就改成.new-class。如果每次都改代码维护成本极高。一个成熟的思路是——先做一轮“元素枚举”把页面上所有关键交互元素的定位信息提取出来存储之后抓取时直接用这份元数据来定位元素而不是硬编码选择器。具体做法是写一段 JavaScript 注入页面遍历 DOM 生成每个元素的选择器路径同时记录标签名、id、name、data-* 属性等元数据。我比较推荐优先用 id因为它是页面唯一的概率最高没有 id 就找唯一的 class 组合再不行就用nth-child路径。Puppeteer 里可以直接用page.evaluate()执行原生 JS 来枚举const elementMeta await page.evaluate(() { const results []; const allElements document.querySelectorAll(input, select, textarea, button, a, [rolecombobox]); allElements.forEach((el) { let selector ; if (el.id) { selector #${el.id}; } else { const classes Array.from(el.classList).slice(0, 2).join(.); selector ${el.tagName.toLowerCase()}.${classes}; } results.push({ tag: el.tagName, text: el.textContent.trim().slice(0, 50), selector, name: el.getAttribute(name), dataAttr: el.getAttribute(data-test) }); }); return results; });Selenium 里也可以用execute_script执行同样的逻辑拿到 JSON 后你只需要把“定位元数据”存进数据库后续抓取脚本每次启动先读取这些元数据来定位元素。这么做还有一个额外好处如果页面改了结构重新跑一次枚举对比新旧元数据的差异就能精准定位是哪个元素的定位失效了。你可能要问“仅存储定位元数据”和直接存整个 DOM 有什么区别区别大了。DOM 快照体积大、含大量噪声而且页面版本更新后旧快照基本没有复用价值。定位元数据只有几个字段抓取时实时结合文档结构来计算灵活性和存储效率都高出一截。4. 进阶操作真正干爬虫时绕不开的硬核环节选型离不开对未来需求的预判。如果你的爬虫会涉及拦截请求、高并发、登录态处理这些进阶能力那技术选型的分水岭会更加明显。4.1 网络拦截与接口级抓取这是 Puppeteer 最让我上头的一个功能。page.setRequestInterception(true)之后浏览器里的每个 HTTP 请求都能被你拦下来你可以修改请求头、阻止某些资源加载、甚至直接返回伪造的响应。await page.setRequestInterception(true); page.on(request, (request) { if (request.resourceType() image || request.resourceType() media) { request.abort(); } else if (request.url().includes(/api/userinfo)) { // 修改请求头后继续 const headers request.headers(); headers[X-Custom-Token] 这里填你的token; request.continue({ headers }); } else { request.continue(); } });这个能力对爬虫太有用了。比如一个页面上挂了 50 张图片、若干统计脚本但你要的只是表单数据把图片全abort()掉加载速度直接乘二。想拿接口 JSON 也不用等页面渲染完成直接监听 response 事件page.on(response, (response) { const url response.url(); if (url.includes(/api/orders)) { response.json().then((data) { console.log(拦截到接口数据:, data); }); } });Selenium 理论上也能监听网络事件但 WebDriver 协议在这方面非常笨重。老方案是配 BrowserMob Proxy 或 mitmproxy先起一个代理服务再把浏览器的代理设置指向它绕了一大圈才能看到请求响应。Selenium 4 之后虽然原生支持了 window performance 资源日志但想篡改请求、阻止加载还是做不到。从抓取这个角度这一项 Puppeteer 是降维打击。4.2 多标签页并发与浏览器实例复用写爬虫的人迟早会遇到并发问题。Puppeteer 可以一个浏览器实例拉起多个标签页并行跑任务这是它的设计优势。注意不要每次任务都launch()一个新浏览器那开销很大。正确姿势是创建一个浏览器实例然后通过browser.newPage()开多个 page 并行处理最后统一关闭。const puppeteer require(puppeteer); (async () { const browser await puppeteer.launch({ headless: new }); const tasks Array.from({ length: 10 }, async (_, index) { const page await browser.newPage(); await page.goto(https://example.com/list?page index, { waitUntil: domcontentloaded }); const data await page.evaluate(() document.title); await page.close(); return data; }); const results await Promise.all(tasks); console.log(results); await browser.close(); })();用Promise.all控制并发时机比一个个顺序执行优雅太多。Selenium 要并发可以开多个线程每个线程维护一个 driver但 driver 实例非常吃资源而且 WebDriver 之间没有很好的复用机制。我试过用 ThreadPoolExecutor 跑 10 个 Selenium driver内存直接飙到好几个 G相同的任务量用 Puppeteer 的 page 并发内存占用要小一截。4.3 登录态保持与基础防检测很多抓取目标需要登录。Puppeteer 的登录态保持非常直接登录一次把 cookie 存下来以后每次启动直接往页面注入 cookie 就行。const fs require(fs); // 登录后保存 cookie const cookies await page.cookies(); fs.writeFileSync(cookies.json, JSON.stringify(cookies)); // 下次启动直接恢复 const cookies JSON.parse(fs.readFileSync(cookies.json)); await page.setCookie(...cookies);Selenium 也有get_cookies()和add_cookie()但实际使用中我发现 Selenium 的 cookie 恢复在一些跨域、iframe 场景下处理起来比较麻烦常常需要手动拼接域名。再说说防检测。这个领域水很深但基础的做法两边都能实现。比如设置userAgent、模拟真实视口大小、禁用自动化提示标志。Puppeteer 里可以用page.setUserAgent和启动参数--disable-blink-featuresAutomationControlledSelenium 里则是通过ChromeOptions来加实验性参数。坦白讲高防护网站两边都会被识破需要更专业的方案这里不展开。5. 常见问题与排查技巧实录不管选哪个实际跑起来总会遇到坑。我把自己和身边朋友踩过的典型问题整理成一个速查表你遇到类似情况可以直接照着排查。5.1 Selenium 装完就报错八成是 WebDriver 版本问题Selenium 安装本身不难pip install selenium一行搞定。但很多新人装完跑webdriver.Chrome()就撞上WebDriverException: session not created十有八九是 chromedriver 和 Chrome 主版本不一致。我现在的标准做法是直接用webdriver-manager它会在启动时自动检测本机浏览器版本并下载对应的驱动程序from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver webdriver.Chrome(serviceService(ChromeDriverManager().install()))如果你的 Selenium 版本是 4.6 以上更省事——Selenium Manager 已经内置了只要环境变量SELENIUM_MANAGER_ENABLED为 true默认就是 true无需手动指定 Service直接webdriver.Chrome()即可自动管理驱动。这里我多提醒一句团队内部使用最好固定规格统一浏览器版本和 driver 版本否则今天你电脑上能跑明天 CI 上就挂。5.2 Puppeteer 下载慢、白屏、超时怎么处理Puppeteer 安装时默认会下载 Chromium国内网络环境经常慢到怀疑人生。解决办法是用镜像源PUPPETEER_DOWNLOAD_HOSThttps://npmmirror.com/mirrors/chromium-browser-snapshots npm install puppeteer或者干脆安装puppeteer-core不自动下载浏览器直接用系统已有的 Chromenpm install puppeteer-core使用puppeteer-core时注意launch()需要显式指定executablePath指向你系统里 Chrome 或 Chromium 的路径。还有一个诡异的场景页面能打开但一直白屏等了半天waitForSelector也没有结果。先别急着怀疑代码优先检查是不是显示器问题。我的排查顺序是先看 URL 是否正确加载打印 page.url()、再看标题和 body 文本是否非空、最后检查页面有没有弹窗/遮罩层挡住了元素。另外如果页面里内嵌了 iframe记住目标元素在 iframe 里需要page.frames()找到对应 frame 再操作这一步特别容易被忽略。5.3 定位不到元素先把等待策略和帧切对定位不到元素是网页抓取最常见的报错几乎每个入行的人都经历过。Selenium 中有隐式等待和显式等待之分隐式等待是 10 秒轮询一次全局只对find_element生效显式等待用WebDriverWait加条件判断更精准。我的习惯是基础页面上用隐式等 3 秒兜底关键交互环节用显式等待精确控制绝不在所有地方无脑等 10 秒否则抓取效率会被拖垮。iframe 的问题是另一个大坑。如果页面结构是嵌套 frame你直接find_element会报找不到必须先用driver.switch_to.frame()切入对应 frame 块。等操作完成后记得driver.switch_to.default_content()切回主文档。Puppeteer 中不需要显式切换找到对应的 Frame 对象再用其方法但核心思路是一样的先确认元素属于哪个 frame再去对应上下文找。6. 最终决策用表格和案例把选择讲透前面分析了很多细节最后我再用一张表和一个真实案例把整个选型逻辑收拢起来。6.1 选型对照表一眼看明白差距决策维度选 Puppeteer选 Selenium主力语言Node.jsJavaScript / TypeScriptPython、Java、C# 等多语言浏览器要求只跑 Chrome / Chromium / Edge必须兼容多浏览器抓取规模中大批量追求速度和资源效率小批量或中量侧重复用测试脚本网络请求处理需要拦截/修改请求这是刚需不需要或可容忍代理中间层团队背景前端 / Node 团队后端 / 测试团队现有代码资产从零开始已有大量 Selenium 代码要复用这个表格不是绝对标准但覆盖了我选型时 90% 的考虑因素。唯一的例外情况是如果你有特殊需要比如要操作桌面版 Firefox 的某个隐私特性那 Selenium 无可替代。6.2 一个真实项目从需求到方案的完整选型过程说个近期的项目。客户要抓某电商后台的销售订单每天凌晨跑一次数据量约 2 万条页面有大量图表渲染订单列表是无限滚动加载。团队情况数据侧是 Python有一个现成的 Django 服务做调度前端有一个人可以帮忙写 Node 脚本。我们原本想用 Selenium因为在 Python 环境里好维护。但正式开发前做了一次技术验证遇到了三个问题无限滚动列表需要频繁拖动页面Selenium 的滚动事件偶尔丢失导致漏数据请求了 500 条的时候内存占用已经很高并发扩展困难最关键是订单列表接口的数据已经被打包到全局 JS 变量里根本不需要等页面完整渲染只需要拦截一次请求就能拿到全部 JSON。最后方案调整为抓取核心用 Puppeteer 写成独立微服务通过 HTTP 接口暴露“获取某天订单”功能Python 侧只负责调度和入库。上线后跑了一次2 万条数据从打开浏览器到全部拿到 JSON 用了不到 6 分钟内存稳定在 400M 左右。这个结果如果用 Selenium 硬做至少翻倍。6.3 我给新手的建议如果你还在犹豫我给一个最简单的决策路径前端或 Node 开发者、目标是数据抓取、希望代码简洁高效那直接选 Puppeteer如果你是 Python 后端工程师、做自动化测试为主、偶尔抓抓数据那就老老实实用 Selenium。别两头都学也别一上来就上框架先用一个小项目把一方练到熟练后面遇到新需求自然会判断。我个人在实际操作中最深的体会是选型成败不完全是工具强弱更多取决于“团队能用什么、项目要什么”。你的技术栈越统一代码越容易维护后期补坑的时间就越少。我见过不少项目因为“性能好”就硬选一个团队不会的语言写爬虫结果人走了代码没人敢动最后整个推倒重来。最后再分享一个选型时很容易忽略的点社区和文档的品质。Puppeteer 的官方文档更新很快示例代码质量高遇到问题基本能在 GitHub issues 里找到答案。Selenium 的文档虽然厚重但部分内容还是老版本的写法参考时要留意版本号。如果你动手能力强也可以关注一下 Playwright它是 Puppeteer 团队核心成员出来做的升级版跨浏览器能力更强很多新项目甚至直接绕过 Puppeteer 选了它。不过那是另一个话题了等大家把这两个玩熟了自然会找到自己的答案。
网站建设高端定制企业官网