☰
Selenium处理JavaScript渲染页面的爬虫实战指南
2026/10/7 10:29:44 网站建设 项目流程

你要是在爬虫这个圈子里混过一阵,大概率遇到过这种情况:requests 明明把 HTML 拿回来了,状态码 200,页面源代码里却找不到你要的数据。打开浏览器右键“查看网页源代码”,空空如也,但页面在浏览器里显示得清清楚楚——商品价格、榜单排名、滚动加载的内容,一样不缺。这不是浏览器在骗你,也不是 requests 出了毛病,而是目标页面用了 JavaScript 渲染:数据根本不是服务器第一屏返回的,而是浏览器加载完 HTML 之后,再用脚本调接口、拼数据、改 DOM。这篇文章就是专门讲怎么用 Selenium 处理 JavaScript 渲染页面,适合已经会 requests、但被动态页面卡住的爬虫开发者,也适合想把浏览器自动化能力并入采集项目的工程人员。

先说清楚一个前提:Selenium 本身不是爬虫框架,它是一个浏览器自动化工具。它做的事情很朴素——启动一个真实浏览器,帮你模拟点击、输入、滚动、等待,然后把浏览器里渲染完成后的内容交给你。很多刚接触动态页面的人第一反应是去抓包找接口,这当然是最优选,但现实里总有一些场景接口路径走不通。这时候,Selenium 就是我们手里最可靠的兜底方案。下面从原理到实操,把我踩过的坑和沉淀下来的经验一次性说透。

1. 从静态到动态:为什么 requests 拿不到 JS 渲染的数据

1.1 服务器返回的是“毛坯房”,装修是浏览器干的

传统静态页面的逻辑很简单:浏览器输入网址,服务器返回一段完整的 HTML,里面标题、正文、列表项全都写好了。requests 拿到这段 HTML,解析一下就能提取数据。但现代前端早就不是这个玩法了,页面结构往往分两层:第一层是服务器返回的 HTML 骨架,里面是空的容器节点和一堆<script>标签;第二层是浏览器执行脚本后,通过 XHR 或 fetch 请求拉取 JSON 数据,再经过 JavaScript 动态创建 DOM 节点,把数据“填”进页面里。

这个过程可以类比成买房子:服务器给你一套毛坯房,里面地板、墙面、家具一概没有,只有图纸和施工队;浏览器是装修队,拿着图纸(JavaScript 脚本),去建材市场(接口)买建材(JSON 数据),再亲手把家具摆进去。requests 干的事情是打开房门看了一眼毛坯房,然后告诉你“这房子里没什么可看的”,但你看页面时是装修完成后的样子。

举个例子,一个典型的异步加载页面,请求时序是这样的:

  1. 浏览器请求 HTML 文档,拿到骨架;
  2. 解析 HTML 时遇到<script>标签,下载并执行 JavaScript;
  3. JavaScript 发起 XHR/fetch 请求,向服务端要数据;
  4. 服务端返回 JSON,JavaScript 遍历数据并生成 DOM 节点;
  5. 浏览器完成布局和绘制,用户才看到完整内容。

requests 在第 1 步就止步了。它拿到的 HTML 里没有数据节点,自然提取不到任何有价值的信息。这也就是为什么很多人用 requests 抓动态页面,返回的结果里连个 select 选项都找不到。

1.2 两条破解路线:找接口,还是开浏览器

面对 JS 渲染页面,摆在爬虫开发者面前的有两条路线。第一条是抓包找接口。打开浏览器开发者工具的 Network 面板,刷新页面,过滤 XHR 和 fetch 请求,一般能看到一串接口返回 JSON 数据。直接用 requests 或者 httpx 模拟这些接口,往往就能拿到最干净的数据,这比解析 HTML 高效得多。

