Selenium实战:破解JavaScript渲染难题,搞定动态页面爬取
做爬虫的朋友大概率都有过这种体验目标页面的数据明明写在浏览器里可你用requests把网页源码抓回来翻遍整个 HTML 却只看到框架、脚本和空壳。问题就出在 JavaScript 渲染上——页面里的数据是浏览器执行完脚本之后才动态生成的Python 的静态请求根本没这个机会。这种时候Selenium 就成了最顺手的工具让它启动一个真实浏览器加载页面、执行 JS、模拟点击和滚动最后再把渲染之后的 DOM 数据捞出来。这篇文章不是基础教程而是围绕“处理 JavaScript 渲染”这个高难度场景把 Selenium 的核心打法和盘托出什么情况下必须上 Selenium、等待和交互怎么写才稳、取数和调试有哪些独家经验。我会尽量用实际操作中积累的例子来讲读完你至少能少踩几十个常见的坑。1. 为什么静态请求拿不到动态页面认识 JavaScript 渲染1.1 一次完整的浏览器渲染流程拆解很多初学者会把“爬虫抓 HTML”和“浏览器看页面”理解成同一件事其实差别很大。你可以把服务器返回的 HTML 看成一份“装修图纸”上面写的不是直接能用的家具而是“这里要放一个柜子”“那里要并排放三把椅子”。浏览器拿到图纸后会先解析 HTML构建出 DOM 树然后遇到script标签就停下来把 JavaScript 代码交给引擎执行。代码里可能又有fetch()、XMLHttpRequest或者 WebSocket 请求浏览器去后端接口拿到数据后再把结果写进 DOM最终呈现给你看到的完整页面。这个流程如果细拆大概是请求 HTML → 解析 DOM / CSS → 执行 JavaScript → 发起异步请求 → 动态修改 DOM → 最终渲染完成。requests这类工具只能走到第一步后面的执行、异步请求、修改 DOM 它都没办法参与。所以哪怕页面上已经显示了 50 条商品信息你抓回来的源码里也只有一个空空的div idapp/div。打个生活化的比方你让 requests 去饭店后厨拿菜谱它能拿到的只是“菜单第 3 页有红烧肉”这样的静态文本但真正端到你面前的菜是厨师浏览器引擎照着菜谱现炒的锅里的香气、颜色、分量requests 根本看不见。Selenium 干的事情就是派一个真人浏览器进厨房把最终做好的菜端出来给你挑。1.2 请求到的页面和最终渲染后的页面差距有多大举个我曾经实际处理的例子。某个数据平台的列表页浏览器里显示着完整的卡片信息、评分、跳转链接我用requests请求后发现返回的 HTML 里不仅数据是空的连负责填充数据的接口地址都找不到。页面里只有一个window.__INITIAL_STATE__变量里面是空的。再翻网络面板才发现页面加载后会调用一个带签名参数的接口参数是用 JS 动态生成的每次刷新都不一样。这种场景下光靠分析和拼装请求参数成本会非常高。更麻烦的是requests还处理不了下面几类情况页面是 Vue 或 React 写的单页应用路由切换后 DOM 完全由 JS 生成数据通过 WebSocket 实时推送静态请求抓到源码时数据还没到需要先点击某个按钮、打开某个弹窗才会发起新的请求页面数据绘制在 Canvas 上文字只是渲染后的像素点DOM 里根本捞不到原文。我把常见的页面类型做了一个对比你就知道什么时候该上 Selenium 了页面类型数据结构静态 requests 可行性是否建议 Selenium纯静态 HTML数据直接写在源码里完全可行不需要服务端模板渲染数据由模板填充源码中直接可见可行但要注意编码和分页可选前端框架 普通接口数据由接口返回DOM 由 JS 构建可尝试直接抓接口接口复杂时建议前端框架 动态签名 / 复杂交互数据依赖执行 JS、点击、滚动、登录态基本不可行强烈建议很多爬虫项目一上来就选 Selenium其实不是最优解。正确做法是先抓源码人工或脚本快速判断数据是否直接存在。只有确认数据确实依赖 JS 渲染并且没有更简单的接口方案时才把 Selenium 搬出来。这个判断做对了能帮你省下大量不必要的资源和时间。1.3 哪些场景必须用 Selenium 接管结合我自己的经验下面这些场景基本可以无脑选择 Selenium第一页面需要真实用户行为才能触发数据加载。比如电商页面要往下滑动才会请求“猜你喜欢”新闻页面要点击“加载更多”按钮才会追加新内容。Selenium 最大的优势就是能模拟真实交互而不是干巴巴地发请求。第二数据依赖登录后的身份状态。很多站点的核心数据必须登录后才能看到登录过程本身又有 JS 加密、识别码等逻辑。用 Selenium 可以完整走一遍登录流程让浏览器持有真实的会话状态后续访问自然会带上 Cookie。第三页面经过多层跳转和重定向。你用 requests 跟踪 302 可能没问题但若页面里有meta refresh、window.location跳转、iframe 嵌套静态请求根本吃不消Selenium 却可以直接等到最终页面出现。第四你需要验证脚本执行后的页面效果。这其实是自动化测试思维有时候爬出来的数据不是重点页面在某次操作后是否出现报错、图表是否闪烁、渲染是否完整这些交互后的状态只有真实浏览器能反馈。2. 用 Selenium 接管浏览器环境准备与方案选型2.1 为什么选 Selenium 而不是其他工具Selenium 的全名叫 Selenium WebDriver它本质上是一个操作浏览器的标准化协议。Chrome、Firefox、Edge 等浏览器都实现了对应的驱动你写的 Python 代码通过 WebDriver 协议把指令发给浏览器驱动驱动再转发给浏览器内核从而实现“打开网页 → 点击 → 输入 → 滑动 → 取值”的一系列操作。选择 Selenium 的理由有几个一是它足够成熟网上资料多遇到问题基本都能搜到解决方案二是跨浏览器、跨语言支持非常好Java、Python、C#、Ruby 都可以三是它不需要修改被访问网站的代码原生模拟浏览器环境对 JavaScript 渲染的兼容性最好。很多大厂的自动化测试框架也基于它所以遇到复杂页面时它的上限很高。当然Selenium 不是完全没有替代品。后端的 Playwright、Pyppeteer 也都能处理渲染问题我在第 5 章会专门做一个对比。但如果你现在已经开始用 Selenium 了先把这套体系玩透再谈迁移也来得及。2.2 环境搭建与最容易踩的版本坑Python 环境下的安装很简单两条命令就能完成基本搭建pip install selenium pip install webdriver-manager第二项是 WebDriver Manager强烈建议装上。它最大的用处是自动下载和你本地 Chrome 版本匹配的驱动文件省去手动去chromedriver.chromium.org找驱动、解压、放路径的麻烦。基础启动代码长这样from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager options webdriver.ChromeOptions() driver webdriver.Chrome(serviceService(ChromeDriverManager().install()), optionsoptions) driver.get(https://example.com) print(driver.title) driver.quit()这套流程最难受的坑就是版本匹配。Chrome 和 ChromeDriver 之间存在严格的对应关系比如你本机 Chrome 升到了某个新版本而 Chromedriver 还停在旧版本启动时会直接报session not created或This version of ChromeDriver only supports Chrome version xx。刚开始做爬虫的人遇到这个报错会一脸懵其实不用慌把 WebDriver Manager 换成最新版让它重新拉取匹配的驱动就行。还有一点很多人容易忽略如果你的生产服务器上没有 Chrome只有 Chromium 或 Edge驱动也得跟着换。比如用 Edge 浏览器就要用EdgeChromiumDriverManager代码里改成webdriver.Edge(...)。驱动、浏览器类型、浏览器版本三者必须对齐这是使用 Selenium 的底层逻辑。2.3 浏览器参数配置每个参数都对应一个坑Selenium 启动浏览器时可以加载一堆选项最常见的参数模板我整理出来供你参考from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headlessnew) # 无头模式不弹窗口 options.add_argument(--disable-gpu) # 关闭 GPU 加速兼容性更强 options.add_argument(--no-sandbox) # 某些 Linux 服务器必须加 options.add_argument(--ignore-certificate-errors) # 忽略证书错误 options.add_argument(--window-size1920,1080) # 设置窗口大小 options.add_argument(--user-agentMozilla/5.0 ...) # 自定义 UA options.add_argument(--disable-blink-featuresAutomationControlled) options.page_load_strategy eager # 页面加载策略 prefs {profile.managed_default_content_settings.images: 2} options.add_experimental_option(prefs, prefs) # 关闭图片加载逐个细讲一下--headlessnew是 Chrome 新版的无头模式。老版本用--headless换成 Chrome 109 之后建议用--headlessnew因为新版在隐藏模式下也能保留更多浏览器行为对检测的抵抗性更强但代价是有些页面在无头模式下渲染出来的结构会不一样。明明手动打开浏览器数据正常的页面换成无头模式就找不到元素这种问题我也遇过多次。遇到这种情况别急着改代码先去掉 headless 参数用有头模式跑一遍确认问题是否只出现在无头模式下。--ignore-certificate-errors不是必须的但很多测试环境的 https 证书不完整不加这个参数页面直接白屏。page_load_strategy eager会让driver.get()在 DOM 加载完成后就立刻返回不用等图片、脚本全部加载完。这个参数对性能优化极其重要后面我会单独展开。prefs里的profile.managed_default_content_settings.images设成 2意思是禁用图片资源这对抓取文字类数据能节省大量带宽和加载时间。3. 核心实操等待、点击、滑动、取值3.1 等待策略向 sleep 硬等说再见用 Selenium 处理 JavaScript 渲染页面时最多人犯的错误就是一上来写time.sleep(3)。这种做法非常不稳定网络快的时候页面 1 秒就渲染完了你还在傻等 2 秒网络慢的时候 10 秒还没出来你只等 3 秒元素根本找不到。正确做法是显式等待和隐式等待配合让浏览器根据实际情况动态决定等待时间。隐式等待的意思是告诉 WebDriver在查到某个元素之前最多等这么久。设置一次全局生效driver.implicitly_wait(10)隐式等待有个局限性它只在查找元素的时候生效无法判断元素是否可见、可点击、还是已经消失。所以更可靠的是显式等待配合expected_conditions使用from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, timeout10, poll_frequency0.5) element wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, .list-item)))这段代码的意思是最多等 10 秒每 0.5 秒检查一次直到页面上的.list-item元素可见才继续执行。如果超时还没出现就抛出TimeoutException。比较常用的等待条件我整理成了速查表等待条件判断内容典型场景presence_of_element_located元素是否出现在 DOM 中页面刚加载完元素还没完全可见时visibility_of_element_located元素是否可见需要点击或读取文本时element_to_be_clickable元素是否可见且可点击点击按钮前frame_to_be_available_and_switch_to_itiframe 是否可用页面数据封装在 iframe 中staleness_of元素是否已经不在 DOM 中判断旧元素是否被刷新替换text_to_be_present_in_element元素内是否包含指定文本等待异步数据填充完成实战中我更推荐优先写显式等待不要依赖sleep。只有在确实无法用条件判断、必须等某个计时器走完的情况下才用sleep兜底而且要尽量把等待时间缩短。3.2 模拟用户交互点击、输入、滚动加载JavaScript 渲染页面另一个常见特征是用户操作会触发新的内容。比如点“加载更多”按钮、输入关键词搜索、横向滑动切换 tab、滚动到页面底部触发懒加载。这些动作 Selenium 都能模拟但很多细节不到位就会失败。先看最常见的点击“加载更多”按钮。这类按钮往往在数据流末尾需要先滚动到可见位置再点击否则 WebDriver 会提示element is not interactablefrom selenium.webdriver.common.action_chains import ActionChains load_more wait.until(EC.element_to_be_clickable((By.XPATH, //button[contains(text(), 加载更多)]))) driver.execute_script(arguments[0].scrollIntoView();, load_more) load_more.click() # 点击后等待新的列表项出现 wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, .card-item)))再来看页面滚动。很多页面的数据是“无限滚动”模式往下滑到一定位置才会加载一批新数据。滚到底部的通用写法是driver.execute_script(window.scrollTo(0, document.body.scrollHeight))如果页面是在某个容器内部滚动不是整个窗口滚动这个写法就没用了。你需要定位到那个容器然后修改它的scrollTopcontainer driver.find_element(By.CSS_SELECTOR, .scroll-container) driver.execute_script(arguments[0].scrollTop arguments[0].scrollHeight, container)热搜词里提到的“selenium 网页左右滑动”在横向轮播或者图表翻页场景中也很常见。横向滚动可以借助ActionChains比如向右侧移动鼠标from selenium.webdriver.common.action_chains import ActionChains slider driver.find_element(By.CSS_SELECTOR, .horizontal-panel) ActionChains(driver).move_to_element(slider).move_by_offset(300, 0).perform() driver.execute_script(arguments[0].scrollLeft 500, driver.find_element(By.CSS_SELECTOR, .horizontal-panel))滚动式加载的数据有一个天然的不稳定性你无法确认数据是不是已经全部加载完了。我在实战里会用一个“等到数据条数稳定”的策略不断滚动到底部连续两次检测到同样的列表长度和底部标记才认为加载结束def wait_for_scroll_finish(driver, locator, max_rounds10): last_count 0 unchanged_rounds 0 for _ in range(max_rounds): driver.execute_script(window.scrollTo(0, document.body.scrollHeight)) time.sleep(1) # 这里只能小幅度等待给渲染留时间 elements driver.find_elements(*locator) current_count len(elements) if current_count last_count: unchanged_rounds 1 if unchanged_rounds 2: return elements else: unchanged_rounds 0 last_count current_count return elementstime.sleep(1)这种写法我前面刚批评过但在这个场景里反而合理因为滚动触发加载的行为没有统一的 DOM 标记可以用很难用 expected_conditions 精确判断只能给一个小缓冲时间然后用轮询逻辑兜底。注意这里的关键是“连续两次长度不变就结束”而不是盲目等待固定的 10 秒。3.3 数据提取的几种姿势文本、属性、执行 JS 取值元素定位到了数据提取就有多种方式。最常规的是取文本和取属性cards driver.find_elements(By.CSS_SELECTOR, .card-item) for card in cards: title card.find_element(By.CSS_SELECTOR, .title).text price card.find_element(By.CSS_SELECTOR, .price).text link card.find_element(By.CSS_SELECTOR, a).get_attribute(href) print(title, price, link)这里有个性能细节如果在循环里每次都调用driver.find_elements重新查整个页面速度会非常慢。正确做法是先把一页的卡片列表一次性查出来然后再逐个解析减少浏览器和 Python 之间的通信次数。如果页面数据特别多可以考虑先用 XPath 把关键字段全部提取出来再配合execute_script一次性把多个值返回data driver.execute_script( const items []; document.querySelectorAll(.card-item).forEach(el { items.push({ title: el.querySelector(.title)?.innerText, price: el.querySelector(.price)?.innerText, link: el.querySelector(a)?.href }); }); return items; ) print(data)这种写法把查找逻辑直接交给浏览器原生 JS速度几乎吊打 Python 层反复查找。因为 Selenium 每调一次find_element都要走 WebDriver 协议跨进程通信很多次而execute_script只通信一次只是 JavaScript 返回的数据结构必须是 JSON 可序列化的。取数过程中还有一个老生常谈但高频踩坑的点iframe。很多第三方数据、登录弹窗都嵌在 iframe 里直接用driver.find_element会报no such element。原因是 WebDriver 默认只在当前主文档的 DOM 中查找不会自动进入 iframe。解决方案是显式切换# 先等待 iframe 可用并切进去 frame wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, mainFrame))) # 切进去之后查找操作都在 iframe 内生效 content driver.find_element(By.CSS_SELECTOR, .main-content).text # 处理完后切回主文档 driver.switch_to.default_content()等你切完 iframe再想找主文档里的元素一定要记得切回来不然就会一头雾水地报element not found。新窗口的切换逻辑类似先记录当前窗口句柄打开新窗口后driver.switch_to.window(driver.window_handles[-1])操作完再切回原来的窗口句柄。3.4 等页面“真正稳定”的做法处理复杂页面时我一般会额外封装一个“等待网络空闲”的思路。Selenium 本身没有直接提供“网络空闲”的 API但可以通过判定 DOM 状态来间接实现。前文已经展示了滚动加载的稳定判断这里再补充一个更通用的轮询方法等待页面上某个标志元素出现且连续两次查询结果一致。比如你要抓一个图表页面会先显示“加载中”然后渲染出图表 SVG。你可以在代码里先轮询等待“加载中”消失再等待图表容器的节点数稳定def wait_until_stable(driver, css_selector, timeout20, stable_rounds2): start time.time() last_value None stable_count 0 while time.time() - start timeout: try: elems driver.find_elements(By.CSS_SELECTOR, css_selector) current_sign (len(elems), driver.execute_script(return document.body.innerText.length)) except Exception: current_sign None if current_sign last_value: stable_count 1 if stable_count stable_rounds: return True else: stable_count 0 last_value current_sign time.sleep(0.5) return False这个函数把“元素数量”和“页面文本总长度”组合成稳定信号只有连续两次采样一致才认为页面稳定。实际跑起来比固定等待可靠得多尤其在网络波动大的环境下能明显减少因页面未渲染完成导致的抓取失败。4. 常见问题与排查技巧实录4.1 元素定位失败按这个顺序排查我调试 Selenium 脚本时遇到过太多NoSuchElementException。新手的反应往往是改选择器但其实问题可能根本不在选择器上。我建议你按下面的顺序排查排查点现象验证方法页面还没有渲染完元素报找不到但手动打开能看到打印driver.page_source看有没有相关节点元素在 iframe 中主文档找不到检查页面源码里有没有iframe元素不是静态节点数据是 JS 异步填充用time.sleep(2)再查一次确认是否动态出现选择器写错报错但页面源码有在浏览器 DevTools 里手动验证选择器元素被遮罩层挡住元素存在但提示不可交互检查页面是否有弹窗、浮层覆盖页面跳转到了新 URL在旧页面找新页面的元素打印driver.current_url确认地址我自己最常用的调试手段是在except分支打印当前页面的关键快照然后保存到本地try: element wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, .target))) except Exception as e: print(当前URL:, driver.current_url) print(页面源码长度:, len(driver.page_source)) with open(debug.html, w, encodingutf-8) as f: f.write(driver.page_source) raise e这一步看起来简单但能帮你快速定位如果保存出来的 HTML 里压根没有你想找的节点说明问题在“页面渲染没完成”或“选错了页面层级”如果节点存在但代码找不到那大概率是 iframe 或 new window 没切换。4.2 页面加载超时与卡死的处理办法Selenium 默认会等页面完全加载完成才执行下一步这对图片多、广告多、埋点脚本多的页面非常不友好。我处理过很多次TimeoutException的崩溃后来总结了三个方面的优化。第一设置页面加载策略。把page_load_strategy从默认的normal改成eager代表 DOM readyState 变为interactive时就认为加载完成不再等待所有资源。这样可以显著提升速度options.page_load_strategy eager第二限制浏览器加载无关资源。很多站点真正需要的数据只是文本和结构图片、字体、视频完全可以关掉。用前面提到的 prefs 配置关闭图片后页面加载时间经常能缩短一半以上。第三给driver.get()包一层超时保护。有的页面会进入诡异的“一直加载中”状态哪怕设置了页面加载策略也无济于事。这时候可以用driver.set_page_load_timeout(20)限制整体加载时间超时就捕获异常重试。配合重试逻辑代码稳定性会好很多from selenium.common.exceptions import TimeoutException for attempt in range(3): try: driver.set_page_load_timeout(20) driver.get(url) break except TimeoutException: driver.execute_script(window.stop()) print(第, attempt 1, 次加载超时强制停止)driver.execute_script(window.stop())是大杀器页面还在加载时强制命令浏览器停止后续资源的获取此时已经渲染出来的 DOM 仍然可以照常操作。这个技巧非常实用很多复杂页面就是靠它强行拉回节奏的。另外页面卡死还会表现为点击按钮无反应。此时先别急着点击检查是不是有弹窗遮挡。如果遇到了 JavaScript 的原生弹窗alert、confirm、prompt需要先接受或关闭它from selenium.webdriver.common.alert import Alert Alert(driver).accept() # 接受弹窗相当于点击确定 Alert(driver).dismiss() # 取消弹窗原生弹窗会阻塞页面脚本执行如果代码里没处理后面的操作会一直等待超时。换个角度说能用完成.until判断弹窗是否存在是防止卡死的最稳手段。4.3 如何快速定位 JavaScript 运行时报错抓动态页面时经常会有一种“潜水”的感觉你看到的页面是 JS 执行完之后的样子但 JS 在哪个环节报错了浏览器控制台的信息你不一定看得到。好在 Selenium 提供了读取浏览器日志的能力你可以在关键时刻把日志捞出来logs driver.get_log(browser) for log_entry in logs: if log_entry[level] SEVERE: print(JS报错:, log_entry[message])用这个方法我抓过一个渲染不完全的图表页面。当时页面大部分数据都正常只有最后几行是空的前端控制台报了一个Cannot read property x of undefined。看到这条报错后我才明白页面内部异步组件的渲染顺序有问题于是加了一个前置的等待条件问题直接解决。不过要注意driver.get_log(browser)必须在 ChromeDriver 开启日志能力时才有效如果没有任何输出可以加上下面的参数再试options.set_capability(goog:loggingPrefs, {browser: ALL})另外execute_script执行自定义 JS 报错时异常信息是标准 JavaScript 错误文本和浏览器控制台里的基本一致。遇到报错先看清是哪一行、哪个属性出了问题比盲目改代码高效得多。4.4 关于“反爬”和自动化的边界问题热搜词里出现“python selenium反爬虫”说明很多人都关心 Selenium 会不会被识别。这个问题我简单说清楚网站完全可以通过 JS 检测window.navigator.webdriver属性、浏览器的插件对象、行为轨迹等指标判断访问是否由自动化工具驱动。部分网站还会加上人机验证、请求频率限制和 IP 封禁策略。我认为做自动化测试和合法爬虫核心不是写一套“绝对不被识别”的伪装方案而是要搞清楚目标站点允许什么、禁止什么。可以合理做的事情包括遵循网站的robots.txt约定、控制抓取频率、不恶意占用服务器资源、不对登录后的私密信息做批量采集。我在实际项目中更多时间花在优化等待策略和选对等待条件上而不是想方设法绕过识别机制。即使要调整用户代理或浏览器参数也应当保持在“模拟正常用户访问”的合理范围内而不是积极规避安全机制。爬虫本身是个中性技术能不能持续用取决于你有没有尊重目标平台的数据边界。这个边界划定清楚你后续写代码心里才有底。5. 性能优化与备选方案5.1 单实例多页面任务少开浏览器一个刚接触 Selenium 的人最容易写出这样的代码在循环里反复driver webdriver.Chrome()处理完一个页面就driver.quit()下一个页面再从头启动。这个写法在功能上没问题但性能上非常差。每次启动 Chrome 都要加载插件、初始化上下文、建立 WebDriver 连接耗时少则几秒多则十几秒。如果一个任务要跑几千个 URL总耗时会多出非常多。更好的做法是让无头浏览器常驻。只要页面之间相互独立就可以在一个 driver 实例上连续处理driver webdriver.Chrome(optionsoptions) try: urls [https://a.com, https://b.com, https://c.com] for url in urls: driver.get(url) # 处理当前URL的逻辑 finally: driver.quit()需要注意的是连续访问不同站点时上一个站点的 localStorage、Cookie 可能会保留。如果你要处理的数据不希望被“串味”可以在每次driver.get()前清理driver.delete_all_cookies() driver.execute_script(window.localStorage.clear();)这里也涉及一个取舍如果你需要使用同一个登录态处理多个页面那就不能删除 Cookie。最好根据实际业务决定清理策略。5.2 并发爬虫为什么慎用 Selenium谈到性能很多人第一反应是“开多线程”。但在 Selenium 场景里多开 10 个浏览器实例你的内存和 CPU 就会爆炸。一个典型 Chrome 实例的内存占用大约在 200MB 到 500MB取决于页面复杂度这么一算并发 5 个就可能吃掉 2GB 以上内存。还有一个更隐蔽的问题每个 driver 实例都要占用端口和进程资源数量一多系统文件句柄不够用到处是奇怪异常。如果你确实需要并发我的建议有两点一是严格控制并发数单机跑 2 到 4 个实例已经很多二是优先考虑用多进程而不是多线程因为浏览器进程本身就是高耗资源的多线程在 Python 里还受 GIL 限制收益不大。考虑到资源占用我更推荐大家优先用第一节说过的思路能请接口就直接请求接口Selenium 只处理最棘手的渲染页面。5.3 Selenium 之外的替代方案怎么选Selenium 虽强但它也有一些固有的麻烦比如版本匹配、需要手动等待、异常处理繁琐。近几年 Playwright 和 Pyppeteer 也很流行很多人问我该不该换。Playwright 的核心优势是内置了自动等待机制page.locator操作元素时会自己等待元素可见不需要你手写WebDriverWait还支持网络请求拦截可以直接 mock 接口数据而且它的浏览器驱动是内置的不需要额外下载。对于新项目我会优先考虑 Playwright。Pyppeteer 是 Puppeteer 的 Python 移植版也能处理 JS 渲染不过社区维护不如前两者活跃。Selenium 的优势则在于生态成熟、岗位需求大、老项目里很多代码都是基于它的学了不容易浪费。简单总结一下我的选型思路工具优点适合场景Selenium成熟、资料全、兼容性好老项目、测试框架、跨语言需求Playwright自动等待、网络拦截、驱动内置新项目、复杂交互、需要高性能Pyppeteer简单易上手临时任务、轻量需求说实话工具本身差异并不大真正影响开发效率的是你对页面渲染机制的熟悉程度。Selenium 你玩得转转到 Playwright 也就是半天的事。5.4 沉淀一套自己的页面渲染处理套路最后想分享一个我在多个项目里验证过的个人习惯。处理任何 JavaScript 渲染页面我不会马上写抓取逻辑而是先打开浏览器 DevTools 的 Network 面板把页面从请求开始到数据完整的整个时间线过一遍。看哪些请求是数据接口哪些请求是追踪脚本页面在什么时机触发第二次加载数据是直接在 DOM 里还是在 iframe 里。这个过程可能只花 10 分钟但能帮你省下后面写代码、调试的半天时间。等代码写完我还会刻意观察跑动过程的浏览器日志看有没有报错、有没有因为等待不足而白跑的情况。久而久之你会形成一套非常稳定的处理流程先判断是否需要 Selenium再配置浏览器参数然后用显式等待和稳定轮询处理动态加载最后提取数据并做好异常兜底。另外一个我私藏的技巧是如果页面数据量很大尽量在execute_script里用原生 JS 把列表数据一次性取出而不是用 Python 层反复遍历。这种做法不仅能降低 WebDriver 的通信次数还能让代码的整体耗时下降一个数量级。遇到特别复杂的页面我甚至会先写一个纯 JS 的数据提取函数在 DevTools 里调试好再粘贴到 Selenium 脚本里跑效率高得多。爬虫做到后面拼的其实不是用了多聪明的工具而是你对浏览器渲染过程的理解深不深。Selenium 只是把你从“只能看到 HTML 源码”的局限里解放出来的第一步真正的进阶是你懂得在合适的位置等待、合适的方式取数以及在各种异常出现时快速定位问题所在的那种判断力。