
你有没有遇到过这种情况Selenium脚本里所有元素都定位到了点击也执行了但页面就是没有反应或者接口偶发报错你在浏览器F12里看得一清二楚脚本却像“瞎子”一样什么都拿不到。Selenium本身暴露的是页面操作层它不会主动告诉你浏览器发送了哪些请求、请求体带上了什么参数、响应到底返回了什么内容。可实际排查问题时很多答案恰恰就藏在这些网络请求里。“selenium 捕获网络请求”这个话题说白了就是给自动化脚本装上一双“F12”的眼睛。做自动化测试、爬虫采集、前端性能分析的人只要遇到接口报错、页面异步加载失败、下载文件无法断言、上传文件异常这类问题都会需要这个能力。这篇我把我实际用过的方案、踩过的坑、可以直接抄的代码都整理出来希望能帮你少走弯路。1. Selenium默认看不到请求但这几个场景又非常需要1.1 纯页面操作看不出的问题Selenium的设计哲学是模拟用户操作打开页面、点击、输入、断言元素。它把浏览器当成一个黑盒DOM暴露给你但浏览器在背后发出的HTTP请求、收到的响应、状态码、耗时这些网络层信息默认不在它的能力范围内。这就导致了一个很尴尬的局面页面元素明明出现了但页面里的表格是空的点击提交后提示“上传失败: 网络请求错误”但你不知道是参数格式不对、接口返回了500还是文件太大直接被网关拦了。你再怎么用WebDriverWait等元素等到超时也等不出答案——因为元素可能永远都不会出现或者错误提示本来就是动态拼接的。我之前维护过一套商城前端的自动化用例有个用例是“注册后自动登录”。元素倒是都点到了但十次里有三次登录失败测试报告里只有“元素未找到”。后来把网络请求抓出来一看原来是注册接口的userId字段偶尔会返回空字符串前端拿空ID去请求登录接口直接被后端拒了。这种问题不抓请求根本没法定位。1.2 捕获请求能解锁的场景把网络请求捕获下来等于在Selenium和浏览器之间加了一个“中间人”你可以拿到这么几类信息所有请求的URL、Method、请求头、请求体所有响应体的状态码、响应头、响应体请求的时间序、耗时甚至可以按接口名做性能分析动态修改请求或响应实现接口mock比如把某个接口的响应直接替换成指定JSON等待“某个接口真正返回成功”后再继续下一步而不是傻等页面元素。这套能力在自动化测试里特别有用。比如你想验证文件下载成功不能只等downloaded.pdf出现还得确认后端返回了200想排查上传为什么会失败直接把上传接口的响应体打印出来错误码一目了然想在测试环境模拟后端异常可以拦截响应强行返回500看前端有没有做容错。这些需求用Selenium自带的API是做不到的必须补上一层“网络日志”。2. 四条主流方案怎么选我踩出来的对比结论2.1 四种方案一句话概括当前社区里比较主流的实现路径有四种我逐个说下它们到底是怎么回事。BrowserMob Proxy一个独立的Java代理程序Selenium把浏览器代理设置为它它负责转发并记录所有HTTP/HTTPS请求。这个方案很成熟但对Python使用者来说需要额外启动Java进程配置偏重。Selenium Wire一个Python库扩展了Selenium的webdriver内部自动起了一个代理所有请求都会经过它直接通过driver.requests就能拿到请求和响应。这是我用得最多的方式简单直接。Chrome DevTools ProtocolCDP的Network事件通过CDP协议开启Network域浏览器会把网络事件推送给脚本。Selenium 4支持execute_cdp_cmd执行CDP命令但完整的事件监听需要自己解析日志门槛稍高。Performance Log在Chrome options里开启performance日志然后通过driver.get_log(performance)获取。这个本质是CDP日志的导出优点是代码量少缺点是不能改响应且日志格式解析略微麻烦。2.2 适用场景和性能开销对比表方案是否需要代理可否修改响应对环境的要求性能开销适合场景BrowserMob Proxy是可以需要Java环境中高Java技术栈、老项目Selenium Wire是可以pip安装即可中Python居多、需要快速拿到请求体/响应体CDP Network事件否需要额外做拦截实现依赖浏览器版本低轻量监控、实时性要求高Performance Log否不能Chrome/Edge低事后分析、性能统计这里重点说下“能否修改响应”的区别。BrowserMob Proxy和Selenium Wire都是在代理层做文章所以可以拦截并伪造响应CDP虽然也提供了Fetch.enable来做请求拦截但需要一套过滤逻辑比Selenium Wire的response_intercept复杂不少。Performance Log则纯粹是“只读”的只能看不能改。2.3 我的选型建议如果是在Python项目里做自动化测试我的建议是优先用Selenium Wire。理由很现实它把代理、证书、会话管理都封装好了你只需要pip install selenium-wire然后from seleniumwire import webdriver其他用法和普通Selenium几乎一样上手成本最低。它唯一让我头疼的地方是版本偶尔跟不上最新浏览器但只要固定浏览器大版本基本没问题。如果你想做的是一个轻量级的“接口耗时巡检”不希望引入代理产生额外网络转发开销那就用CDP或者Performance Log。CDP方式更接近浏览器底层性能最高也最适合嵌入到已有的Selenium 4脚本里不过要接受它的事件回调和日志解析这层学习成本。3. 实操用Selenium Wire拿到请求与响应全文3.1 安装与初始化代理注入方式Selenium Wire的安装和Selenium一样简单pip install selenium-wire需要注意导入时不是from selenium import webdriver而是from seleniumwire import webdriver。它会自动启动一个本地代理并把浏览器的流量指向这个代理。初始化Chrome的方式如下from seleniumwire import webdriver options webdriver.ChromeOptions() options.add_argument(--headlessnew) # 如果访问的是自签名证书的测试环境建议打开避免SSL证书报错 options.accept_insecure_certs True driver webdriver.Chrome(optionsoptions) driver.get(https://example.com/login)这里有一个细节accept_insecure_certs在不少自动化环境里都容易忽略。代理模式下浏览器访问HTTPS站点时证书链会被替换成代理自己的证书如果脚本没有信任这个证书页面可能直接显示“您的连接不是私密连接”Selenium自然也就操作不下去了。加上这一句之后我基本没有再遇到证书拦截的问题。3.2 拦截、筛选与读取请求体/响应体跑完driver.get之后所有请求都会存放在driver.requests这个列表里。遍历一下就能看到完整信息for request in driver.requests: print(request.method, request.url) print(request.headers) print(request.body) if request.response: print(request.response.status_code) print(request.response.headers) print(request.response.body)注意“请求体”和“响应体”都是字节串直接打印可能会看到b...这种格式。如果是JSON接口最好做一次解码和解析import json for request in driver.requests: if api/login in request.url and request.method POST: body request.body.decode(utf-8, errorsignore) print(请求参数:, body) if request.response: resp_body request.response.body.decode(utf-8, errorsignore) try: data json.loads(resp_body) print(接口返回:, data) except json.JSONDecodeError: print(非JSON响应:, resp_body[:500])实际使用中一个页面会发起十几个请求我们不能全部打印否则日志会被刷爆。更常见的做法是写一个过滤器def find_request(driver, keyword, methodNone): for req in driver.requests: if keyword in req.url and (method is None or req.method method): return req return None这条工具方法我几乎在所有的自动化项目里都会写。等到定位问题时只要传接口关键字就能拿到对应请求和响应非常方便。3.3 修改请求与响应mock接口的正确姿势Selenium Wire最香的功能之一是response_intercept。你可以在浏览器真正接收响应之前把响应内容替换掉用来模拟后端错误、超时、空数据等场景。比如我想让某个查询接口永远返回空列表from seleniumwire import webdriver driver webdriver.Chrome() def interceptor(request): if api/user/list in request.url: request.create_response( status_code200, headers{Content-Type: application/json}, body{code:0,data:[]} ) driver.response_intercept interceptor driver.get(https://example.com/user)这样前端拿到的就是空列表我可以接着断言页面有没有显示“暂无数据”这个兜底文案。这种玩法非常适合做异常分支的自动化覆盖——不用改后端不用造数据纯粹在前端层模拟稳定性非常高。还有一个使用细节driver.requests这个列表会一直保留到驱动关闭。如果你执行了多步操作请求越积越多内存也会慢慢上涨。我在长时间运行的用例中会在关键步骤开始前先清空一次del driver.requests清空之后再执行点击或提交这样driver.requests里存的就是当前步骤相关的请求筛选起来更干净也不容易误判。4. 实操基于CDP的Network事件监听不依赖代理4.1 开启Network域代码骨架如果你不想额外装selenium-wire或者你已经在用Selenium 4的原生webdriver那可以走CDP路线。Selenium 4里有一个execute_cdp_cmd方法可以直接向浏览器发送CDP命令。首先开启Network域from selenium import webdriver capabilities { goog:loggingPrefs: {performance: ALL, browser: ALL} } options webdriver.ChromeOptions() options.set_capability(goog:loggingPrefs, capabilities) driver webdriver.Chrome(optionsoptions) driver.execute_cdp_cmd(Network.enable, {}) driver.get(https://example.com)开启Network.enable之后浏览器会记录网络事件但这些事件不会直接推到Python里而是写入浏览器的performance日志。所以要取数据需要用driver.get_log(performance)logs driver.get_log(performance) for entry in logs: print(entry[message])每条message是一个JSON字符串里面包含method和params比如Network.requestWillBeSent、Network.responseReceived、Network.loadingFinished等。4.2 捕获请求/响应事件的字段解析拿到日志之后解析是重头戏。我习惯写一个简单的事件解析函数把关键字段抽出来import json for entry in logs: message json.loads(entry[message]) method message[method] params message.get(params, {}) if method Network.requestWillBeSent: request params[request] print(请求URL:, request[url]) print(请求方法:, request[method]) if postData in request: print(请求体:, request[postData]) elif method Network.responseReceived: response params[response] print(响应URL:, response[url]) print(状态码:, response[status]) print(响应头:, response[headers])这个方式不需要代理浏览器直接暴露数据性能损耗最小。但也有一个很直观的问题你只能拿到“事件参数”里暴露的字段response body本身并不在Network.responseReceived里。如果关心接口返回的具体内容还需要发Network.getResponseBody命令并且要传入请求IDrequest_id params[requestId] body driver.execute_cdp_cmd(Network.getResponseBody, {requestId: request_id}) print(body.get(body))需要注意的是并不是所有响应都能body。如果请求刚发出还没结束或者资源已经被回收会抛异常。所以我在封装时会加一个try/except或者放在Network.loadingFinished事件之后再调用。4.3 等待指定接口返回后再执行下一步CDP方式最大的优势是“实时性”。你可以通过轮询performance日志判断某个接口是否已经返回。比如我想在点击搜索按钮后等待/api/search接口返回再断言结果区可以这样写driver.find_element(...).click() def search_finished(driver): logs driver.get_log(performance) for entry in logs: message json.loads(entry[message]) if message[method] Network.responseReceived: url message[params][response][url] if /api/search in url: return True return False WebDriverWait(driver, 10).until(search_finished)注意一个坑driver.get_log(performance)在调用之后会清空日志缓冲。也就是说第二次调用不会返回第一次已经读过的内容。上面这个写法没问题因为WebDriverWait每次循环都会调用一次search_finished每次都会拉取新日志。但如果你用的是driver.requests这类累积式接口就别混着用否则容易把日志“读丢”。5. 拿捕获结果解决真实问题下载等待、上传失败、慢请求定位5.1 下载文件等到后端返回成功再断言很多人在自动化里处理下载是用os.path.exists去轮询下载目录。这个方法太脆了文件还没落盘时前端可能已经开始写临时文件下载中断时判断也会误报成功。更稳的做法是捕获下载请求确认HTTP层面真的返回了200再检查文件。用Selenium Wire实现很直接from seleniumwire import webdriver import os, time download_dir /tmp/downloads def download_success(driver, keyword): for req in driver.requests: if keyword in req.url and req.response and req.response.status_code 200: return True return False driver.find_element(...).click() wait WebDriverWait(driver, 30) wait.until(lambda d: download_success(d, /download)) # 再等文件落盘 deadline time.time() 10 while time.time() deadline: if any(report in f for f in os.listdir(download_dir)): break time.sleep(0.5)请求层面的成功配合文件系统层面的存在双条件都满足下载断言基本不会误报。5.2 定位“上传失败: 网络请求错误”到底是谁的锅我在排查上传问题时被“上传失败: 网络请求错误”这种提示坑过很多次。前端只给一段含糊文案既没有状态码也没有后端错误信息。很多人的第一反应是改脚本重试但只有把上传请求抓出来才能知道是请求本身的问题还是服务端的限制。有一次我在做小程序的自动化上传流程脚本里点击“上传”后前端一直弹“上传失败: 网络请求错误”。用Selenium Wire打印了上传请求的响应发现返回的联系是413 Request Entity Too Large后端明确提示代码包大小超过限制。这才发现问题根本不在Selenium脚本而是资源包体积超了阈值。围绕上传场景我一般会重点看三个东西上传URL是否发送成功请求Method是不是POST/PUT请求体是否有完整的文件流或form-data响应体的状态码和后端错误信息。用代码来定位也很简单for req in driver.requests: if req.method POST and upload in req.url: print(req.url) print(req.body[:200] if req.body else body为空) if req.response: print(req.response.status_code) print(req.response.body.decode(utf-8, errorsignore)[:500])很多时候“网络请求错误”不是网络问题而是请求参数异常、响应超时或文件体量过大。抓一次请求这个问题就水落石出了。5.3 通过时间戳统计每个接口的耗时做性能分析的时候我想知道一个页面上哪些接口最慢。Selenium Wire并没有直接给出每个请求耗时但我可以在操作前后清理请求列表再记录一个整体时间。更精细的做法是走CDP日志把Network.requestWillBeSent和Network.loadingFinished的时间戳配对算出每个请求的完整耗时。import json logs driver.get_log(performance) time_by_url {} for entry in logs: message json.loads(entry[message]) method message[method] params message.get(params, {}) if method Network.requestWillBeSent: url params[request][url] time_by_url[url] params[timestamp] # 用事件时间近似开始时间 elif method Network.loadingFinished: url None # 不能只靠URL因为同一个URL可能有多次请求更严谨的是用requestId做关联 request_id params[requestId] # 这里仅演示思路实际需要把requestId和URL建立映射 time_by_url[request_id] params[timestamp] - time_by_url.get(request_id, 0)这里必须提醒同一次会话中同一个URL可能被请求多次不能单纯用URL做key。最严谨的做法是在requestWillBeSent时把requestId和URL存成映射再用loadingFinished的requestId去回查。我在实际脚本中会维护一个字典key是requestIdvalue是开始时间最后再归并到URL上。6. 绕不开的坑证书、端口、并发、兼容性6.1 代理模式下的SSL证书信任只要走代理模式就一定会遇到HTTPS证书问题。Selenium Wire内置了一个CA证书但如果你在Docker容器里跑脚本容器里没有安装这个CAChrome就会拦截所有HTTPS请求。我踩过这个坑之后在初始化配置里强制加了两项options.add_argument(--ignore-certificate-errors) options.accept_insecure_certs True这样能解决大部分问题。如果还不行可以手动指定Selenium Wire的CA路径把证书塞进系统信任链不过比较麻烦。我一般直接建议用CDP方案来绕开代理尤其是跑在容器里的定时任务CDP模式不用处理代理证书省心很多。6.2 端口冲突与并行测试的隔离Selenium Wire启动时会自动选择一个空闲端口但当你用pytest-xdist或concurrent.futures开多个浏览器实例时端口分配偶尔会乱。我见过最诡异的现象是登录态的cookie被串到另一个浏览器实例上因为两个driver共用了一个代理端口。解决办法是手动给每个实例指定独立的端口from seleniumwire import webdriver options { port: 8090, # 每个线程用不同端口 } driver webdriver.Chrome(seleniumwire_optionsoptions)另外driver.requests是绑在实例上的多线程千万别共用一个driver去读请求列表否则数据会互相污染。我之前在写并发下载用例时就是因为全局共享driver导致请求列表里的URL对不上实例排查了半天才发现是线程安全问题。6.3 浏览器版本与CDP协议的兼容性CDP协议是跟着浏览器走的。Chrome每次大版本更新都可能有字段调整。比如Network.requestWillBeSent里的request.postData在旧版本叫postData新版本也可能变成postDataEntries某些内部请求比如chrome://newtab也会被记录过滤时必须排除chrome://和devtools://开头的URL。Selenium Wire对Chrome版本的兼容性也没有想象中好。它底层依赖某个版本的ChromeDriver协议如果Chrome自动升级到新版可能导致Selenium Wire无法建立代理会话。我的习惯是锁定浏览器版本或者在CI里用固定版本镜像而不是每次都拉最新版。生产环境稳定比追新更重要。另外不管用哪种方案请求日志都会占内存。页面越复杂网络事件越多长时间跑用例时最好定期清理一下历史请求。Selenium Wire里可以用del driver.requestsCDP日志则会在每次get_log之后自动清空这两种方式都能防止内存膨胀。我个人在项目里的习惯是功能测试、上传下载断言这类场景优先用Selenium Wire因为它把请求/响应体封装得最好读性能巡检、接口耗时统计这类轻量任务就切到CDP日志减少代理这一层转发数据也更接近真实用户体验。两种方式各有各的脾气但只要把上面这些坑都填平它们都能成为Selenium自动化脚本里非常可靠的那双“F12”眼睛。