但接口路线有走不通的时候,而且实际项目里这种情况一点不少见:

  • 接口 URL 带了动态签名,比如?timestamp=xxx&sign=yyy,这个签名是 JavaScript 在内存里算出来的,你复制出来也没法带下一次;
  • 接口返回的是加密内容,需要在浏览器环境里解密才能看到明文;
  • 数据不是一次性加载,而是依赖用户行为触发,比如鼠标滚动、点击按钮、拖拽滑块;
  • 页面根本不上接口,纯靠 JavaScript 在本地生成数据,比如某些图表、某些 Canvas 绘制的验证码或报表;
  • 页面上有复杂的交互流程,比如多级筛选、日期联动、条件查询,你光请求一个初始接口根本拿不到目标数据。

这几类情况里,靠 requests 模拟接口的难度会指数级上升,投入产出比非常低。这时候就需要第二条路线:让一个真实浏览器替我们完成全部渲染和交互,最后直接把“装修完毕的房子”里的数据拿出来。Selenium 走的就是这条路。

2. Selenium 的原理与定位:它不是魔法,而是一套“操作系统级的遥控器”

2.1 WebDriver 协议与浏览器驱动:为什么浏览器愿意听你指挥

很多人第一次用 Selenium 的时候会困惑:我明明没有碰浏览器,代码怎么写了一句driver.get(url),浏览器就自己跳转过去了?这背后是 WebDriver 协议在起作用。

简单来说,Selenium 通过 WebDriver 协议与一个叫做“浏览器驱动”的中间程序通信。这个驱动本身是一个本地服务,负责把 Selenium 发来的命令翻译成浏览器能理解的原生指令。以 Chrome 为例,chromedriver 会通过 Chrome DevTools Protocol 与你机器上安装的 Chrome 浏览器建立连接,然后执行导航、查找元素、点击、输入、执行 JavaScript 等操作。

这个架构可以理解成一套无人机遥控系统:Selenium 是遥控器,WebDriver 协议是遥控信号,chromedriver 是信号转发器,Chrome 是无人机。遥控器不会直接碰无人机,但每条指令都能准确传达并执行。

正因为中间多了一层驱动,版本匹配就成了第一个经典坑:chromedriver 必须与浏览器主版本号一致。比如你本机 Chrome 是 126 版本,却装了 125 版本的 chromedriver,启动时大概率会报session not created: This version of ChromeDriver only supports Chrome version 125之类的错误。Firefox 用 geckodriver,Edge 用 msedgedriver,道理完全一样。

2.2 无头模式与有头模式:调试和上线要分开对待

Selenium 最常见的两个运行模式是有头模式和无头模式。有头模式会弹出一个真实浏览器窗口,每一步操作都肉眼可见,调试阶段强烈建议开着它:你能直观看到页面加载到哪一步、等待条件卡在哪里、元素到底有没有出现。无头模式不显示界面,占用资源更少,速度也更快,适合部署到服务器上定时跑。

Chrome 的无头模式这些年换过好几代写法。老版本用--headless,新版本推荐--headless=new,两者在功能完整性上有差异,部分页面老模式可能跑不动 JavaScript 或 canvas 渲染。如果你在无头模式下抓不到数据,但切回有头模式一切正常,先别急着怀疑代码,先把无头模式升级成新版本,或者给无头模式加上--disable-gpu、--no-sandbox这类参数再试。

2.3 环境搭建最容易翻车的三个细节

环境配置我见过的坑,集中在这三点:

报错或现象常见原因解决方向
WebDriverException: Message: unknown error: cannot find Chrome binarySelenium 找不到浏览器执行文件指定浏览器路径,如options.binary_location = "/usr/bin/google-chrome"
SessionNotCreatedException驱动版本与浏览器版本不匹配下载与浏览器主版本一致的驱动,或使用自动管理工具
有头模式正常、无头模式白屏或超时服务器缺少字体、libgbm、libnss3 等系统依赖安装依赖包,如apt-get install -y libgbm1 libnss3 libatk-bridge2.0-0

