Selenium捕获网络请求:原理、方案选型与Selenium Wire实战
做自动化测试做到一定阶段大家基本都会撞上同一个尴尬Selenium 脚本跑得飞快元素定位也稳但页面上的接口到底有没有调通、后端返回的是 500 还是 404、某个请求是不是超时了你完全看不见。Selenium 天然只关心 DOM 层不关心网络层这就是“Selenium 捕获网络请求”这个需求出现的原因。这篇东西我会把原理、方案选型、实操代码、以及我在真实项目里踩过的坑一次性讲清楚帮你在自动化测试里把网络请求这块短板补上。1. Selenium 为什么会“看不见”网络请求1.1 Selenium 的定位决定了它的边界先说个最底层的问题Selenium 到底是个什么东西它是通过 WebDriver 协议去驱动浏览器的WebDriver 这个协议在设计上就只包含了“查找元素”“点击元素”“获取文本”“切换窗口”这类 UI 操作。换句话说Selenium 和浏览器之间的对话全程都是围绕页面 DOM 展开的网络请求这种事根本不在 WebDriver 的职责范围内。所以你会看到这样的现象脚本里明明已经把按钮点下去了页面也跳转了但你不知道这个跳转背后请求了几个接口、哪个接口报了错、接口返回的数据结构是什么。在功能测试里这问题不大但一旦你想做接口层面的断言、想验证前后端联调是否正常、想排查线上环境才出现的偶发 bug没有网络请求数据基本等于盲人摸象。1.2 网络请求藏在哪一层要理解怎么抓网络请求得先知道请求数据在哪一层。浏览器里真正处理 HTTP 请求的是浏览器内核的网络栈它负责 DNS 解析、TCP 连接、TLS 握手、发送请求、接收响应。开发者工具DevTools里的 Network 面板展示的数据就是从这个网络栈里拿出来的。Selenium 默认拿不到这些数据因为它走的是 WebDriver 协议这个协议根本没有暴露网络栈的接口。但浏览器底层还有一个东西叫 CDPChrome DevTools ProtocolDevTools 面板的一切功能都是基于它做的。除了 CDP还有一条路是自己在 Selenium 和浏览器之间插一个代理让所有请求都先经过这个代理再从代理手里把请求数据捞出来。这两条路就是整个捕获方案的两大流派。2. 三条主流路线逐个拆解2.1 BrowserMob Proxy老牌方案但配置让人头大先说传统方案 BrowserMob Proxy。它的思路很直接在本地启动一个代理服务器然后让 Chrome 走这个代理所有请求都会经过它。你可以把它理解成在浏览器和后端之间加了一个“快递中转站”每个包裹都拆开看一眼再送走。这个方案的核心优势是能拿到的数据非常全甚至可以导出标准 HAR 格式的抓包文件后续做性能分析很方便。Java 生态和 Jenkins 集成也成熟。但它的问题也很明显。第一BrowserMob Proxy 本体是一个 Java 程序你得额外下载并启动一个进程而且每次跑测试前都要先拉起代理用完还要关掉测试脚本里就多了一大坨启动和清理逻辑。第二Python 客户端库更新慢和 Selenium 4 的兼容性有时候会出幺蛾子。第三代理模式下 HTTPS 会出现证书信任问题不处理的话浏览器直接给你报证书错误。我个人对这个方案的评价是能用但如果不是团队里已经有完整的 Java 基础设施不太建议新项目从它入手。它更适合“我就想快速导出一份 HAR 手动分析”这种轻量场景。2.2 Selenium Wire最省事的中间件方案我现在的首选是 Selenium Wire。它本质上是对 Selenium 做了一个封装内部集成 mitmproxy 作为代理你不需要自己启动任何额外进程直接像以前一样写 Selenium 代码就能从 driver.requests 里拿到所有请求。什么叫“像以前一样”就是你原本怎么初始化 driver现在还是怎么初始化只是 import 的包从 seleniumwire 进来。原本的 find_element、click、send_keys 这些 API 全部保留完全兼容。新增的 requests 属性就能拿到请求列表每个请求对象里包含 URL、请求头、请求体、响应状态码、响应头、响应体甚至耗时都有。这个方案最打动我的地方在于它把代理这层东西完全透明化了不需要你去管理代理生命周期也不需要自己处理证书问题。Selenium Wire 会自动注入自己的 CA 证书HTTPS 请求照样能解包。对于绝大多数测试场景来说这就够了。2.3 Selenium 4 CDP不走代理的原生监听第三条路线是利用 Selenium 4 自带的 execute_cdp_cmd 去执行 CDP 命令或者用 JS 往里注入一段网络监听代码。严格来说Selenium 4 的 execute_cdp_cmd 更适合执行“一次性指令”比如设置下载目录、模拟网络状态、禁用缓存。想拿它做持续的网络请求监听就得自己处理事件流代码会非常啰嗦。实际操作中很多人会退而求其次用 JS 注入的方式改写 window.fetch 和 XMLHttpRequest把请求信息塞到一个全局数组里然后通过 execute_script 读出来。这个办法的好处是不依赖任何第三方库原生 Selenium 就能跑。但坏处也很明显fetch 和 XHR 能覆盖大多数动态请求但拿不到静态资源请求、图片加载请求、以及不走这两套 API 的请求改写原型还可能被页面的 CSP 策略拦住导致注入失败。如果你只是临时排查某一个接口是否被调用这个方案可以将就。但要做系统的网络请求采集我不推荐。2.4 选型建议方案原理上手难度数据完整度适合场景BrowserMob Proxy独立代理进程较难高可导出 HARJava 体系、需要 HAR 报告Selenium Wire内嵌代理封装极易高覆盖所有请求日常自动化测试、接口断言JS 注入 / CDP前端拦截中等低只覆盖动态请求临时排查、无第三方依赖限制我自己现在的固定搭配是日常测试用 Selenium Wire需要正式的性能分析报告时才临时上 BrowserMob Proxy。两者不冲突但绝大多数场景根本用不到后者。3. 实操用 Selenium Wire 捕获请求从安装到断言3.1 安装和初始化配置用 Selenium Wire 的前提是环境里已经装好了 Python 和对应版本的浏览器驱动。它依赖 mitmproxy所以装的时候会连带装不少依赖包只要你的网络源正常pip 会自己处理。pip install selenium-wire注意一点Selenium Wire 目前对 Python 版本有要求Python 3.8 以上基本没问题太老的版本建议先升级。安装完成后初始化方式和 Selenium 很相似只是导入路径变了from seleniumwire import webdriver options webdriver.ChromeOptions() options.add_argument(--headlessnew) options.add_argument(--ignore-certificate-errors) driver webdriver.Chrome(optionsoptions) driver.get(https://httpbin.org/get) print(driver.last_request.url) driver.quit()看到没连 ChromeOptions 都可以直接用。但这里要提醒一句如果你之前用的是 selenium.webdriver.Chrome 这种写法改成 seleniumwire.webdriver 之后driver.quit() 的时候偶尔会多花一两秒因为要清理代理进程这是正常现象不要以为是卡死了。3.2 拿到请求和响应driver.requests 是一个列表按时间顺序存着所有捕获到的请求。每个请求对象都有几个关键属性request.url请求地址request.method请求方法request.headers请求头request.body请求体POST 时一般有值response.status_code响应状态码response.headers响应头response.body响应体默认是 bytesresponse.date响应时间latency从发起到收到响应中间隔了多少秒最常见的用法是遍历所有请求筛选出你关心的接口做断言for req in driver.requests: if /api/user/info in req.url: assert req.response.status_code 200 data json.loads(req.response.body.decode(utf-8)) assert data[code] 0注意 response.body 拿的是原始字节要 decode 之后再用。如果响应体很大完整存下来会占内存这时候可以配置不存响应体只保留状态码和头信息。3.3 过滤无关流量真实项目里的页面会加载大量静态资源JS、CSS、图片、字体这些请求你一般都不关心全存下来既占内存又拖慢执行速度。Selenium Wire 提供了 scopes 属性可以按正则表达式只捕获关心的请求driver.scopes [ .*/api/.*, .*\\.woff2$, ]这样 driver.requests 里就只会出现匹配 scopes 的请求。它在运行过程中可以动态修改所以你可以先跑完一轮再过滤也可以先过滤再跑。还有一个常用技巧是定期清理已经处理过的请求避免列表无限膨胀# 处理完当前请求后清空 if len(driver.requests) 50: del driver.requests[:-10]这句的意思是保留最近 10 条把前面的全删了防止长时间跑测试时内存涨得太离谱。3.4 和 pytest 集成自动化测试里最稳妥的用法是写一个 fixture在用例执行前清空历史请求用例结束后统一断言import pytest from seleniumwire import webdriver pytest.fixture def driver(): options webdriver.ChromeOptions() options.add_argument(--headlessnew) driver webdriver.Chrome(optionsoptions) yield driver driver.quit() def test_login_api(driver): driver.get(https://example.com/login) driver.find_element(name, username).send_keys(admin) driver.find_element(name, password).send_keys(123456) driver.find_element(id, loginBtn).click() login_request next( req for req in driver.requests if req.method POST and /api/login in req.url ) assert login_request.response.status_code 200这种写法把网络请求断言和 UI 操作放在一起既验证了用户能登录又验证了底层接口是通的比单独跑接口测试更能发现问题。4. 结合真实场景的实战打法4.1 上传本地文件别再折腾远程文件控件很多人在 Selenium 里做文件上传时卡在“怎么往 input[typefile] 里填路径”。其实 Selenium 自己的 send_keys 就能直接传路径根本不需要模拟键盘、不需要 pyautogui、更不需要对着弹窗发呆upload_input driver.find_element(css selector, input[typefile]) upload_input.send_keys(/path/to/your/file.pdf)这个方法对原生 input[typefile] 是 100% 能用的因为浏览器允许脚本直接设置文件选择的路径。真正麻烦的是那些隐藏了原生 input、用自定义按钮触发选择文件的页面这时你得先找到隐藏的 input 元素再执行 send_keys。可以用 document.querySelector 先把隐藏 input 的 display 改成 block但更稳妥的做法还是直接用 JS 定位from selenium.webdriver.common.by import By hidden_input driver.execute_script( return document.querySelector(input[typefile]); ) hidden_input.send_keys(/path/to/your/file.pdf)上传之后的请求同样可以从 driver.requests 里抓到如果你要验证上传是否成功直接看那个 POST 请求的响应状态码和响应体就行不用去等页面上的 loading 动画消失。4.2 下载文件如何判断“真的下载完成了”“为什么下载文件总是显示请求网络”这个问题我见过太多次了。下载文件的判断在 Selenium 里一直是个老大难因为点击下载按钮之后页面不会给你任何回调你只能自己去文件夹里确认文件是否真的落盘了。我的做法是两步并行。第一步设置下载目录为指定的临时文件夹options.add_experimental_option( prefs, { download.default_directory: /tmp/downloads, download.prompt_for_download: False, safebrowsing.enabled: True, }, )第二步写一个轮询函数判断目标文件是否出现且大小稳定import os import time def wait_for_download(download_dir, timeout30): end_time time.time() timeout while time.time() end_time: files [ f for f in os.listdir(download_dir) if not f.endswith(.crdownload) and not f.endswith(.tmp) ] if files: file_path os.path.join(download_dir, files[0]) size1 os.path.getsize(file_path) time.sleep(1) size2 os.path.getsize(file_path) if size1 size2: return file_path time.sleep(0.5) raise TimeoutError(fDownload timeout: {download_dir})chorme 下载时先写 .crdownload 临时文件下载完成才重命名为正式文件名所以遍历时排除掉这些后缀基本就能判断完成。再加上文件大小连续两次一致双保险。如果你用 Selenium Wire 且下载走的是 API也可以用响应头的 content-length 提前判断文件大小但文件夹检测始终是最直接的兜底方案。4.3 用网络请求做接口断言捕获请求最大的价值不是“看一眼”而是把它变成自动化测试里可断言的指标。比如前端埋点类请求、搜索联想类请求、分页加载类请求都是 UI 操作后触发的动态接口用 Selenium 直接不好断言但捕获请求后就能精确验证。我这里有一个常用的模式用 WebDriverWait 等一个特定请求出现避免睡眠等待的不确定性from selenium.webdriver.support.ui import WebDriverWait def wait_for_request(driver, url_keyword, timeout10): return WebDriverWait(driver, timeout).until( lambda d: any( url_keyword in req.url for req in d.requests if req.response ) ) req wait_for_request(driver, /api/search) assert req.response.status_code 200这个用法比 time.sleep(3) 好一百倍因为它只会在目标请求出现后立刻返回响应速度取决于接口而不是你猜的固定秒数。项目里跑大批量用例时累积节省的时间非常可观。4.4 自定义下拉框的定位坑捕获请求和下拉框看似不相干但在做 UI 自动化时经常撞一起。比如你要在页面上选一个筛选项再观察筛选请求是否带了正确的参数。如果这个下拉框不是原生 select而是一堆 div ul li 拼出来的直接 find_element(By.XPATH) 是定位不到选项的因为这些 li 默认是隐藏状态。我踩过的坑是这样点击 div 触发下拉框展开后如果立刻去点 li大概率报 “element not interactable”。原因是下拉框有展开动画或者 li 在点击时才通过接口动态渲染出来。正确做法是点击 div 后用显式等待等 li 出现并可见再去点击from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 先点击展开下拉框 driver.find_element(By.CSS_SELECTOR, .custom-select).click() # 等待选项渲染并可点击 option WebDriverWait(driver, 5).until( EC.element_to_be_clickable((By.XPATH, //li[contains(text(),选项A)])) ) option.click() # 断言筛选请求带了预期参数 req wait_for_request(driver, /api/list) assert statusdone in req.url这个模式通用性很强先触发显示再等待目标元素可交互最后做网络请求断言。以后遇到日期选择器、树形选择器、甚至虚拟滚动的长列表思路都一样。5. 常见问题与排查技巧5.1 HTTPS 证书报错Selenium Wire 靠中间人的方式解包 HTTPS 请求浏览器会认为连接不安全。虽然它默认会自动注入证书但某些环境下尤其是不走 headless、用真实浏览器打开的时候会弹出证书警告。解决方法是在初始化时加入两个参数options.add_argument(--ignore-certificate-errors) options.add_argument(--ignore-ssl-errors)这两个参数在 headless 模式下特别重要。如果你发现 driver.get() 的时候页面无法打开、直接报 TLS 相关错误先检查这两个参数有没有加上。5.2 抓不到请求或请求丢失有几个典型原因。第一请求是通过 Worker 或者 Service Worker 发起的默认配置下 Selenium Wire 不一定能捕获到这种场景不多遇到了可以查一下 Selenium Wire 的 issue 区。第二页面导航发生在 driver.get() 之前导航前的请求不会被记录所以清操作历史之后要确保后续操作都是从当前页面发起的。第三scopes 配置写错把关心的接口给筛掉了。排查手段很简单先把 scopes 清空跑一个最小复现看看 request.url 里到底有没有目标请求。5.3 性能与内存捕获请求是有代价的尤其是响应体默认会被完整保存。长时间跑回归测试时driver.requests 越攒越多内存迟早撑爆。我的建议是只保留必要字段的请求响应体太大的接口单独过滤掉定期清理 driver.requests在配置里限制存储的请求数量比如 options {request_storage: 200}如果只是验证状态码没必要保存响应体实际上我跑过 500 条用例的回归Selenium Wire 的内存增长在没有清理策略的前提下会多到令人窒息。后来加了两行清理代码问题直接消失。5.4 下载弹窗和沙箱在公司内网环境里下载文件还经常遇到浏览器弹窗拦截、或者下载被安全策略阻止的情况。处理下载弹窗已经在上文说明了用 prefs 关掉 prompt。沙箱问题则是另一个高发区无头模式在 Linux 上跑经常报 sandbox 错误加上 --no-sandbox 和 --disable-dev-shm-usage 基本都能解决。5.5 问题速查表现象原因解决办法页面报证书错误代理证书未被信任加 ignore-certificate-errorsdriver.requests 为空scopes 过滤太严清空 scopes 重新验证内存持续上涨请求列表未清理定期 del driver.requests下载文件一直存在 .crdownload下载未完成轮询文件名和文件大小元素定位到但点击无效自定义下拉框未展开先点击展开再等 li 可点击无头模式启动报错缺少沙箱参数加 --no-sandbox --disable-dev-shm-usage做 Selenium 网络请求捕获这件事本质上不是在补 Selenium 的缺陷而是在理清自动化测试的分层UI 层负责验证用户操作路径网络层负责验证系统背后的数据流。两者结合才是完整可信任的测试结果。我在实际项目里的体会是Selenium Wire 的方案虽然简单但它解决了一个非常致命的问题让自动化脚本不再是个黑盒。以前排查线上问题得一遍遍手动打开浏览器、按 F12、刷新页面、盯着 Network 面板看半天。现在脚本跑完直接输出每个关键请求的状态码和耗时任何异常都能第一时间定位到是前端逻辑出错、后端返回异常、还是网络本身的问题。这套东西一旦跑顺了你会发现排查问题的效率提升了不止一个量级。如果条件允许你还可以在现有框架上再做一步扩展把捕获到的请求数据统一落库配合报表平台做请求成功率趋势分析这个后续可以慢慢玩。