1. Edge 上的 Selenium 为什么总比预期多绕几圈同一份自动化脚本在别的 Chromium 系浏览器里跑得顺风顺水换到 Edge 上却开始报各种稀奇古怪的错——这个场景我遇到过太多次了。有人是公司环境统一配了 Edge有人是客户现场只能用系统自带浏览器也有人纯粹是想做跨浏览器兼容性验证。不管你属于哪一种只要脚本要跑在 Edge 上就得先接受一个事实Edge 虽然内核是 Chromium但它的发行节奏、驱动分发方式、默认配置目录、企业策略层和别的 Chromium 浏览器并不完全一样。这些差异平时看不出来一旦出问题就特别隐蔽。这篇内容整理的是我在用 Selenium 调试 Edge 浏览器过程中反复踩到的坑以及每个坑背后真正的成因和具体解法。适合两类人看一类是刚把脚本从其他浏览器迁移到 Edge、正在被SessionNotCreatedException折磨的新手另一类是脚本已经在跑但总在 CI 或者无人值守环境里间歇性失败想找稳定性突破口的老手。文中代码以 Python 为主Selenium 版本按 4.x 来讲因为 4.6 之后内置的驱动管理机制改变了整个排查思路这个后面会详细说。先把最容易出问题的三条主链路摆在前面第一条是驱动与浏览器版本的对齐这是所有报错里出现频率最高的第二条是浏览器启动参数与用户数据目录这条决定了你的浏览器能不能稳定起来第三条是元素交互时的等待与渲染时序这条决定了脚本能不能真正跑完。这三条链路几乎覆盖了日常 80% 以上的 Edge 调试问题剩下的零碎问题基本都能归类进去。下面按这个顺序逐条拆。2. 驱动版本对不上从报错原文反推该对齐哪一层2.1 那句 session not created 到底在说什么几乎所有新手遇到的第一个报错都长这样selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of EdgeDriver only supports Microsoft Edge version 118 Current browser version is 126.0.2592.87 with binary path C:\Program Files (x86)\...\msedge.exe很多人看到这一大段就懵了其实它已经把答案写得很清楚了。翻译成人话就是你手里的msedgedriver是给 118 版本准备的但你机器上的 Edge 已经升到 126 了两者对不上驱动拒绝建立会话。这里有个关键的认知点WebDriver 协议要求驱动与浏览器主版本号一致也就是版本号第一段。118 的驱动只能驱动 118 的浏览器126 的驱动只能驱动 126 的浏览器。次版本号可以有小幅差异但主版本必须对齐。这不是 Edge 独有的规矩所有 Chromium 系浏览器都这样只是 Edge 的自动更新比较激进很多时候你没动过任何东西浏览器自己半夜升级了第二天脚本就挂了。顺带说一个容易混淆的报错Message: msedgedriver executable needs to be in PATH这个不是版本问题是根本找不到驱动可执行文件。前者是找到了但对不上后者是压根没找到排查方向完全不同别搞混了。2.2 手动对齐、Selenium Manager 和第三方管理器的取舍驱动管理这块目前有三条主流路线我在不同项目里都用过各有适用场景。先说结论新项目优先用 Selenium Manager老项目或者内网离线机器用手动对齐需要精细控制版本的时候用第三方管理器。方案适用场景优点明显短板手动下载放置内网、离线、版本需锁定完全可控无网络依赖每次浏览器升级都要人工跟进Selenium Manager4.6 内置绝大多数联网场景零配置自动匹配版本首次运行需联网内网会卡住第三方驱动管理库需要指定版本、多浏览器混跑版本可指定支持多浏览器多一层依赖偶发缓存问题Selenium Manager 是 Selenium 4.6 之后内置的它的工作逻辑是启动时先探测本机 Edge 的实际版本然后去驱动仓库找匹配版本找不到就下载下载完缓存到本地。整个过程对使用者是透明的你什么都不用配直接webdriver.Edge()就能跑。这在 4.11 之后稳定性提升明显我现在的默认做法就是什么都不指定。但有三个坑要提前知道。第一首次运行需要网络如果你的构建机在内网隔离环境Selenium Manager 会卡在探测阶段然后超时报错信息还很含糊。第二它探测的是默认安装路径下的 Edge如果你用的是绿色版或者装在非标准目录探测会失败这时候必须用binary_location手动指定浏览器路径。第三缓存目录权限问题某些 CI 环境里用户对缓存目录没写权限下载会静默失败。手动对齐这条路虽然笨但在受控环境里最可靠。做法是先查本机 Edge 版本可以用命令行或者看浏览器设置页的关于。# Windows PowerShell 查 Edge 版本 (Get-Item C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe).VersionInfo.ProductVersion # macOS /Applications/Microsoft\ Edge.app/Contents/MacOS/Microsoft\ Edge --version拿到版本号后去驱动下载页面对应版本下载解压后放到一个固定目录然后在代码里显式指定from selenium import webdriver from selenium.webdriver.edge.service import Service service Service(executable_pathrD:\drivers\msedgedriver.exe) driver webdriver.Edge(serviceservice)这里有个实践细节值得说我习惯把驱动放在项目目录下的drivers/里而不是塞进系统 PATH。原因是PATH 里放多个版本驱动是灾难你永远不知道当前生效的是哪个排查时会浪费大量时间。放项目内、路径写死、跟着代码一起进版本库驱动文件不大的话出问题一眼就能看出用的哪个版本。2.3 内网机器和版本锁定场景下的处理办法离线环境是驱动问题的高发区。我见过最典型的情况是内网机器上的 Edge 被策略锁在某个固定版本但开发同学本地用的是最新驱动代码提交到流水线就炸。解决办法是把浏览器版本 驱动版本作为一个整体来管理写在配置文件里构建时校验。思路是这样启动前先探测实际 Edge 版本和配置里声明的期望版本比对不一致就直接抛异常并打印清晰提示而不是等到 WebDriver 报一句天书。import subprocess import re import sys EXPECTED_MAJOR 126 def get_edge_major_version(): try: out subprocess.check_output( [powershell, -Command, r(Get-Item C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe).VersionInfo.ProductVersion], stderrsubprocess.DEVNULL, textTrue ) return out.strip().split(.)[0] except Exception: return None major get_edge_major_version() if major and major ! EXPECTED_MAJOR: print(f[环境校验失败] 期望 Edge 主版本 {EXPECTED_MAJOR}实际 {major}) print(请更新配置中的驱动版本或联系环境管理员同步浏览器版本) sys.exit(1)这段代码看着朴素但在我参与的几个多人协作项目里它把驱动版本问题的排查时间从平均半小时压缩到十秒以内。因为报错发生在最前面而且提示是人话不用再去翻 WebDriver 的长篇堆栈。提示驱动版本升级后记得清掉 Selenium Manager 的本地缓存目录否则某些情况下它会继续复用旧驱动。缓存位置随系统不同一般在用户目录下的.cache/selenium里。3. 浏览器能启动但立刻退出启动参数与配置目录的隐雷3.1 用户数据目录被占用导致的启动失败这个坑非常隐蔽表现也很多样有时候是SessionNotCreatedException有时候是浏览器窗口一闪而过有时候是驱动起来了但driver.current_url一直返回data:,。共同点是你手动打开 Edge 是正常的只有脚本启动会出问题。根本原因在 Edge 的用户数据目录user data directory。Edge 默认的用户数据目录是一个独占资源同一个目录在同一时间只能被一个浏览器实例使用。如果你本机已经开着 Edge 在正常上网脚本再去用默认目录启动新的浏览器进程会尝试连接已有实例而不是新建一个独立实例驱动就拿不到控制权各种诡异现象就出来了。解法很直接给自动化脚本一套独立的用户数据目录。from selenium import webdriver from selenium.webdriver.edge.options import Options import tempfile, os options Options() profile_dir os.path.join(tempfile.gettempdir(), edge_auto_profile) options.add_argument(f--user-data-dir{profile_dir}) # 同时指定 profile 目录名避免和默认 profile 混用 options.add_argument(--profile-directoryDefault) driver webdriver.Edge(optionsoptions)用tempfile.gettempdir()是为了跨平台Windows 下会落到临时目录macOS 和 Linux 下一样能用。这么做还有两个额外好处一是每次跑都是干净环境不受本机浏览器历史、缓存、扩展的影响结果可复现二是脚本崩了留下的脏数据不会污染你日常用的浏览器配置。但独立目录也有代价登录态、Cookie、本地存储全都是空的。如果你的脚本依赖已登录状态就得在脚本里做登录流程或者从一份预先准备好的 profile 目录复制过来。我一般做法是准备一份profile_template目录里面预置好必要的登录态和配置每次运行时复制一份到临时目录再用跑完就删。这样既保证了独立性又保留了必要状态。3.2 无头模式下的尺寸、渲染与 UA 差异无头模式是自动化跑批的标配但它在 Edge 上有几个必须处理的差异点不处理就会遇到有头能跑无头不能跑的经典问题。第一个是窗口尺寸。有头模式下浏览器窗口通常开得比较大页面元素都在可视区域内无头模式默认视口只有 800x600很多响应式布局会把元素折叠进汉堡菜单或者出现懒加载不触发的情况。表现就是NoSuchElementException但你手动打开页面明明能看到。options.add_argument(--headlessnew) # 新版无头渲染更接近有头 options.add_argument(--window-size1920,1080) options.add_argument(--disable-gpu) # 部分环境需要注意--headlessnew和老的--headless不是一回事。新版无头模式走的是完整渲染路径页面表现更接近真实浏览器对现代前端框架更友好。如果你的 Edge 版本比较新建议直接用new别用裸的--headless。另外--window-size在无头模式下是生效的--start-maximized在无头模式下不生效这是个常见误解。第二个是User-Agent。无头模式的默认 UA 里会带HeadlessEdg字样很多站点的风控会直接识别并返回不同内容甚至直接拦掉。如果你的脚本在有头模式正常、无头模式拿到的是空页面或者验证页八成是这个问题。ua (Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0) options.add_argument(f--user-agent{ua})这里补充一句改 UA 是为了让自动化访问的页面表现一致属于测试环境里的常规做法别拿它去做绕过正常访问控制的用途那是另一回事也不在本文讨论范围内。第三个是字体和渲染差异。无头环境尤其是容器里经常缺中文字体导致截图里中文全是方框或者某些依赖字体度量的布局判断出错。如果你的脚本要做视觉比对务必在容器镜像里装好字体包。3.3 常用启动参数逐条拆解参数这块我列一个自己常用的清单都是实际项目里反复用到的附上为什么要加。参数作用什么时候需要--user-data-dir路径指定独立配置目录只要本机开着浏览器就必须加--headlessnew新版无头模式跑批、CI 环境--window-size1920,1080固定视口尺寸无头模式下必加--disable-gpu关闭 GPU 加速容器、虚拟机环境--no-sandbox关闭沙箱容器里以 root 运行时--disable-dev-shm-usage用磁盘代替共享内存Linux 容器内存小的时候--inprivate隐私模式启动需要干净会话且不想留痕--disable-extensions禁用扩展排查扩展干扰时--remote-debugging-port9222开调试端口需要接管浏览器时--no-sandbox这条要特别说明它确实能解决容器里启动失败的问题但会降低浏览器隔离性。只在明确受控的容器环境里用本地开发机不要加。我见过有人把这个参数无脑塞进公共配置结果本地跑的时候安全边界被削弱了还不知道。--disable-dev-shm-usage是 Linux 容器的经典补丁。容器默认的/dev/shm只有 64MBEdge 渲染复杂页面时很容易把它撑爆然后浏览器进程静默崩溃日志里只有一句DevToolsActivePort file doesnt exist非常难查。加上这个参数后改为写磁盘稳定性立刻上来。4. 元素就在那里却点不到定位、等待与时序的三重错位4.1 隐式等待和显式等待混用会把问题放大等待是 Selenium 里最容易被写错的部分。很多人习惯在启动时来一句driver.implicitly_wait(10)然后觉得万事大吉了。这个做法在简单页面上没问题但一旦和WebDriverWait混用行为会变得很难预测。核心机制是这样的隐式等待是全局的它作用于每一次find_element调用找不到就轮询到超时。显式等待是局部的你自己控制条件和超时。当两者同时存在时显式等待内部调用find_element时每次轮询都会先吃一遍隐式等待的超时。结果就是你以为显式等待 10 秒实际上可能等了 10 × N 秒其中 N 是显式等待的轮询次数。脚本变慢你还找不到原因。我的做法是统一用显式等待把隐式等待设为 0。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By driver.implicitly_wait(0) # 明确关掉 wait WebDriverWait(driver, 15, poll_frequency0.5) elem wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, #submit-btn))) elem.click()element_to_be_clickable比presence_of_element_located靠谱得多。后者只保证元素在 DOM 里但元素可能被遮挡、可能是display:none、可能还在动画过程中。前者会同时检查可见性和可交互性能避免掉一大批ElementClickInterceptedException。另外一个容易被忽略的点是页面加载策略。Edge 默认的normal策略会等load事件完全触发遇到那种带长轮询或者大图的页面会等到超时。改成eager只等 DOM 就绪能显著提速。from selenium.webdriver.edge.options import Options options Options() options.page_load_strategy eager用eager的前提是你的等待逻辑写得足够扎实因为它不再保证资源加载完。如果脚本里有依赖图片尺寸或者样式表计算结果的逻辑慎用。4.2 iframe、Shadow DOM 和动态渲染的处理顺序元素定位失败的另一个大户是上下文没切对。最常见的是 iframe元素明明在页面上但find_element就是找不到因为它在另一个文档上下文里。# 等 iframe 本身加载完成再切进去 iframe wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, iframe#pay-frame))) driver.switch_to.frame(iframe) # 操作完记得切回来否则后续定位全在 iframe 里找 driver.switch_to.default_content()顺序很关键先等 iframe 元素本身出现再切换再等内部元素。我见过有人直接switch_to.frame(0)结果页面还没渲染完切进去是个空文档报NoSuchFrameException。Shadow DOM 是另一个麻烦。现代前端组件大量使用 Shadow DOM 封装样式普通选择器穿透不进去。Selenium 4 提供了专门的支持host driver.find_element(By.CSS_SELECTOR, my-component) shadow_root host.shadow_root inner shadow_root.find_element(By.CSS_SELECTOR, .inner-button) inner.click()注意shadow_root只能往下一层遇到嵌套 Shadow DOM 要一层层剥。这时候与其硬写不如直接在浏览器开发者工具里确认层级结构用document.querySelector在控制台里试通了再翻译成 Selenium 代码能省很多来回。4.3 被遮挡、不可交互与点了没反应的补救手段ElementClickInterceptedException这个报错本质是 Selenium 模拟的真实点击被挡住了。挡住的可能是固定定位的导航栏、可能是加载遮罩、也可能是弹窗。Edge 上的表现和别的 Chromium 浏览器一致但因为它默认的滚动行为和渲染时序差异出现概率会稍高一些。排查顺序建议这样先确认是不是遮罩没消失 → 再确认元素是否在视口内 → 最后才用 JS 点击兜底。from selenium.webdriver.common.action_chains import ActionChains try: elem.click() except Exception: # 先滚动到元素位置确保在视口内 driver.execute_script(arguments[0].scrollIntoView({block:center});, elem) # 再试一次常规点击 try: elem.click() except Exception: # 最后兜底JS 直接触发 driver.execute_script(arguments[0].click();, elem)这里必须强调JS 点击是兜底不是首选。因为 JS 点击绕过了真实的事件链路某些依赖mousedown/mouseup时序或者浏览器原生行为的组件会表现异常比如下拉框不展开、拖拽失效。用它之前先想想为什么被挡住多半是等待逻辑有问题。还有一类是StaleElementReferenceException元素过期了。原因是页面重渲染后你手里持有的元素引用指向了已经被销毁的节点。解法不是加重试而是重新定位def click_with_retry(driver, locator, retries3): for i in range(retries): try: driver.find_element(*locator).click() return except StaleElementReferenceException: if i retries - 1: raise time.sleep(0.3)注意这里的find_element写在循环内部每次重新查找这才是关键。如果写成在外面查一次、循环里反复点重试多少次都没用。5. 文件上传下载、弹窗与多标签页的实战处理5.1 下载目录与保存对话框的绕行方案Edge 的下载行为在自动化里比较麻烦默认会弹下载栏某些文件类型还会弹是否打开/保存的对话框。这个对话框是浏览器原生 UIWebDriver 点不到。处理思路是通过偏好设置预置下载行为让浏览器不要弹任何东西。from selenium.webdriver.edge.options import Options options Options() prefs { download.default_directory: rD:\auto_downloads, download.prompt_for_download: False, download.directory_upgrade: True, safebrowsing.enabled: True, } options.add_experimental_option(prefs, prefs)这里有几个实战经验。第一下载目录必须是绝对路径且已经存在目录不存在的话 Edge 会静默回退到默认下载目录你会发现自己找半天找不到文件。第二download.prompt_for_download设为 False 只解决询问保存位置不解决文件类型处理。像 PDF、某些 Office 格式Edge 可能选择直接预览而不是下载需要额外配置。prefs[plugins.always_open_pdf_externally] True第三下载是异步的click()返回不代表文件已经落盘。直接去读文件会读到半个文件或者根本不存在。必须做轮询等待import os, time def wait_download(dir_path, timeout30): end time.time() timeout while time.time() end: files [f for f in os.listdir(dir_path) if not f.endswith(.crdownload)] if files: return os.path.join(dir_path, files[0]) time.sleep(0.5) raise TimeoutError(下载超时).crdownload是 Chromium 系浏览器下载中的临时文件后缀过滤掉它才是真正下载完成的标志。这个细节很多教程没提但实际项目里不加这层过滤读到的经常是残缺文件。5.2 文件选择框的两种打法文件上传比下载简单因为input typefile是可以用send_keys直接塞路径的不需要真的去点开系统文件选择框。file_input driver.find_element(By.CSS_SELECTOR, input[typefile]) file_input.send_keys(rD:\test\data.xlsx)两个注意点。一是元素必须可见如果这个 input 被 CSS 隐藏了常见做法是opacity:0或者display:nonesend_keys会失败。前一种情况可以用 JS 临时改样式后一种就得找页面上那个伪装成按钮的触发元素很多时候它背后挂着一个隐藏的 input。# opacity:0 这种可以用 JS 临时改成可见 driver.execute_script( arguments[0].style.displayblock; arguments[0].style.opacity1;, file_input )二是路径格式Windows 下反斜杠要注意转义用原始字符串r...最稳妥。如果真的遇到无法绕开的原生文件选择框比如某些老系统用的是自定义控件那就得上系统级自动化工具了但那属于另一个技术栈成本和维护难度都上一个台阶能用send_keys解决就别走这条路。5.3 原生弹窗与多标签页的切换陷阱WebDriver 能处理的弹窗只有alert、confirm、prompt这三种原生对话框。Edge 里表现正常但有两点要注意。第一弹窗出现是异步的且弹窗出现时后续的 WebDriver 命令会阻塞。所以必须有等待逻辑from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC alert WebDriverWait(driver, 10).until(EC.alert_is_present()) print(alert.text) # 先读文本方便排查 alert.accept() # 或者 alert.dismiss()先print(alert.text)是个好习惯。我踩过坑页面弹了一个我没预料到的确认框我以为是业务弹窗直接 accept 了结果把整个流程点乱了。加上这行日志至少能回溯。第二登录认证框不是 alert。那种输入用户名密码的浏览器原生认证框switch_to.alert抓不到。处理方式是在 URL 里带凭据或者预先在 profile 里存好登录态。前者的写法是https://user:passhost/path但现在很多站点已经不支持这种方式了。多标签页的问题在于新标签页打开后driver 的上下文还停在旧标签页。必须显式切换main_handle driver.current_window_handle before set(driver.window_handles) driver.find_element(By.CSS_SELECTOR, .open-new).click() # 等新窗口出现 WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) len(before)) new_handle (set(driver.window_handles) - before).pop() driver.switch_to.window(new_handle) # 处理完记得关掉并切回 driver.close() driver.switch_to.window(main_handle)这里用集合差集来找新窗口比driver.window_handles[-1]可靠因为窗口句柄的顺序不保证。6. 把黑盒变可观测日志、截图与远程调试端口6.1 打开驱动日志的正确姿势Selenium 的报错信息默认非常克制很多情况下只给你一句element not interactable什么上下文都没有。想看到驱动和浏览器之间的实际通信必须主动打开日志。from selenium.webdriver.edge.service import Service service Service( executable_pathrD:\drivers\msedgedriver.exe, log_outputrD:\logs\edgedriver.log, service_args[--verbose] ) driver webdriver.Edge(serviceservice)--verbose会让驱动打印完整的请求和响应包括每次findElement的定位器、返回结果、页面加载状态。日志文件会很大但排查问题时是金矿。我遇到过一次元素定位诡异失败的问题翻日志才发现页面在加载过程中触发了两次导航第一次渲染的 DOM 很快被替换掉了元素定位到的是即将销毁的那一份。没有驱动日志这个问题根本没法定位。浏览器端的日志也能拿到但 Edge 需要额外开启options.set_capability(goog:loggingPrefs, {browser: ALL}) # 读取页面控制台日志 for entry in driver.get_log(browser): print(entry[level], entry[message])这个对前端报错特别有用。页面上某个按钮点了没反应看看控制台是不是有 JS 异常往往比瞎猜快得多。6.2 用远程调试端口接管一个已打开的浏览器有些场景下webdriver.Edge()从零启动浏览器行不通需要复用已登录的会话、需要人工先完成某些操作、或者要调试一个已经运行很久的页面。这时候可以用调试端口接管。第一步带调试端口启动 Edge注意要配独立用户目录否则会被已有实例吃掉C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe ^ --remote-debugging-port9222 ^ --user-data-dirD:\edge_debug_profile第二步代码里通过debuggerAddress连接from selenium import webdriver from selenium.webdriver.edge.options import Options options Options() options.add_experimental_option(debuggerAddress, 127.0.0.1:9222) driver webdriver.Edge(optionsoptions) print(driver.title) # 直接操作已打开的页面这条路子特别好用的一点是连接上去之后浏览器窗口是可见的你可以一边看它自己操作一边改代码。调试复杂交互流程时效率极高。要提醒的是这种方式下driver.quit()会关掉整个浏览器如果你还想继续用用driver.close()只关当前标签页。另外端口冲突是常见问题多开的时候换个端口就行别死磕 9222。6.3 失败现场留证的截图与页面快照自动化脚本在 CI 里失败你拿到的往往只有一行异常没有页面状态没法复现。所以我在所有正式项目里都会装一个失败即留证的钩子。import traceback, datetime, os def safe_run(driver, func): try: func() except Exception as e: ts datetime.datetime.now().strftime(%Y%m%d_%H%M%S) os.makedirs(artifacts, exist_okTrue) driver.save_screenshot(fartifacts/fail_{ts}.png) with open(fartifacts/fail_{ts}.html, w, encodingutf-8) as f: f.write(driver.page_source) with open(fartifacts/fail_{ts}.log, w, encodingutf-8) as f: f.write(traceback.format_exc()) f.write(f\ncurrent_url: {driver.current_url}\n) raise三样东西缺一不可截图看视觉状态page_source看 DOM 结构异常堆栈看代码路径。有了这三样大部分问题不用复现就能定位。截图这块有个小坑无头模式下截图有时候是全白通常是因为截图时机太早页面还没渲染完加个几百毫秒的等待或者等某个标志元素出现再截。7. 无人值守环境下的并发、残留与封装7.1 多进程并发时的端口与目录冲突单个脚本跑通之后下一步往往是并发跑多个用例。这里最容易翻车的是多个进程共用同一个用户数据目录表现是其中几个进程莫名启动失败或者行为错乱。解法是每个进程一个独立目录用进程 ID 或者 UUID 区分import os, uuid def build_options(): options Options() profile os.path.join(tempfile.gettempdir(), fedge_{os.getpid()}_{uuid.uuid4().hex[:6]}) options.add_argument(f--user-data-dir{profile}) options.add_argument(--headlessnew) options.add_argument(--window-size1920,1080) return options端口同理如果用了--remote-debugging-port别固定写死按进程号做偏移或者干脆让系统分配。并发数量也要控制。一个 8 核机器上开 20 个浏览器实例结果通常是集体变慢然后大面积超时。经验值是按 CPU 核数的 1 到 1.5 倍来设并发上限单个浏览器实例的内存占用按 300MB 到 500MB 估算先算内存再定并发数。7.2 进程残留与僵尸驱动的清理Selenium 脚本异常退出时最常见的结果是浏览器进程和驱动进程都变成孤儿留在后台。跑上几天机器上堆了几百个进程内存吃满后续任务全部失败。根子在driver.quit()有没有被执行。quit()会关闭所有窗口并终止驱动进程close()只关当前窗口两者不能混。正确的写法是 try/finally 或者上下文管理器driver webdriver.Edge(optionsoptions) try: do_something(driver) finally: driver.quit()即便如此遇到进程被强制杀掉CI 超时、容器被回收的情况finally也来不及执行。所以我一般在任务开始前先做一次清理把上一次遗留的清掉。Windows 下可以按进程名清理但务必限定范围别把本机正常使用的浏览器也杀了# 只清理驱动进程风险较低 taskkill /F /IM msedgedriver.exe # 清理自动化专用的浏览器进程通过命令行参数过滤更安全 wmic process where namemsedge.exe and commandline like %edge_auto_profile% delete第二条命令通过命令行参数里的 profile 路径来过滤只会杀掉自动化启动的实例不影响用户手开的浏览器。这个思路比无差别杀进程安全得多。7.3 一套可复用的封装骨架最后给一份我在多个项目里迭代出来的骨架把前面说的坑都规避掉了。它不是完整框架但把关键决策点都收进去了可以直接拿去改。import os, tempfile, uuid, logging from selenium import webdriver from selenium.webdriver.edge.options import Options from selenium.webdriver.edge.service import Service from selenium.webdriver.support.ui import WebDriverWait log logging.getLogger(__name__) def create_driver(headlessTrue, download_dirNone, driver_pathNone): options Options() profile os.path.join(tempfile.gettempdir(), fedge_{uuid.uuid4().hex[:8]}) options.add_argument(f--user-data-dir{profile}) if headless: options.add_argument(--headlessnew) options.add_argument(--window-size1920,1080) options.add_argument(--disable-gpu) options.add_argument(--no-first-run) options.add_argument(--disable-featuresmsEdgeSidebarV2) prefs { download.prompt_for_download: False, download.directory_upgrade: True, safebrowsing.enabled: True, } if download_dir: os.makedirs(download_dir, exist_okTrue) prefs[download.default_directory] os.path.abspath(download_dir) options.add_experimental_option(prefs, prefs) options.set_capability(goog:loggingPrefs, {browser: ALL}) options.page_load_strategy about_page_strategy eager service None if driver_path: service Service(executable_pathdriver_path, log_outputos.path.join(tempfile.gettempdir(), edgedriver.log), service_args[--verbose]) driver webdriver.Edge(optionsoptions, serviceservice) driver.implicitly_wait(0) driver._wait WebDriverWait(driver, 15, poll_frequency0.5) log.info(driver started, profile%s, profile) return driver有几个点在封装时值得强调。implicitly_wait(0)一定要显式写哪怕是默认值写在代码里是给后来者一个明确信号这个项目不用隐式等待。page_load_strategy设成eager能省不少等待时间但要在项目里统一别有的脚本用normal有的用eager排查时会混乱。把WebDriverWait挂在 driver 上是个取巧但很实用的做法省得每个模块都重新构造一个。至于--disable-featuresmsEdgeSidebarV2这类参数属于针对性优化Edge 的侧边栏、首次运行引导、各种推荐气泡会在启动时抢焦点或者遮挡页面元素把它们关掉能减少很多莫名其妙的干扰。具体要关哪些 feature建议先在开发者工具里观察启动时弹了什么再逐个用--disable-features处理别一股脑全抄。另外driver_path这个参数留了口子。默认不传走 Selenium Manager 自动管理内网或者需要锁版本时传进去走手动对齐。两种模式在同一个封装里共存切换成本几乎为零。我在实际项目里的体会是Selenium 调试这件事难点从来不在 API 怎么用API 一共就那么几个。真正花时间的是搞清楚每次失败发生在哪一层是环境层的版本对不上是启动层的目录冲突还是交互层的时序问题。把这三层分开看大部分问题都能在几分钟内定位到。前面那些排查路径和封装写法本质上就是把这三层的边界划清楚让报错自己去指向答案而不是靠猜。
阅读完成 · 觉得有帮助?