如果你不想手动下载驱动,可以考虑用webdriver-manager这个库自动匹配版本,或者直接用selenium-manager——Selenium 4.6 以上版本自带了这个功能,可以自动解析浏览器版本并下载对应驱动。不过在生产环境里,我还是建议显式指定驱动路径,避免每次启动都联网检查,也方便在内网环境部署。

2.4 最小可用 Demo:先跑通再谈优化

环境搭好之后,第一段代码不建议写太复杂,核心目标是跑通“启动浏览器、打开页面、拿点东西、关浏览器”这四步。下面这段是最小可用的 Selenium 脚本:

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=new") options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") driver = webdriver.Chrome(options=options) try: driver.get("https://example.com/dynamic-page") # 显式等待某个元素出现,最大等待时间 10 秒 element = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "data-container")) ) print(element.text) finally: driver.quit()

注意看WebDriverWait这一行,它是动态页面抓取的灵魂。没有它,你很可能在页面数据还没渲染出来时就急着提取,结果拿回一个空元素。关于等待策略的细节,后面有专门一章细说,这里先记住一条:凡是动态页面,等待是必需的,而且要用显式等待,别用死等。

3. 实操:从“空列表”到“正确数据”的全过程

3.1 场景设定:一个看似普通、实则要命的异步榜单页

为了把过程讲清楚,我虚构一个很典型的场景:有一个异步加载的排行榜页面,页面顶部是固定的标题和筛选条件,下方是一个榜单列表。列表数据不是服务器写在 HTML 里的,而是页面加载后通过 JavaScript 调用接口拿到的,接口返回前页面里只有一个空的<div id="rank-list"></div>。

如果用 requests 写第一版代码,大概是这样的:

import requests from lxml import html resp = requests.get("https://example.com/rank-page") tree = html.fromstring(resp.text) items = tree.xpath("//div[@id='rank-list']/div[@class='item']") print(len(items)) # 输出 0

列表项数量是 0,不是选择器写错了,而是请求的 HTML 里压根没有这些div.item。你在浏览器里看到的榜单,是 JavaScript 执行完以后才生成出来的,requests 根本走不到这一步。

3.2 Selenium 版本的完整代码与逐段解析

换 Selenium 之后,逻辑变成了“让浏览器打开页面 -> 等榜单出现 -> 提取数据”。完整的代码框架如下:

import time 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 def fetch_rank_data(): options = webdriver.ChromeOptions() options.add_argument("--headless=new") options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("excludeSwitches", ["enable-automation"]) driver = webdriver.Chrome(options=options) try: driver.get("https://example.com/rank-page") # 核心等待:确认榜单容器内的 .item 元素出现 WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.CSS_SELECTOR, "#rank-list .item")) ) # 提取所有榜单项 items = driver.find_elements(By.CSS_SELECTOR, "#rank-list .item") result = [] for item in items: title = item.find_element(By.CSS_SELECTOR, ".title").text.strip() score = item.find_element(By.CSS_SELECTOR, ".score").text.strip() result.append({"title": title, "score": score}) return result finally: driver.quit() if __name__ == "__main__": data = fetch_rank_data() print(data)

逐段来看:

  • options.add_argument("--disable-blink-features=AutomationControlled")和excludeSwitches这两行是降低自动化特征的常用手段。效果有限,且不同版本 Chrome 行为有差异,后面会专门讨论反检测这个话题,这里先不展开。
  • WebDriverWait的轮询默认是每 0.5 秒检查一次条件,直到超时。presence_of_element_located只检查元素是否出现在 DOM 里,并不保证可见,但对于绝大多数“等数据渲染出来”的场景已经足够。
  • find_elements返回的是一个元素列表,每个元素还可以继续调用find_element做相对查找,这样比全局 XPath 更稳定,不太容易因为页面上其他同类型元素干扰而定位错。

