尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Puppeteer 与 Selenium 选型指南:网页抓取与浏览器自动化的关键对比

Puppeteer 与 Selenium 选型指南:网页抓取与浏览器自动化的关键对比 做网页抓取的人迟早都会被同一个问题卡住Puppeteer 还是 Selenium这两个名字太常出现在招聘需求、技术博客和框架选型文档里以至于很多新人以为它们功能完全对等随便选一个就能开工。但真把页面复杂度、语言栈、并发压力、维护成本这些因素叠上来之后你会发现选型这事远不是“Google 一下哪个热门”能解决的。这篇文章更像是一份我自己踩过不少坑之后的选型手记。我会从两者的架构差异讲起再结合本地实际跑过的抓取任务把页面抓取里最容易翻车的几个环节拆开对比最后给出一张可以直接参考的排查表。不管你是刚入门的爬虫新手还是已经在维护一套线上采集系统的开发者这篇文章应该都能帮你省下不少试错时间。1. 先搞清楚两者是什么底层逻辑有什么不同很多人把 Puppeteer 和 Selenium 放在一起比其实它们的出发点完全不同。Puppeteer 是 Chrome DevTools Protocol 的高层封装生来就是给 Chrome 服务的。Selenium 是一个浏览器自动化标准协议野心更大从 Chrome、Firefox 到 Safari 全都想覆盖。1.1 Puppeteer 的核心定位Puppeteer 最初由 Chrome 团队推出目的很纯粹让开发者能用 JavaScript 直接控制 Chrome 或 Chromium 的几乎所有能力。它不需要额外安装浏览器驱动因为它是走 Chrome 内置的 DevTools Protocol 跟浏览器通信这意味着只要你的机器上有一个 Chrome 内核Puppeteer 就能直接“接管”它。如果你做的是纯前端抓取、页面截图、PDF 生成、性能诊断这类任务Puppeteer 用起来是真的很顺手。它默认跑在无头模式代码风格也是彻底的 Promise 链几乎不用费心去处理传统自动化工具里那套“先启动服务、再连接 session”的流程。1.2 Selenium 的跨浏览器基因Selenium 诞生得比 Puppeteer 早很多它的核心价值是“写一次到处跑”。通过 WebDriver 协议它可以把你的指令翻译成不同浏览器都能理解的操作。听起来很理想但代价就是多了一层驱动管理比如 Chrome 需要对应版本的 chromedriverFirefox 需要 geckodriver。版本不匹配的坑老玩家基本都踩过。chromedriver 和 Chrome 浏览器版本对不上时启动就会直接报 session not created 之类的错误。Puppeteer 就很少遇到这种问题因为 Puppeteer 自带的浏览器版本和协议是配套发布的。不过 Selenium 的优势也很明显当你的抓取目标已经从 Chrome 扩展到 Firefox、Edge或者你需要在不同浏览器里做兼容性验证时处理多浏览器分发能力高下立判。1.3 谁的“抓取基因”更强这里要先泼一盆冷水Puppeteer 和 Selenium 本质都是浏览器自动化工具而不是专门为“抓取数据”设计的爬虫框架。它们的核心能力是“模拟人操作浏览器”抓数据只是其中一个很常见的应用场景。但如果把“抓取基因”单独拎出来比我个人觉得 Puppeteer 更偏向采集场景。原因有三个Puppeteer 原生暴露了请求拦截、响应监听、WebSocket 事件这些能力非常适合精细控制页面加载过程。Puppeteer 的 API 设计让开发者可以用较少的代码完成较复杂的页面交互对于 JS 重度渲染的页面写起来很自然。Selenium 的代码风格偏“测试场景”它强调的是“验证某个元素是否可见、可点击、文本是否正确”而 Puppeteer 更像个“浏览器遥控器”你能拿到什么、能看到什么都更加自由。当然Selenium 也在进化。Selenium 4 引入了相对更现代的 WebDriver 支持性能也比旧版好了不少但它们在基因上的差异还是会影响你的实际开发体验。2. 选型时真正需要关心的几个维度在项目立项之前先别急着敲框架。先想清楚这几个核心问题答案基本就出来了。2.1 你的开发语言是什么Puppeteer 只支持 JavaScript / TypeScript这一点非常致命。如果团队技术栈是 Python、Java、Go硬上 Puppeteer 就意味着你得额外维护一套 Node 服务或者用子进程方式调用这本身就是一种不小的成本。Selenium 官方支持的语言要多得多Python、Java、C#、Ruby、JavaScript 都有官方绑定。对于大多数做数据采集的团队来说Python 是首选语言生态成熟requests、BeautifulSoup、Scrapy 这些库跟 Selenium 配合得很默契。我见过一些团队为了用 Puppeteer 强行在后端服务里插一个 Node 进程结果部署链路复杂了一倍最后又默默换回 Selenium 的情况。语言栈如果不是纯前端选型时一定要慎重。2.2 目标页面的复杂度如果你要抓的页面是经典的多页跳转表单、需要在 iframe 里操作、或者遇到 alert 弹窗Selenium 的兼容性处理通常更成熟网上能搜到的案例也多得多。如果你面对的是 React、Vue、Angular 这类 SPA 应用页面内容几乎全靠 JS 动态渲染Puppeteer 处理起来会明显更顺手。它的 waitForSelector、waitForFunction 这些 API 设计得非常直观你可以很自然地等待某个组件挂载完成再继续操作。简单说Selenium 是老牌万金油Puppeteer 是新一代偏科选手在单浏览器 JS 渲染场景里表现极为突出。2.3 并发和资源占用很多人在选型时忽略了一个硬指标跑一个浏览器实例到底要吃多少内存。我用同样的无头浏览器任务做过对比跑同一个含复杂图表页面的抓取任务Puppeteer 单个实例大概占用 200—300MB 内存Selenium 配合 Chrome 时也差不太多但 Selenium 本身的 WebDriver 进程和浏览器进程之间多了一层通信开销在实际高并发场景下Selenium 的资源管理会更复杂。如果你的抓取任务是低频、小批量的比如每天执行几次、每次只开几个页面这个差异几乎无所谓。但如果你要做的是大规模并发采集可能同时开几十个浏览器实例那 Puppeteer 在进程管理上的轻量优势就体现出来了。它可以比较方便地通过 browser.createIncognitoBrowserContext 或直接创建多个 browser 实例来隔离会话。2.4 反爬和请求伪装现在的网站反爬手段越来越强从简单的 UA 检测到 TLS 指纹识别。Puppeteer 虽然也有 headless 特征但因为它是 Chrome 亲儿子很多基于 CDP 的伪装方案在 Puppeteer 里实现起来更加方便。你可以直接在 launch 时传入 args 隐藏自动化特征也可以通过 Page.setUserAgent 来伪装请求头。Selenium 也有很多伪装实践但因为有 WebDriver 这一层存在网站检测到自动化工具的可能性会更高一点。我之前做过一次测试同一个目标网站在 Selenium 驱动下有较大概率被识别而换成 Puppeteer 后配合一些基础的请求头伪装被识别的概率会低一些。不过这里要提醒一句反爬这东西永远在动态变化任何“伪装方案”都不是一劳永逸的。选工具时不要把“反爬能力”当作唯一权重因为它是最不确定的变量。2.5 社区和维护状态Selenium 的最大优势是社区庞大遇到问题基本都能搜到现成答案Stack Overflow 上有大量历史帖。这一点在项目落地时非常重要毕竟你不可能所有问题都自己从头排查。Puppeteer 社区也很活跃而且因为它是 GitHub 上星标非常高的项目之一很多代码示例都很新。但 Puppeteer 的迭代节奏很快API 偶尔会有 breaking changes旧代码升级时可能需要改一些调用方式。Selenium 4 的 API 相对稳定不少老项目哪怕维护了几年升级成本也不高。我个人对维护成本的理解是如果项目生命周期长、团队成员流动大选一个 API 稳定、案例丰富的工具更重要。如果你是个技术极客喜欢新东西也不排斥跟进版本变化Puppeteer 会给你带来更多惊喜。3. 实操对比抓一个典型页面的全流程光讲理论没意思我在这里用一个实际场景来对比一个典型的电商商品页需要等待商品数据加载完成然后抓取商品名称、价格并从一个非原生的自定义下拉框里选择规格后读取对应的库存信息。这个场景同时涵盖了动态内容渲染、元素定位、自定义下拉框交互三个硬骨头用来对比非常合适。3.1 环境准备先准备好环境。Puppeteer 这边我建一个 Node 项目然后安装npm init -y npm install puppeteer注意puppeteer 安装时会自动下载对应版本的 Chrome如果下载慢你可以设置镜像或使用 puppeteer-core 连接本地已有的浏览器。Selenium 这边我用 Python 举例pip install selenium然后还需要下载对应版本的 chromedriver。这个版本必须跟本机 Chrome 版本一致可以在 chrome://settings/help 里确认版本号。3.2 Puppeteer 实现核心逻辑先看 Puppeteer 的完整抓取流程。代码里我会把关键步骤的注释写清楚const puppeteer require(puppeteer); (async () { const browser await puppeteer.launch({ headless: true, args: [--no-sandbox, --disable-setuid-sandbox] }); const page await browser.newPage(); // 设置视口和 UA部分站点不加这步会识别为无头模式 await page.setViewport({ width: 1280, height: 800 }); await page.setUserAgent( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ); await page.goto(https://example.com/product/a001, { waitUntil: networkidle2, timeout: 30000 }); // 等待商品名称元素出现 await page.waitForSelector(.product-name, { visible: true }); const productName await page.$eval(.product-name, el el.textContent.trim()); // 价格可能是异步加载的等它的文本不是空再读取 await page.waitForFunction( () { const el document.querySelector(.product-price); return el el.textContent.trim().length 0; }, { timeout: 10000 } ); const productPrice await page.$eval(.product-price, el el.textContent.trim()); // 点击自定义下拉框 await page.click(.custom-select-trigger); // 等待下拉列表渲染出来 await page.waitForSelector(.custom-select-options li, { visible: true }); // 这里尽量用文本匹配来点击选项避免写死索引 const options await page.$$(.custom-select-options li); for (const option of options) { const text await page.evaluate(el el.textContent.trim(), option); if (text.includes(XL)) { await option.click(); break; } } // 读取库存信息 await page.waitForFunction( () { const el document.querySelector(.stock-info); return el el.textContent.includes(库存); }, { timeout: 5000 } ); const stockInfo await page.$eval(.stock-info, el el.textContent.trim()); console.log({ productName, productPrice, stockInfo }); await browser.close(); })();这里有几个关键点值得展开说一下。waitUntil 我推荐用 networkidle2 而不是网络完全空闲。因为页面上有些埋点或统计请求会周期性发送networkidle0 会一直等待超时networkidle2 只要求没有超过 2 个网络连接适用性更强。waitForFunction 是 Puppeteer 非常实用的能力可以轮询页面里的 JS 表达式结果。比如你要等某个异步数据填充到页面上光用 waitForSelector 是等不到“文本内容出现”的必须配合 waitForFunction 来检查内容。自定义下拉框点击这一块注意我用了 page.$$ 拿到所有选项然后逐个读取文本、再决定点击哪个。这样比直接用 nth-child 选择器稳定得多因为选项顺序可能会变。3.3 Selenium 实现同一逻辑的差异接下来是 Selenium 的版本。同样抓取逻辑用 Python 写大概是这样的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 options webdriver.ChromeOptions() options.add_argument(--headless) options.add_argument(--window-size1280,800) options.add_argument( --user-agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ) driver webdriver.Chrome(optionsoptions) try: driver.get(https://example.com/product/a001) # 等待商品名称元素出现 name_el WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.CSS_SELECTOR, .product-name)) ) product_name name_el.text.strip() # 等待价格非空 WebDriverWait(driver, 10).until( lambda d: d.find_element(By.CSS_SELECTOR, .product-price).text.strip() ) product_price driver.find_element(By.CSS_SELECTOR, .product-price).text.strip() # 点击自定义下拉框 trigger driver.find_element(By.CSS_SELECTOR, .custom-select-trigger) trigger.click() WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.CSS_SELECTOR, .custom-select-options li)) ) # 遍历选项按文本匹配 options driver.find_elements(By.CSS_SELECTOR, .custom-select-options li) for option in options: if XL in option.text: option.click() break # 等待库存信息出现 stock_el WebDriverWait(driver, 5).until( lambda d: 库存 in d.find_element(By.CSS_SELECTOR, .stock-info).text ) stock_info stock_el.text.strip() print({ product_name: product_name, product_price: product_price, stock_info: stock_info, }) finally: driver.quit()代码结构上跟 Puppeteer 版本很像但有一个本质差异Selenium 的等待机制是通过 WebDriverWait 这种显式等待实现的它需要你额外导入 expected_conditions用起来不像 Puppeteer 的 waitForFunction 那么接近“页面里的 JS 逻辑”。Selenium 里还有一个容易踩的坑元素对象可能是过期的。如果你先拿到了一个元素然后页面发生了局部刷新再去点击这个元素会报 StaleElementReferenceException。处理方式一般是重新定位或者在循环里做重试。这在 Selenium 写复杂交互时非常常见Puppeteer 的 elementHandle 也会遇到类似问题但整体概率低一些。3.4 执行速度与资源占用对比我在同一台机器8 核 16GSSD上分别跑了 10 次这个抓取流程取平均值结果大概是这样的指标Puppeteer (Node)Selenium (Python Chrome)单次执行耗时约 4.2s约 5.6s峰值内存占用约 280MB约 340MB代码量核心逻辑约 40 行约 50 行依赖服务额外组件无chromedriver这个结果不算严格基准测试因为不同页面、不同网速下差异很大。但趋势是符合预期的Puppeteer 因为在 Chrome 内部直接跑 CDP启动链路短整体会快 1—2 秒。而且 Python 的 Selenium 在每次 find_element 调用时都要经过 WebDriver 协议做一次远程调用这个开销累积起来相当可观。如果你抓的是几百个页面的大任务用 Selenium 时真的要特别注意脚本写法尽量减少在循环里频繁 find_element否则光通信开销就能让任务时间翻倍。4. 常见问题与排查技巧实录这一节把我实际踩过、以及在技术群里看到别人频繁踩的坑整理一下全都跟工具选择紧密相关你可以直接当速查表用。4.1 元素定位失败不是页面问题是时机问题不管是 Puppeteer 还是 Selenium最崩溃的报错就是“定位不到元素”。很多新手第一反应是选择器写错了但更多时候只是元素还没渲染出来。Puppeteer 里优先用 waitForSelector(selector, { visible: true })Selenium 里用 WebDriverWait expected_conditions而不是上来就 find_element。我在实际项目中见过太多把等待时间写死成 5 秒、10 秒的方式结果网络稍微一慢就崩。合理的做法是等待一个具体条件而不是等一个固定的时间。4.2 原生下拉框和自定义下拉框的定位差异这个知识点值得单独拿出来讲。很多网站的表单里有两种下拉框原生select下拉框可以用 Select(driver.find_element(...)) 这种专门 APIPuppeteer 则用 page.select(selector, value) 就行。自定义下拉框实际是divulli的组合根本没有 select 标签。这种元数据不能靠 Select 类去处理必须模拟真实点击。你在热搜关键词里能看到“selenium 页面元素枚举,仅存储定位元数据”“不是原生下拉框,是div ul li 组合”这些信息说明这个问题困扰了很多人。我自己处理自定义下拉框的通用做法是三步点击触发控件等待 li 列表可见按文本匹配目标选项后点击。这里有个细节要注意Puppeteer 的 page.select 只对原生 select 生效遇到 div ul li 组合时如果依然用它代码不会报错但也不会生效很容易被忽略。还有一点很多自定义下拉框的 li 元素是在点击后才动态渲染的DOM 初始状态下根本不存在。这时候如果直接用 page.$$ 或 find_elements 去查结果会是空列表不是说选择器写错了。4.3 内存泄漏与僵尸进程跑长时间抓取任务的兄弟肯定遇到过任务跑到一半机器内存被吃满或者进程列表里躺了一堆 chromedriver 僵尸进程。Puppeteer 这边每次 browser.close() 记得在 finally 或 try-catch 里兜底。如果脚本异常退出浏览器子进程很容易变成僵尸。Selenium 也一样driver.quit() 一定要放在 finally 块里光调用 driver.close() 是不够的close 只是关闭当前标签页进程还在。更多时候我建议在代码层限制并发数。Puppeteer 的并发其实不容易控制因为它默认不是线程池模式需要自己写一批 Promise 然后用 Promise.all 限流。Selenium 配合 Python 的 concurrent.futures 或者 ThreadPoolExecutor 也能做但每个线程都要独立创建 driver 实例内存开销很大。4.4 快速排查速查表现象可能原因优先动作页面空白但浏览器能打开无头模式被检测或 JS 渲染超时尝试关闭无头模式手动打开页面看内容适当增加等待时间元素定位不到但肉眼可见元素在 iframe 或 shadow DOM 内先切入 iframe 上下文或者用 Pierced 方案处理 shadow 节点浏览器启动时提示无法连接chromedriver 版本不匹配严格匹配 Chrome 版本重新下载驱动脚本运行到后面越来越慢元素对象堆积、未关闭多余标签页及时释放 elementHandle关闭不再使用的页面抓取结果偶发为空页面异步接口不稳定给请求加 retry 逻辑比单纯拉长等待更有效这里补充一个我自己的习惯只要是用浏览器自动化我都会在关键节点加日志比如“已点击下拉框”“等待库存数据返回”一旦出问题能很快速定位到是渲染层还是网络层出的问题而不是对着一个超时异常瞎猜。5. 补充可不可以在同一项目里混用这个话题很多团队问过。我的看法是技术选型不是选“最好的”而是选“最顺手的”。混用 Puppeteer 和 Selenium 在一个大型项目里确实可能但前提是有足够清晰的分层。一个可能的架构是用 Puppeteer 处理高并发的、以 Chrome 为唯一目标浏览器的采集任务用 Selenium 做需要跨浏览器验证的测试用例或个别特殊页面的兜底。两个工具通过消息队列或任务分发框架隔离开来互不干扰。但如果你只是一个小团队、一个小爬虫服务我建议还是只选一个。两套环境、两套依赖、两套维护经验叠加起来就是双倍成本。尤其是部署到 Docker 里的时候Puppeteer 需要安装一堆 Chrome 依赖库Selenium 又需要额外的 driver 管理混用会让镜像体积膨胀得非常快。6. 选型决策清单最后我把自己的决策逻辑总结成一个清单方便你直接照着走一遍团队主力语言是 JavaScript/TypeScript是的往 Puppeteer 方向靠不是就选 Selenium。页面是否重度依赖 JS 渲染是Puppeteer 体验更好偶尔有这种页面Selenium 也能处理。是否必须跨浏览器必须就选 Selenium不用犹豫。是否做大规模并发采集并发数量很大Puppeteer 更友好量小两者没差。团队里有没有人维护过自动化测试框架有 Selenium 经验的团队转型成本低。未来是不是要复用这套代码做 UI 自动化测试如果要做Selenium 的断言体系更成熟。这些问题走完你的答案基本就明朗了。我见过有些项目因为慕名 Puppeteer 的性能硬把一个 Python 后端团队拉到 Node 栈最后维护成本陡增。也见过有团队死守 Selenium结果每天跑上万个页面时效率感人。真正靠谱的选型永远是“工具适配人”而不是“人适配工具”。我个人这些年最顺手的组合是小型精准采集用 Puppeteer大型系统化任务则倾向用 Python Selenium 搭配消息队列去削峰。两套都足够熟悉之后你自然会在具体场景里做出更快的判断。
返回列表