做Selenium自动化做了几年,我最大的感受是:能被find_element和click解决的需求,只能算 Selenium 的入门玩法。真正让你在复杂页面、登录态、无人值守环境中活下来的,往往是三样进阶技能——JavaScript 执行、Cookie 管理和浏览器配置。很多同学脚本写了一半跑不动,报ElementClickInterceptedException、InvalidCookieDomainException,或者一开无头模式就各种崩溃,根本原因不是定位没用对,而是这三块能力没补齐。
这篇文章我就以“JavaScript执行 + Cookie管理 + 浏览器配置”为主线,把我在实际项目里踩过的坑、验证过的方案,一次性讲透。不看那种罗列 API 的文档式教程,只写能落地的实战操作。适合正在写自动化测试脚本的朋友,也适合拿 Selenium 做数据采集、做系统间数据同步的同学参考,哪怕你只用过 Selenium 基础功能,按这篇文章的步骤也能直接上手。
1. 为什么是这三板斧:先理清解决的核心问题
1.1 三个技巧各自的定位和适用场景
JavaScript 执行解决的是“页面不配合”的问题。Selenium 的本质是模拟用户操作,但页面的真实状态往往比理想要复杂得多:元素被遮罩挡住点不到、输入框是readonly状态、页面要滚动到特定位置才加载数据、日期控件内部压根不是标准 input。这些场景下,通过driver.execute_script()直接往页面上下文里注入 JavaScript,可以绕过很多阻塞。
Cookie 管理解决的是“会话状态”的问题。自动化过程里最烦人的就是登录,验证码、扫码、短信验证码这类环节,你想全自动过基本不可能。Cookie 是服务器用来识别你身份的凭证,只要把登录后的 Cookie 保存下来,下次启动时直接加载,就能跳过整个登录流程。更进一步,还能把 Selenium 里的 Cookie 转给普通 HTTP 库,用于接口请求。
浏览器配置解决的是“运行环境”的问题。自动化脚本最终都要跑到服务器、CI 环境或者无头模式下,而这些环境跟本机开发环境有巨大差异。无头模式要不要开 GPU、怎么隐藏自动化特征、下载文件的默认目录怎么改、Linux 服务器上如何避免资源限制导致的崩溃,这些都属于浏览器配置的范畴。
1.2 什么时候应该用 Selenium,什么时候不该用
很多人把 Selenium 当成万能工具,遇到什么需求都拿它上,其实完全没必要。一个最简单的判断标准:目标站点如果数据需要 JS 渲染才能出现,或者你要验证的是完整用户路径(登录、点击、跳转、提交),那就用 Selenium;如果只是请求一个接口拿 JSON 数据,直接requests或者httpx更合适,效率和稳定性都高一个量级。
我自己的经验是,Selenium 最擅长的是“一两个账号,处理复杂交互”,最不擅长的是“大规模并发抓取”。一个浏览器实例至少占两三百兆内存,开十个并发就能把普通服务器拖垮。真要搞大规模采集,常规做法是:用 Selenium 负责登录和拿 Cookie,拿到之后交给requests去做后续的高并发请求。这个思路后面我会专门演示。
1.3 基础环境怎么搭,别在版本上栽跟头
开始正文之前,先确认一下环境。我用的是 Python + Selenium 4.x,这是目前最主流、也最省事的组合。Selenium 4.6 之后自带 Selenium Manager,会自动去下载匹配的浏览器驱动,不再需要你手动折腾所谓“驱动和浏览器版本号一致”的问题。如果你用的是刚装的 Chrome,那就直接写:
from selenium import webdriver driver = webdriver.Chrome()如果你还在用老版本 Selenium,或者卡在某一段网上抄来的旧代码里,我建议尽早升级到新版本。旧代码里那些webdriver.Chrome(executable_path=...)的写法在新版本里已经废弃了,不光是写法变了,底层通信方式也变了,继续守着老版本只会给自己添堵。
2. JavaScript 执行实战:从入门写法到花式操作
2.1 execute_script 的基础用法与返回值类型
JavaScript 执行的核心就一个接口:execute_script。它能接收一段 JavaScript 字符串,以及任意数量的参数,这些参数以arguments[0]、arguments[1]的形式传递到脚本中。
# 最基础的用法:返回页面标题 title = driver.execute_script("return document.title") print(title) # 带参数传递:滚动到指定元素位置 element = driver.find_element(By.ID, "content") driver.execute_script("arguments[0].scrollIntoView();", element) # 带参数传递:修改元素文本 input_box = driver.find_element(By.NAME, "keyword") driver.execute_script("arguments[0].value = 'selenium';", input_box)这里要特别注意两件事。第一,JavaScript 字符串里如果写了return,execute_script会返回表达式的值;如果不写return,默认返回None。第二,返回值不一定是普通数据类型,如果你返回的是页面上的一个 DOM 节点,Selenium 会自动把它包装成WebElement对象返回,这样就能继续用element.text、element.click()这类方法了。
JS 字符串的引号嵌套是个高频坑。比如你要往脚本里动态塞一个字符串变量,直接拼字符串很容易因为引号问题报 JS 语法错误。我的建议是,尽量用arguments传参,而不是把变量直接拼进代码字符串里,这样既安全又清晰。
2.2 强制点击被遮挡元素,绕过拦截的老大难
ElementClickInterceptedException应该是 Selenium 使用者见过最多的异常之一。页面上飘着一个加载中的遮罩层、一个弹窗背景层、或者一个悬浮的广告,目标元素明明能定位到,但就是点不到。这时候有两种处理思路。
第一种是先把遮挡物移开,比如用 JS 把遮罩层的display改成none:
driver.execute_script("document.querySelector('.mask').style.display = 'none';")第二种思路更直接,用 JS 原生click()方法强制触发点击事件:
element = driver.find_element(By.XPATH, "//button[contains(text(),'确定')]") driver.execute_script("arguments[0].click();", element)JS 的.click()跟 Selenium 的.click()底层机制不一样。Selenium 的点击会先检查元素是否可见、是否可点击,然后模拟真实鼠标事件,整个过程更接近用户行为;JS 的.click()是直接派发一个点击事件,不管元素有没有被遮挡,只要事件监听器存在就能触发。所以它能绕过拦截,但它也会跳过一些页面自带的校验逻辑,容易造成“按钮点了但请求没发出去”的假象。
我自己的经验是,能用普通点击就用普通点击,JS 强制点击是最后的手段。如果页面因为响应速度导致遮罩层一闪而过,优先用显式等待让遮罩消失,而不是无脑上 JS 点击,否则后续逻辑里某些依赖用户事件的地方可能不生效。
2.3 修改元素属性,处理 readonly、disabled 和日期控件的骚操作
readonly和disabled是自动化脚本的常见拦路虎。比如一个只读输入框,send_keys往里面输入内容时,Selenium 会报ElementNotInteractableException。这其实是浏览器的原生行为在起作用,想绕过它,就得从 DOM 层面把属性改掉。
# 去掉 readonly 属性 input_box = driver.find_element(By.ID, "birthday_input") driver.execute_script("arguments[0].removeAttribute('readonly');", input_box) input_box.send_keys("1990-01-01") # 或者直接通过 JS 给 value 赋值 driver.execute_script("arguments[0].value = '1990-01-01';", input_box)给value赋值等于绕过输入事件,能解决大部分控件无法send_keys的问题。但要注意,某些前端框架(比如 Vue、React)并不会感知直接赋value的操作,虽然 DOM 上看起来值变了,框架内部的数据模型没变,提交表单时依然拿不到这个值。这时候需要额外触发原生的input事件:
driver.execute_script(""" var input = arguments[0]; var nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; nativeInputValueSetter.call(input, '1990-01-01'); input.dispatchEvent(new Event('input', { bubbles: true })); """, input_box)这段代码的核心逻辑是:找HTMLInputElement.prototype上value属性的原生 setter,把值设置进去,再手动派发一个input事件。这样 Vue/React 的监听器就能收到变化,页面数据的联动也会跟着触发。类似的手法在处理自定义下拉框、富文本编辑器的时候都很有用,属于高频代码片段,建议收藏。
2.4 滚动加载与懒加载页面的完整采集方案
现在很多页面都采用无限滚动加载,页面高度会随着滚动不断变化。用肉眼滚动很简单,脚本想要拿全所有数据,核心思路是“滚动到底部 -> 等待新内容加载 -> 再滚”。一个基础的循环实现长这样:
import time while True: # 记录当前页面高度 last_height = driver.execute_script("return document.body.scrollHeight") # 滚动到底部 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这个循环什么时候停?当滚动后页面高度不再变化,说明没有新的内容加载了。实际操作时我习惯再加一个兜底逻辑:连续多次高度不变才退出,因为有些页面异步请求比较慢,高度短暂停顿后还会继续增长。
还有一种情况是页面内部滚动容器,而不是整个body滚动。比如弹窗里的列表,这类场景要把 JS 里的滚动目标改成内部容器:
driver.execute_script("arguments[0].scrollTop = arguments[0].scrollHeight;", container)判断滚动是否到底,可以对比scrollTop + clientHeight和scrollHeight。如果它们相等,说明容器已经滚到底了。这也是前端判断滚动到底部的标准方法,拿到 Selenium 里来管理懒加载同样适用。
2.5 iframe 与 Shadow DOM 里的元素操作技巧
iframe 是 Selenium 操作里一个永恒的话题。iframe相当于页面里嵌入的另一个文档,顶层页面的选择器进不去,必须先通过switch_to.frame切到对应上下文。自动化里最烦的是两种:一是 iframe 没有id、name等属性,二是多个 iframe 嵌套。
没有属性时用 Selenium 自带的切换方式也能处理,比如先定位 iframe 元素再切:
iframe = driver.find_element(By.TAG_NAME, "iframe") driver.switch_to.frame(iframe) # 操作 iframe 内的元素 driver.find_element(By.ID, "inner_button").click() # 切回主文档 driver.switch_to.default_content()Shadow DOM 就复杂得多。它是一种封装的 DOM 子树,普通find_element根本定位不到它内部的元素。这时候 JS 的优势就体现出来了,可以直接拿到shadowRoot再往下找元素:
element = driver.execute_script(""" return document.querySelector('my-component').shadowRoot .querySelector('.inner-element'); """)注意返回的是WebElement,可以直接继续操作。这类写法在处理自定义组件(比如某些 UI 库里的复杂控件)时非常好用,属于真正的“官方 API 做不到、JS 手法必杀”的典型场景。
3. Cookie 管理实战:登录一次,长跑半年
3.1 Cookie 的读取、添加和删除基本操作
Selenium 对 Cookie 的操作接口并不复杂,无非是四种:获取、添加、删除单个、删除全部。
# 获取所有 cookie cookies = driver.get_cookies() # 添加 cookie driver.add_cookie({"name": "token", "value": "xxxx"}) # 删除单个 cookie driver.delete_cookie("token") # 删除全部 cookie driver.delete_all_cookies()每个 Cookie 的本质是一个字典,至少包含name、value两个字段,还可能包含domain、path、expiry、secure、httpOnly等字段。domain表示这个 Cookie 在哪个域名下生效,path表示它在哪个路径下生效,expiry是过期时间,secure表示是否只在 HTTPS 连接下传输。大部分 Cookie 的字段你不需要手动构造,因为从get_cookies()拿到的数据本身就是完整的,直接用就行。
有一点值得注意:Cookie 里的expiry是 Unix 时间戳,单位是秒。如果你想判断某个 Cookie 是否快过期,可以用 Python 的datetime做一下转换,别把单位搞错,否则会得到完全错误的过期判断结果。
3.2 添加 Cookie 前必须先访问域名,这个坑太常见了
添加自定义 Cookie 时最容易遇到的就是InvalidCookieDomainException,报错信息类似“invalid cookie domain”。这个异常出现的根本原因是:你还没有访问过目标域名的页面,浏览器上下文里根本没有这个域,自然也就无法注入该域下的 Cookie。
所以正确操作顺序是:先driver.get("https://目标站点"),哪怕只是打开首页,再去add_cookie,顺序反了必然报错。
driver.get("https://example.com") driver.add_cookie({"name": "sessionid", "value": "abc123"}) driver.refresh() # 这时候登录态才生效另外还有一个细节:Selenium 的 Cookie 模型跟浏览器里的 Cookie 模型并不完全一致。比如现代浏览器的一些 Cookie 带了SameSite属性,某些add_cookie版本对部分字段的支持有差异,实际操作时如果单独添加某个 Cookie 报错,可以用 try-except 跳过那些异常字段,只保留name、value、domain、path这几个关键字段再试。
3.3 登录状态持久化,用本地文件免去每次手动登录
这是自动化项目里能节省大量时间的核心方案。思路很简单:第一次手动或半自动登录完成后,把当前的 Cookie 序列化保存到本地文件;后续每次脚本启动时直接加载 Cookie,跳过登录流程。
import pickle # 保存 Cookie 到本地 cookies = driver.get_cookies() with open("session.pkl", "wb") as f: pickle.dump(cookies, f) # 加载 Cookie with open("session.pkl", "rb") as f: cookies = pickle.load(f) driver.get("https://目标站点") for cookie in cookies: try: driver.add_cookie(cookie) except Exception: # 过滤掉字段不兼容的 cookie,保留正常的 continue driver.refresh()操作顺序记住:必须先get一次目标域名,再挨个add_cookie,最后refresh()让页面重新走一遍请求。加一个异常处理是为了防止某些 Cookie 字段在当前浏览器版本里不受支持导致中断。保存 Cookie 的文件要注意保密,它等同于你的登录凭证,别传到公开仓库里。
加载 Cookie 后建议做一个有效性校验。最粗暴也最有效的办法:跳转到登录后才能访问的页面,判断页面是否包含特定元素,比如用户名、头像、退出按钮等。如果找不到,就说明 Cookie 已经失效,需要重新走登录流程。这个逻辑可以做成一个独立的函数,每次脚本启动时调用。
3.4 进阶玩法:Selenium 拿 Cookie 转给 requests 做接口请求
Selenium 的浏览器实例是重资源,一个页面的加载时间在一两秒以上,做高并发完全没戏。我实际的替代方案是:用 Selenium 登录并获取 Cookie,之后把 Cookie 交给requests直接请求接口,并发能力和稳定性都有质的提升。
import requests cookies = driver.get_cookies() session = requests.Session() for cookie in cookies: session.cookies.set(cookie["name"], cookie["value"]) response = session.get("https://目标站点/api/data", timeout=10)很多人会以为要把domain、path这些字段全部带上,其实requests的 SimpleCookie 模型只需要name和value,域名和路径是由请求 URL 自动处理的。如果你要同时跑多个账号,可以把账号和 Cookie 文件做映射,一个账号对应一个Session对象,配合线程池做任务分发,整个系统就变成了一个轻量级的高并发采集平台。
这个方案有一个前提:目标站点的数据接口必须能直接通过接口拿到,并且你对接口参数有足够了解。如果接口加了签名、加密参数之类的机制,那就老老实实用 Selenium 处理页面,别硬拆。判断基准是,先用浏览器开发者工具看一眼网络请求,如果有现成的 JSON 接口能用,才值得做这一步。
3.5 Cookie 失效的几个常见原因
Cookie 莫名其妙失效,是这类方案最容易翻车的地方。我总结过最常见的几种原因:
- 过期时间到了,尤其是一些平台的会话有效期很短,半天就过期。
- Cookie 里的某些字段在传输过程中被浏览器拒绝,比如
SameSite冲突、Secure字段要求 HTTPS 环境等。 - 登录设备或浏览器指纹变化。部分风控严格的站点会把浏览器指纹和会话绑定,换机器、换内核、改 UA,原本的 Cookie 直接作废。
- 域名范围出错。保存的是主域名的 Cookie,但你用子域名去请求,Cookie 可能带不过去。
排查方法也很简单:加载完 Cookie 后用浏览器的开发者工具看一下当前站点实际有哪些 Cookie,跟保存的文件做对比。如果关键 Cookie 缺失,大概率是add_cookie环节被异常过滤掉了;如果 Cookie 都在但仍未登录,那就是风控层面不认当前环境。
4. 浏览器配置实战:让自动化稳定地跑在无人值守环境
4.1 ChromeOptions 常用参数,一张表看清取舍
浏览器配置的核心入口是ChromeOptions。它负责在浏览器启动前注入各种开关和配置项。我把最常见的参数整理成一张表,方便你直接对照着用。
| 参数 | 作用 | 使用场景 |
|---|---|---|
--headless=new | 无头模式运行,不显示界面 | 服务器、CI 环境跑脚本 |
--disable-gpu | 禁用 GPU 硬件加速 | 无头模式常配合使用,避免渲染问题 |
--window-size=1920,1080 | 设置窗口大小 | 页面按固定视口渲染,截图更可控 |
--no-sandbox | 禁用沙箱 | Linux 容器环境中必须加 |
--disable-dev-shm-usage | 解决/dev/shm空间不足 | Docker 容器环境必须加 |
--disable-extensions | 禁用扩展 | 减少干扰、减少资源开销 |
--lang=zh-CN | 设置浏览器语言 | 页面语言不一致时用到 |
--incognito | 无痕模式 | 不保留缓存和本地数据时使用 |
--disable-infobars | 禁用信息栏 | 老版本浏览器提示“正在被自动化控制”时 |
--disable-blink-features=AutomationControlled | 关闭自动化控制标记 | 降低被识别为自动化工具的概率 |
参数不是越多越好,关键是理解每个参数解决什么问题,按需添加。比如本地开发环境完全不用加--no-sandbox,但在 Docker 里跑不加就大概率报错。我把它们分成三类:第一类是性能相关的(禁用 GPU、禁用扩展),第二类是环境相关的(no-sandbox、dev-shm),第三类是伪装相关的(UA、自动化特征隐藏)。分类记比死记硬背好用得多。
4.2 合理设置 user-agent 和浏览器指纹
自动化脚本跑起来,第一个被目标网站怀疑的就是浏览器的身份信息。普通 Selenium 启动的 Chrome,UA 字符串本身和正常浏览器差别不大,但window.navigator.webdriver这个字段默认是true,这是最明显的自动化特征。处理方式有两种。
第一种,通过启动参数控制:
options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") options.add_argument("--disable-blink-features=AutomationControlled")第二种,通过 CDP 命令在新页面加载前注入脚本,覆盖掉webdriver属性:
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { "source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})" })两种方式可以搭配使用,实测效果更稳。不过要提醒一句,这些技巧只应该用来处理你拥有合法授权或者明确允许自动化的目标站点。做技术验证、写测试脚本没问题,但不要用在有明确服务条款限制的平台上,自动化不等于可以为所欲为。
4.3 无头模式、下载目录和文件处理的完整配置
无头模式是跑无人值守脚本的标配,但它有几个实际坑。第一个坑是下载文件不好找默认路径。无头模式下浏览器的默认下载目录是系统临时目录,脚本都不知道文件下到哪了。解决办法是通过prefs指定一个固定下载目录:
prefs = { "download.default_directory": "/data/downloads", "download.prompt_for_download": False, "download.directory_upgrade": True, "safebrowsing.enabled": True, } options.add_experimental_option("prefs", prefs)第二个坑是无头模式某些页面可能渲染异常。比如有些页面在无头模式下无法获取某些系统字体、视频硬解码能力不同,导致页面错位、按钮不可见。这时候我建议用--window-size锁定一个合理的视口大小,尽量模拟标准屏幕的渲染结果。实在不行,可以退一步用带界面的方式跑,通过 Linux 下的虚拟显示方案解决。
第三,下载文件之后要等文件真正落盘再继续操作,不能一触发下载就去读文件。我的习惯是写一个文件等待函数,不断轮询目标目录下是否出现指定文件,并判断文件大小是否不再变化。这个细节看着小,实际跑批脚本时经常是崩溃点。
4.4 稳定性和性能参数调优
除了功能性配置,稳定性在长期跑批的场合比什么都重要。最经典的两个参数是--no-sandbox和--disable-dev-shm-usage。在 Docker 容器里,/dev/shm默认只有 64M,浏览器稍一忙就报session deleted because of page crash,加了--disable-dev-shm-usage之后,浏览器改用临时目录,崩溃率大幅下降。
页面加载超时也是一个高频问题。默认情况下 Selenium 会等页面完全加载,如果某个请求卡死,整个脚本就卡住不动了。建议加上超时控制:
driver.set_page_load_timeout(30) driver.set_script_timeout(10) driver.implicitly_wait(10)三个超时各管一件事:set_page_load_timeout管页面加载整体超时,set_script_timeout管异步 JS 超时,implicitly_wait管元素查找等待。配合显式等待,基本能覆盖大部分超时场景。
还有一点容易被忽略:不需要的图片资源会拖慢页面加载。如果你只关心接口数据或文本内容,可以禁用图片加载:
prefs = {"profile.managed_default_content_settings.images": 2} options.add_experimental_option("prefs", prefs)这个配置在采集场景里能让单页面加载时间明显缩短,但如果是视觉回归测试,千万别关图片。
5. 高频报错排查与经验清单
5.1 高频报错速查表
把 Selenium 实战中遇到的高频报错和对应的解决方案整理成一张表,基本上能覆盖日常开发中的八成问题。
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
element not interactable | 元素不可交互,常见于 disabled、readonly、被遮挡 | 用 JS 改属性后再操作,或显式等待元素可交互 |
element click intercepted | 元素被其他元素遮挡 | 移开遮挡物或用 JS 强制click() |
no such element | 元素定位不到 | 排查 iframe、Shadow DOM、页面未加载完,配合显式等待 |
invalid cookie domain | 添加 Cookie 前未访问目标域名 | 先get域名再add_cookie |
session deleted because of page crash | 浏览器崩溃,常见于 Docker 容器 | 加--no-sandbox、--disable-dev-shm-usage |
session not created | 驱动版本与浏览器版本不匹配 | 升级 Selenium,用自带的驱动管理器 |
timeout: Timed out receiving message from renderer | 页面加载超时 | 设置页面加载超时,改用显式等待 |
The current frame is not the target frame | iframe 切换混乱 | 确认当前上下文,切换前打印当前 frame |
排查这类问题有个通用铁律:先看错误信息的前半句话,别盯着后半句看。no such element后面通常会跟着一长串建议,真正关键的是这个异常之前、你在哪个页面状态下发生的问题。每个异常信息里的元素定位表达式、当前页面 URL 都值得打印出来辅助判断。
5.2 一个人调试脚本时省时间的技巧
写 Selenium 脚本最怕的就是“跑十分钟才报错,报错信息还看不懂”。我习惯在脚本里内置两个调试工具:页面截图和 DOM 导出。每次操作关键步骤后存一份截图,报错时把当前页面 HTML 也存一份,这样能快速还原现场。
import os import time def debug_snapshot(driver, tag=""): os.makedirs("debug", exist_ok=True) ts = int(time.time()) driver.save_screenshot(f"debug/shot_{tag}_{ts}.png") with open(f"debug/dom_{tag}_{ts}.html", "w", encoding="utf-8") as f: f.write(driver.page_source)不要小看这个简单的工具函数。很多页面异常在报错之前就已经出现征兆,比如某段关键内容没渲染出来、某个按钮灰掉了一直没恢复。有了截图和 DOM,你就能像看录像一样复盘每一步操作,而不是凭记忆猜。
调试时有个技巧是“二分法注释”。如果一段长脚本跑到后面才报错,先把后半段注释掉,跑前半段,确认前半段没问题,再逐步放开。这个思路看似笨,但比满屏打印日志、靠猜定位要省时间得多。配合显式等待和步骤标签,绝大多数问题十分钟内能定位。
5.3 等待策略是万金油,但别滥用 time.sleep
很多人写 Selenium 脚本,习惯性在每步操作后面加一句time.sleep(2),简单粗暴,但最大的坑是:固定等待时间既可能过长拖慢脚本,也可能过短导致元素还没渲染出来。正确做法是优先用显式等待。
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait = WebDriverWait(driver, 10) # 等待元素出现在 DOM 中且可见 button = wait.until( EC.element_to_be_clickable((By.ID, "submit_button")) ) # 等待某个元素消失 wait.until( EC.invisibility_of_element_located((By.CLASS_NAME, "loading")) )我还经常用 JavaScript 来判断页面加载状态,特别是做页面跳转之后:
wait.until(lambda d: d.execute_script("return document.readyState") == "complete")这行代码比time.sleep(3)高级的地方在于:它真正等到页面加载完成才继续走下一条指令,时间代价最小。显式等待的until内部是每百毫秒轮询一次条件是否成立,条件不满足就继续等,直到超时。
5.4 提前避开环境差异,写出可移植的脚本
同一套脚本在你电脑上跑得好好的,放到服务器上就各种报错,这是环境差异造成的。我在移植脚本时一般会检查三件事:浏览器有没有装、驱动能不能自动匹配、文件和路径的写法是否兼容 Linux。
options = webdriver.ChromeOptions() # 判断系统环境,动态设置参数 import platform if platform.system() == "Linux": options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") options.add_argument("--headless=new")代码里不要写死绝对路径,用pathlib或os.path拼路径,避免 Windows 和 Linux 之间路径分隔符不同导致的低级错误。下载目录、Cookie 保存路径这些配置项,最好集中放在一个配置对象里,方便切换环境时统一改。这个习惯能让你在本地调试和服务器跑批之间无缝衔接。
最后聊两句实战心得
如果让我给这套包含 JavaScript 执行、Cookie 管理和浏览器配置的 Selenium 进阶打法排个使用顺序,我会说:先配好浏览器环境和 Cookie 持久化,再考虑用 JS 处理那些“不配合”的元素,最后才是各种等待策略和参数调优。环境不稳定,后面的技巧跑得再好也会翻车;Cookie 不持久化,每次启动都从登录开始,效率上限就卡死了。
另外还有一点切身感受:跑批类的自动化脚本,别以为写完就万事大吉,一定要有一定的自恢复能力。比如捕获到页面异常时自动重启浏览器,Cookie 失效时自动重新登录并更新文件,把“异常上报”也写到代码里。我见过太多脚本白天跑得好好的,半夜一个偶发弹窗就挂了,第二天起来发现任务队列是空的。自动化项目真正的分水岭,不在菜鸟和老手的 API 熟悉程度,而在对异常情况的预判和兜底设计。
按这套思路去搭建,你自己实际跑上一两个项目,大概率会体会到:真正影响自动化效率的,从来不是定位方式,而是对浏览器运行机制的理解深度。希望这篇文章里的代码和踩坑记录,能帮你少走几步弯路。