3.3 数据带出浏览器的三种方式:text、page_source 与 execute_script

榜单数据显示出来后,怎么把数据带出浏览器是很多人容易纠结的地方。我归纳出三种常用方式,各有适用场景:

第一种是用element.text直接拿元素的可见文本。适合榜单、评论、标题这类以文本为主的内容,代码简洁,但拿不到隐藏在属性里的数据,比如>data = driver.execute_script("return window.__INITIAL_STATE__;")

这是很多前后端分离项目的数据存放位置,用这种方式能一次性拿到整个页面的初始数据对象,效率往往比逐个节点爬要高得多。

4. 等待策略:Selenium 爬虫稳定性的分水岭

4.1 竞态条件的本质:去车站接人,你得知道她什么时候到

动态页面抓取最大的不稳定因素,就是“你启动代码的时候,页面数据可能还没渲染完”。请求已经发出,但 JavaScript 还在网络请求的路上;你的代码已经开始找元素了,但元素还没被创建出来。这不是 Selenium 慢,而是程序和数据之间发生了竞态条件——两个操作在同时赛跑,谁先结束不可预测。

我用等车来类比:如果对方告诉你“十分钟后到车站”,你在车站等十秒再开始找人就很容易错过;但如果你压根不知道对方几点到,那么就只能在车站一刻不停地盯着出口,一看到她出来就迎上去。Selenium 的显式等待,干的就是这个“盯着出口”的活儿。

4.2 三种等待方式的对比:为什么死等是最差的选择

Selenium 里有三种常见的等待方式,很多新手会混用,甚至从头到尾只用time.sleep,结果就是一套脚本要么慢得离谱,要么隔三差五跑飞。

等待方式实现方式优点缺点适用场景
强制等待time.sleep(5)简单,无脑时间修死,页面快也等、慢也等,本地跑没问题,线上部署容易超时几乎没有,能不用就不用
隐式等待driver.implicitly_wait(10)一次设置,全局生效只对元素查找生效,等待时间不可控,页面加载策略改变时会误导判断配合显式等待作为兜底
显式等待WebDriverWait(driver, 10).until(EC.xxx)绑定具体条件,精准,可轮询需要写更多代码动态渲染页面的首选

time.sleep的致命问题在于它不是“等数据出现”,而是“固定等一段时间”。如果网络慢,5 秒不够,数据没出现,代码继续往下跑,抓了个空;如果网络快,1 秒就加载完了,你却白白等了 9 秒。批量采集时积累下来,性能和稳定性双双牺牲。

隐式等待倒是解决了“轮询”的问题,它让find_element在找不到元素时最多等待指定时间,但它的粒度太粗,没法针对不同条件做差异化处理,也覆盖不了“等待某个文本出现”“等待某个元素可点击”这类复杂场景。我个人的习惯是写爬虫一律使用显式等待,必要时把所有find_element的兜底交给隐式等待,但不会依赖它来保证数据渲染完成。

4.3 我常用的六组 Expected Conditions 及其适用场景

显式等待的核心是expected_conditions(简称 EC),它提供了一批现成的条件判断。我做动态页面抓取时,最常用的有这几组:

  • presence_of_element_located:元素出现在 DOM 中,不管是否可见。适合“等数据容器出现”。
  • visibility_of_element_located:元素可见且宽高大于 0。适合“等弹窗显示出来再点关闭”。
  • element_to_be_clickable:元素可点击。适合“等某个按钮解除禁用状态”。
  • text_to_be_present_in_element:元素文本中包含指定文字。适合“等某个状态字段从‘加载中’变成‘成功’”。
  • element_to_be_selected:下拉框选项被选中。适合“等筛选条件生效”。
  • frame_to_be_available_and_switch_to_it:iframe 可用并自动切换进去。适合处理内嵌页面。

把它们封装成简单函数,可以让主流程代码干净很多。比如:

from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def wait_presence(driver, css_selector, timeout=10): return WebDriverWait(driver, timeout).until( EC.presence_of_element_located((By.CSS_SELECTOR, css_selector)) )

封装之后,主体代码里就是wait_presence(driver, "#rank-list .item")一行,可读性和复用性都上来了。

4.4 等待超时的排查思路:别急着加长超时,先看卡在哪一步

WebDriverWait超时会抛TimeoutException,很多人的第一反应是“超时不够长,改成 30 秒”,结果还是照样超时。正确的排查思路是搞清楚页面到底卡在哪个环节:

第一步,把页面截图保存下来。超时后执行driver.save_screenshot("debug.png"),看看浏览器当时长什么样。是白屏?是骨架加载出来了但没数据?还是弹了个登录框挡在前面?一张截图能帮你判断页面停在哪个阶段。

第二步,打印当前页面的信息。比如driver.title是否正常,driver.current_url是否符合预期,driver.page_source[:2000]里有没有容器节点。

第三步,把等待条件一步步拆小。先等最外层的<div id="rank-list">,再等里面的.item。如果外层等了很久才出现,说明接口响应慢或者被限流;如果外层秒出现、内层迟迟不来,说明渲染逻辑出了问题,可能是接口失败后前端没有兜底处理。

排查超时问题,思路比工具重要。加长超时只是把症状掩盖了,真正要解决的是“为什么数据在预期时间内没有到达”。

5. 进阶场景的坑与解法:iframe、弹窗、无限滚动与动态点选

5.1 元素明明就在页面上,却一直定位不到?先看看它在不在 iframe 里

iframe 是 Selenium 爬虫里最常见也最隐蔽的坑。页面主页面上有一个容器,里面其实嵌着另一个页面的 iframe,你需要操作的元素全在那个 iframe 里。此时用driver.find_element去页面上直接找,怎么找都找不到——因为 iframe 相当于一个独立的文档,当前上下文还在主页面上。

正确的做法是先切换到 iframe,再操作里面的元素。切回主页面时用driver.switch_to.default_content()。切换方式有三种:

# 按索引切换 driver.switch_to.frame(0) # 按 name 或 id 切换 driver.switch_to.frame("mainFrame") # 按元素切换 frame_element = driver.find_element(By.CSS_SELECTOR, "iframe[src*='report']") driver.switch_to.frame(frame_element)

在实际的项目里,我遇到过嵌套两层 iframe 的情况:主页里嵌一个大的框架页,框架页里又嵌一个报表页。处理这种场景的通用套路是写一个递归切页函数,依次找到每个 iframe 元素并切进去,找不到再回退到上一层。抓完后按反序把上下文一层层退回来。这个操作细节不多,但真的会卡住很多人。

5.2 新标签页、alert 弹窗与登录态:窗口句柄是绕不开的概念

页面在某个操作之后会新开一个浏览器标签页,这个场景在登录流程、报表跳转流程里非常常见。Selenium 默认只操作最初的那个标签页,新打开的你得手动切换。切换依赖的是窗口句柄(window handle),可以理解成每个标签页的身份证号。

# 记住操作前的句柄,等新窗口打开后再切换 main_handle = driver.current_window_handle driver.find_element(By.ID, "open-report").click() WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) > 1) new_handle = [h for h in driver.window_handles if h != main_handle][0] driver.switch_to.window(new_handle)

还有一种是 JavaScript 的 alert 弹窗。它和普通页面弹层不是一回事,alert 会阻塞页面交互,必须用driver.switch_to.alert.accept()或.dismiss()来关闭。一旦弹窗没处理,后续所有元素操作都会卡住。我习惯在每次点击可能触发弹窗的按钮之后,主动先检查有没有 alert 需要关闭:

try: alert = driver.switch_to.alert print(alert.text) alert.accept() except Exception: pass

登录态的切换则涉及到 Cookie 的处理。Selenium 打开的新浏览器是干净的 session,没有登录态,很多页面会因此和正常用户看到的内容不一样。如果你的采集任务需要登录后的数据,可以先把登录后的 Cookie 用driver.get_cookies()导出,下次启动时用driver.add_cookie()导入。注意add_cookie之前必须先访问一次目标域名的任意页面,否则浏览器不会允许设置该域名下的 Cookie。

5.3 无限滚动加载:滚动事件才是数据刷新的触发器

很多榜单和信息流页面做的是无限滚动:页面底部接近视口时,触发新一批数据的加载。Selenium 抓这种页面,核心不是去找接口,而是重复执行“滚动到底部 -> 等待新元素出现 -> 再滚动”的循环。

我用的滚动方案有两种。第一种是直接window.scrollTo(0, document.body.scrollHeight),简单粗暴;第二种更接近用户行为,通过scrollIntoView让最后一个循环触发页面加载:

last_height = driver.execute_script("return document.body.scrollHeight") for _ in range(20): driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") time.sleep(1.5) new_height = driver.execute_script("return document.body.scrollHeight") if new_height == last_height: break last_height = new_height

注意time.sleep(1.5)在这里也是不得已,因为滚动加载的触发时机没法用某个 EC 精准卡住——你根本不知道这次滚动会加载多少条数据。更好的做法是记录当前元素数量,等数量增加到预期值再继续下一轮,但这会让代码复杂度上升。对多数场景来说,上面的循环已经够稳定了。

这里还要警惕一个和“echart 闪烁”“前端动画”相关的问题:有些页面在数据加载完成后还有过度动画,元素高度一直在变,滚动时容易误触发懒加载。我的经验是,滚动循环结束后不要立刻提取数据,再time.sleep(1)让布局稳定一下,这样拿到的element.text才不会是动画中间态。

5.4 关于 webdriver 特征检测,说几句正经话

动态页面和反爬是一体两面。前端开发人员有一万种方法判断你是不是自动化脚本,比如检测navigator.webdriver属性、检测浏览器指纹、检测鼠标轨迹、检测启动参数。这也是 Selenium 抓取和正常浏览行为差异最大的地方。

先说合规前提:任何爬虫开发都要先确认目标网站的 robots.txt 和服务条款,以及数据的使用方式是否合法。我这里讲检测原理是为了帮助大家理解为什么自己的脚本会被挡,也是为了让内部系统的自动化测试、自有平台的数据巡检更稳定,不是鼓励大家绕过反爬去抓不该抓的数据。

检测和控制是场军备竞赛。navigator.webdriver的值是可以通过execute_script改掉或者通过 CDP 附加参数隐藏的,但网站也可以用更复杂的指纹策略来识别。我的态度是:不要把精力全部放在反检测上,更不要指望一劳永逸。真正长期稳定的方式是优先走合法接口、申请授权、控制访问频率、做好数据合规。Selenium 的价值在于解决“页面不渲染就没有数据”的技术问题,而不是解决“该不该抓”的规则问题。

6. 性能优化与工具箱里的其他选择

6.1 Selenium 慢在哪:重新审视每次都“重装系统”的代价

Selenium 被诟病最多的是性能。同样是抓一个列表页,requests 可能 0.5 秒就结束了请求,Selenium 从启动浏览器到渲染完成可能要 5 到 10 秒。慢的原因有三块:一是每次启动都是全新的浏览器进程,加载扩展、初始化渲染进程、建立连接都要时间;二是页面渲染过程本身耗时,尤其是图片、视频、字体这类资源;三是等待策略如果写得粗糙,比如用了大段time.sleep,时间就被白白浪费了。

理解了慢的构成,提速就有方向。最直接的三个手段:开无头模式、启用pageLoadStrategy降低加载策略、禁用无关资源。

pageLoadStrategy是一个很容易被忽视的参数,它控制driver.get()什么时候返回。默认是normal,要等页面完全加载完才继续执行;改成eager可以做到 DOM 访问完成、但静态资源还没全部加载完就返回;none则只要求页面开始加载就返回,元素定位完全交给显式等待。但注意,eager和none都可能导致你需要的元素还没出现就往下跑,所以必须配合显式等待使用。

禁用图片和媒体资源,对提速也很有效。一个 2MB 的页面可能 1.5MB 都是图片,禁用之后网络耗时能削掉一大半。

6.2 复用浏览器会话:不需要每次重新“装修房子”

如果你每天要跑同一个需要登录态的采集任务,最省时间的做法不是每次都开新浏览器,而是复用一个已经登录好的浏览器进程。Chrome 支持通过调试端口启动,之后 Selenium 只要连接这个端口,就能操作同一个浏览器实例。

chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-profile

启动之后,代码里通过debuggerAddress直接加入这个会话:

options = webdriver.ChromeOptions() options.add_experimental_option("debuggerAddress", "127.0.0.1:9222") driver = webdriver.Chrome(options=options)

这样做的好处是显而易见的:你把登录、人工验证、初始化这些步骤统统治做好了,Selenium 只需要负责采集。缺点是端口复用方案在 CI 环境里不太好配,但如果你是自己写脚本在本地跑,这套方案非常实用。

6.3 替代方案对比:Playwright、Puppeteer 与 Selenium 怎么选

把 Selenium 摸透之后,了解一下同类工具的边界是很有必要的。Playwright 和 Puppeteer 是这两年很热门的浏览器自动化库,它们比 Selenium 更年轻,设计更现代。

对比维度SeleniumPlaywrightPuppeteer
支持浏览器Chrome/Firefox/Edge/SafariChromium/Firefox/WebKitChromium/Firefox 有限支持
语言Java/Python/C#/JS 等Python/Java/JS/.NET主要为 Node.js
自动等待需显式写等待条件内置自动等待,默认开启内置部分自动等待
生态与文档最老牌,案例多快速增长,文档现代偏前端团队
技术栈依赖驱动版本常要手动管理自带浏览器下载和管理自带浏览器下载和管理

如果你是从零开始一个新项目,我会建议试试 Playwright,它的自动等待机制能把“元素还没渲染出来”这类问题减少一大半。但如果你维护的是老项目、团队里只有你会 Python、且目标是快速解决问题,那 Selenium 依然是非常可靠的选择——它虽然笨重,但足够稳定,踩坑的人也足够多,遇到任何问题基本都能搜到答案。

6.4 工具箱化的下一步:从单条抓取到分布式采集

学会 Selenium 处理 JS 渲染页面之后,很多人会想把它塞进更大的采集系统里。我见过的配置方式大致分两种:第一种是把 Selenium 采集封装成独立服务,通过消息队列接收 URL 任务,抓完数据后写入结果队列;第二种是配合爬虫可视化界面做成“爬虫工具箱”,让非技术同事也能配置简单的采集规则。

这两种方向的共同点是:Selenium 管浏览器的部分,调度和存储交给框架。如果你要抓的量比较大,一台机器开 50 个浏览器实例是不现实的,更合理的方案是每台机器只跑固定数量的浏览器进程,机器之间通过队列协调任务。这也是分布式爬虫的基本形态。等你到了这一步再回头看,会发现 Selenium 只是整个链路里的一个环节,但它恰恰是很多动态页面场景里绕不开的那个环节。

我在实际项目里的习惯是:能找接口就先找接口,接口拿不到再上 Selenium;Selenium 能解决就不上更重的东西。它在性能上确实不算优雅,但在“非得让浏览器干这件事”的场景里,它足够可靠。这套思路,配合显式等待和循环滚动,已经帮我处理过大大小小几十个动态页面。希望这篇文章的踩坑记录,能让你少走我走过的那几段弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询