1. 项目概述为什么我们需要Selenium Wire来生成HAR文件如果你做过Web性能分析或者排查过页面加载慢、接口响应异常的问题那你大概率听说过HAR文件。HAR全称HTTP Archive本质上是一个JSON格式的文件它像一部高清摄像机忠实地记录了一次网页加载过程中浏览器与服务器之间发生的所有网络请求和响应。从最初的HTML文档请求到后续的CSS、JavaScript、图片加载再到各种API接口调用每一个请求的URL、方法、请求头、响应头、状态码、时间戳、甚至请求体和响应体如果配置了捕获都会被原原本本地记录下来。传统的性能分析工具比如浏览器的开发者工具F12 - Network面板都支持导出HAR文件。这很方便对吧但它的局限性也非常明显这一切都依赖于人工在浏览器界面上的手动操作。你无法将它集成到自动化的测试流程中无法在无人值守的服务器上运行更无法对成百上千个页面进行批量的性能数据采集和分析。当性能测试需要规模化、自动化、持续化时手动导出HAR就成了瓶颈。这就是Selenium Wire登场的时候。Selenium Wire是Selenium的一个扩展它在标准Selenium WebDriver的能力之上增加了一个核心功能拦截和检查浏览器发起的HTTP/HTTPS请求。它就像一个安装在浏览器和网络之间的透明代理所有流量都经过它因此它可以轻松地捕获到完整的网络交互信息并将其导出为HAR格式。这意味着你可以用写代码的方式驱动浏览器完成复杂的用户操作登录、搜索、下单等同时自动捕获整个操作链路产生的HAR文件为后续的自动化性能分析、监控告警、瓶颈定位提供了坚实的数据基础。这不仅仅是“导出HAR”而是将性能数据采集变成了软件开发流程中的一个可编程、可集成的环节。2. 核心工具解析Selenium Wire与标准Selenium的异同在深入实操之前我们必须厘清Selenium Wire和标准Selenium以下简称Selenium的关系这决定了我们如何正确选择和使用它。相同点浏览器自动化。两者底层都基于WebDriver协议都能驱动真实的浏览器如Chrome, Firefox进行自动化操作比如打开网页、点击元素、填写表单、获取页面内容等。你熟悉的find_element_by_xxx、click()、send_keys()等方法在Selenium Wire中完全适用。核心差异网络请求拦截能力。这是Selenium Wire存在的根本理由。标准Selenium像一个坐在驾驶位的司机只能控制浏览器车去哪里、做什么但看不到也控制不了车与外界服务器通信的具体内容网络请求。Selenium Wire在司机Selenium和汽车浏览器之间安装了一个“高级行车记录仪可编程过滤器”。它不仅能记录所有进出的通信请求/响应还能让你在代码层面对这些通信进行读取、修改、阻塞或模拟。技术实现原理Selenium Wire启动时会在本地启动一个轻量级的HTTP代理服务器。当你通过Selenium Wire创建WebDriver时它会将浏览器的代理设置指向这个本地代理。此后浏览器发出的所有请求都会先经过这个代理代理记录下信息后再转发给目标服务器服务器的响应也会先经过代理记录再返回给浏览器。正是通过这个“中间人”角色它实现了全量的网络流量捕获。一个重要权衡性能与功能。由于所有流量都要经过额外的代理层Selenium Wire的执行速度会比纯Selenium慢一些并且会消耗更多的内存来存储请求/响应数据。因此它并非用来替代Selenium进行所有UI自动化测试而是专门用于那些需要深度网络监控和分析的场景比如自动化性能审计与指标收集。测试第三方API集成与广告跟踪。安全测试中检查敏感信息泄露。爬虫开发中应对反爬机制分析请求参数。注意Selenium Wire主要捕获由浏览器发起的HTTP/HTTPS请求。对于WebSocket、WebRTC等其他协议或者页面内通过fetch、XMLHttpRequest发起的请求它都能捕获。但对于使用原生TCP/UDP的通信则无能为力。3. 环境搭建与基础配置实战理论清晰后我们开始动手。一个稳定可复现的环境是成功的第一步。3.1 安装依赖首先确保你已安装Python建议3.7及以上版本。然后使用pip安装Selenium Wire及其依赖。这里有个关键点为了能捕获HTTPS请求Selenium Wire需要生成和安装自己的CA证书到浏览器中。pip install selenium-wire为什么不是selenium因为selenium-wire包已经自带了selenium作为其核心依赖之一所以你不需要单独安装selenium。安装完成后你可以从seleniumwire中导入webdriver。3.2 编写你的第一个HAR捕获脚本我们来创建一个最简单的脚本访问一个网页并保存HAR文件。from seleniumwire import webdriver import json import time # 1. 创建Selenium Wire的WebDriver实例 options { disable_encoding: True, # 禁用响应内容编码方便直接查看文本 request_storage: memory, # 将请求存储在内存中默认选项 } driver webdriver.Chrome(seleniumwire_optionsoptions) try: # 2. 发起网络请求 driver.get(https://httpbin.org/headers) # 一个方便的测试网站会返回请求头 # 3. 等待一下确保所有请求如图片、脚本加载完成 time.sleep(2) # 4. 获取并处理HAR数据 # 获取当前会话的HAR数据 har_data driver.har # 5. 将HAR数据保存为JSON文件 with open(my_first_har.har, w, encodingutf-8) as f: json.dump(har_data, f, indent2, ensure_asciiFalse) print(HAR文件已保存为 my_first_har.har) # 可选简单打印一下捕获到的请求数量 print(f共捕获到 {len(har_data[log][entries])} 个请求) finally: # 6. 关闭浏览器释放资源 driver.quit()代码逐行解析options: 我们通过一个字典来配置Selenium Wire的行为。disable_encodingTrue非常实用它告诉代理不要对响应内容进行压缩编码如gzip这样我们在HAR文件中看到的response.content.text就是可读的明文而不是乱码。driver.har: 这是Selenium Wire提供的属性直接返回一个符合HAR 1.2规范的Python字典。这个字典的结构与你在浏览器Network面板导出HAR后用文本编辑器打开看到的结构完全一致。json.dump(..., indent2, ensure_asciiFalse): 使用indent让生成的JSON文件有缩进便于人工阅读ensure_asciiFalse确保中文字符等Unicode字符能正确保存而不是被转义成\uXXXX的形式。首次运行会遇到的问题当你第一次运行上述脚本时Chrome浏览器可能会显示“您的连接不是私密连接”的警告页面。这是因为Selenium Wire自动生成的CA证书尚未被你系统的证书存储信任。脚本会暂停在这里需要你手动点击“高级”-“继续前往不安全”。这对于自动化来说是致命的。3.3 关键配置自动信任CA证书为了实现真正的无人值守自动化我们必须解决证书信任问题。有两种主流方法方法一启动时注入已信任的证书推荐思路是先运行一次脚本让Selenium Wire生成证书文件然后手动将其导入到系统的受信任根证书颁发机构。之后所有使用该配置文件的浏览器实例都会自动信任代理。首次运行后在代码所在目录或用户主目录下找到Selenium Wire生成的证书通常是一个.pem文件具体路径可在脚本中通过print(driver.proxy.ca_cert)查看。将.pem证书导入到你的操作系统或浏览器的受信任根证书中具体步骤因系统而异。在创建WebDriver时指定该证书路径。from seleniumwire import webdriver options { ca_cert: /path/to/your/seleniumwire-ca.pem, # 指定已受信任的CA证书路径 } driver webdriver.Chrome(seleniumwire_optionsoptions)方法二使用ignore_certificate_errors选项快速但不够安全这个选项会让Selenium Wire的代理忽略所有证书错误相当于在代码层面点击了“继续前往”。这种方法最快捷但会降低HTTPS连接的安全性仅建议在可控的测试环境中使用。from seleniumwire import webdriver options { ignore_certificate_errors: True, } driver webdriver.Chrome(seleniumwire_optionsoptions)实操心得在团队协作或CI/CD环境中推荐使用方法一。可以创建一个专门的“证书准备”步骤将受信的CA证书作为安全凭据或配置文件提前部署到运行环境中。方法二虽然方便但可能会掩盖一些真实的证书配置问题不适用于对安全性有要求的测试场景。4. 高级捕获策略与HAR文件优化基础的捕获只能满足简单场景。在实际项目中页面复杂、请求繁多我们需要更精细的控制策略来获取高质量、有针对性的HAR数据。4.1 请求过滤只捕获你关心的一个现代单页应用SPA加载一次可能产生上百个请求。如果你只关心其中某个API的响应捕获全部请求会产生大量噪音并消耗不必要的内存和磁盘空间。Selenium Wire提供了强大的请求过滤功能。from seleniumwire import webdriver import json # 定义过滤规则 def custom_filter(request): # 只捕获来自特定域名或路径的请求 if api.example.com in request.host: return True # 只捕获POST请求 if request.method POST: return True # 排除所有图片请求 if request.path.endswith((.png, .jpg, .gif, .ico)): return False # 默认情况下捕获其他请求 return True options { request_storage: memory, request_storage_max_size: 100, # 内存中只保留最近100个请求 disable_capture: True, # 先全局禁用捕获 custom_response_handler: custom_filter, # 使用自定义过滤器 } driver webdriver.Chrome(seleniumwire_optionsoptions) # 启用捕获但只针对通过过滤器的请求 driver.scopes [.*] # 这是一个正则表达式列表匹配所有URL。结合custom_filter使用。 try: driver.get(https://www.example.com) # ... 执行一些操作 ... # 获取通过过滤器捕获的请求 for request in driver.requests: if request.response: print(f{request.method} {request.url} - {request.response.status_code}) # 保存过滤后的HAR har_data driver.har with open(filtered.har, w, encodingutf-8) as f: json.dump(har_data, f, indent2) finally: driver.quit()代码解析与技巧disable_captureTrue先关闭全局捕获提升性能。custom_filter函数这是过滤的核心。它接收一个request对象返回True表示捕获False表示忽略。你可以在这里实现任何复杂的逻辑。driver.scopes这是一个正则表达式列表。只有当请求的URL匹配scopes中的任一模式时才会被交给custom_filter函数判断。设为[.*]表示所有请求都先进入过滤流程。driver.requests这是一个列表存储了所有已捕获的请求对象无论是否已收到响应。你可以遍历它进行实时分析。内存管理通过request_storage_max_size限制内存中存储的请求数量防止长时间运行导致内存溢出。超过数量的旧请求会被自动清除。4.2 捕获请求与响应正文默认情况下为了性能和节省内存Selenium Wire不会捕获请求和响应的正文内容body。但在性能分析中请求体如表单数据、JSON参数和响应体如API返回的JSON至关重要它们能帮你分析数据传输效率。options { disable_encoding: True, # 必须为True否则响应正文可能是压缩后的乱码 request_storage: memory, capture_request_body: True, # 启用请求体捕获 capture_response_body: True, # 启用响应体捕获 # 限制响应体大小避免大文件如视频撑爆内存 response_body_max_size: 1024 * 1024, # 1MB } driver webdriver.Chrome(seleniumwire_optionsoptions)重要注意事项disable_encodingTrue是前提如果服务器返回gzip压缩的内容而此选项为Falseresponse.body将是压缩后的二进制数据无法直接阅读。警惕大文件capture_response_bodyTrue会捕获所有响应体包括图片、视频、大型JS文件。务必设置response_body_max_size来保护你的程序。超过此大小的响应体将不会被捕获其response.body属性为None。访问正文数据捕获后你可以通过request.body和response.body来访问。它们通常是字节串bytes。对于文本内容如JSON你需要解码if request.body: print(f请求体: {request.body.decode(utf-8)}) if response.body: # 注意response.body可能是bytes也可能是str当disable_encodingTrue且内容是文本时 body_content response.body if isinstance(body_content, bytes): body_content body_content.decode(utf-8) print(f响应体: {body_content[:500]}) # 只打印前500字符4.3 处理动态内容与等待策略在SPA中很多内容是通过JavaScript异步加载的。如果你在页面刚加载完就立即保存HAR可能会错过这些关键的异步请求。错误的做法driver.get(url) har driver.har # 此时异步请求可能还没开始或没完成正确的做法使用显式等待。等待特定元素出现使用Selenium的WebDriverWait这表示页面主体框架和关键内容已加载。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By driver.get(url) # 等待页面主体或某个关键元素加载完成 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.TAG_NAME, body)) ) # 可以再等待一个由JS动态渲染的元素 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, dynamic-content)) ) time.sleep(1) # 额外等待1秒确保后续异步请求也完成 har driver.har等待网络空闲更高级的策略是监听网络请求状态。可以检查在一段时间内是否有新的请求发生或者所有请求的响应是否都已返回。这需要你定期检查driver.requests列表。import time def wait_for_network_idle(driver, timeout30, idle_time2): 等待网络空闲在idle_time秒内没有新请求且所有已有请求完成 end_time time.time() timeout last_request_count len(driver.requests) last_change_time time.time() while time.time() end_time: current_count len(driver.requests) # 检查是否有新请求 if current_count ! last_request_count: last_request_count current_count last_change_time time.time() # 检查所有已捕获请求是否都已完成 all_done all(req.response for req in driver.requests) # 如果请求数稳定且所有请求完成并持续了idle_time秒则认为空闲 if all_done and (time.time() - last_change_time) idle_time: return True time.sleep(0.5) return False driver.get(url) if wait_for_network_idle(driver): print(网络已空闲可以捕获HAR。) har driver.har else: print(网络等待超时捕获的HAR可能不完整。)实操心得对于复杂的页面结合使用“元素等待”和“网络空闲等待”是最稳妥的。time.sleep()应作为最后的手段并尽量使用最短的必要时间。在性能测试中等待时间本身也是需要记录和分析的指标之一。5. HAR文件深度解析与性能指标提取拿到HAR文件只是第一步从这座数据金矿中提炼出有价值的性能指标才是最终目的。HAR文件结构清晰我们可以用Python轻松解析。5.1 HAR文件结构速览一个HAR文件的顶层结构如下{ log: { version: 1.2, creator: { ... }, // 创建者信息如Selenium Wire pages: [ ... ], // 页面列表一次浏览器会话可能有多个页面 entries: [ ... ] // 这是核心所有网络请求的数组 } }每个entry条目对应一个网络请求包含request请求信息、response响应信息、timings时间指标等关键对象。5.2 使用Python进行核心指标分析让我们编写一个分析脚本计算一些关键性能指标。import json from datetime import datetime def analyze_har(har_file_path): with open(har_file_path, r, encodingutf-8) as f: har_data json.load(f) entries har_data[log][entries] print(f总请求数: {len(entries)}) # 初始化统计变量 total_size 0 # 总传输大小字节 requests_by_type {} slow_requests [] # 记录慢请求例如耗时1秒 # 各阶段时间统计 dns_times [] connect_times [] ssl_times [] send_times [] wait_times [] receive_times [] for entry in entries: request entry[request] response entry[response] timings entry[timings] # 1. 按MIME类型统计请求 content_type response.get(content, {}).get(mimeType, unknown) # 简化分类 if javascript in content_type: req_type JS elif css in content_type: req_type CSS elif image in content_type: req_type Image elif html in content_type: req_type HTML elif font in content_type: req_type Font else: req_type Other requests_by_type[req_type] requests_by_type.get(req_type, 0) 1 # 2. 计算请求总大小注意HAR中的size是压缩后的大小 body_size response.get(bodySize, 0) headers_size response.get(headersSize, 0) total_size (body_size headers_size) # 3. 提取并计算各阶段时间单位毫秒 # HAR中-1表示该阶段不适用或未测量 dns timings.get(dns, -1) connect timings.get(connect, -1) ssl timings.get(ssl, -1) # SSL/TLS协商时间 send timings.get(send, 0) # 发送请求头/体的时间 wait timings.get(wait, 0) # 等待服务器响应的时间TTFB receive timings.get(receive, 0) # 接收响应数据的时间 # 计算总耗时排除无效值 total_time sum([t for t in [dns, connect, ssl, send, wait, receive] if t 0]) # 收集有效时间数据用于后续分析 if dns 0: dns_times.append(dns) if connect 0: connect_times.append(connect) if ssl 0: ssl_times.append(ssl) send_times.append(send) wait_times.append(wait) receive_times.append(receive) # 4. 识别慢请求例如总耗时1000ms if total_time 1000: slow_requests.append({ url: request[url], method: request[method], status: response[status], total_time_ms: total_time, content_type: content_type }) # 输出统计结果 print(f\n总传输数据量: {total_size / 1024:.2f} KB) print(f\n请求类型分布:) for req_type, count in sorted(requests_by_type.items()): print(f {req_type}: {count}) # 计算各阶段时间的平均值P50中位数更抗干扰这里为简单用平均 def avg(time_list): return sum(time_list) / len(time_list) if time_list else 0 print(f\n各阶段平均耗时ms:) print(f DNS查询: {avg(dns_times):.2f}) print(f TCP连接: {avg(connect_times):.2f}) print(f SSL握手: {avg(ssl_times):.2f}) print(f 请求发送: {avg(send_times):.2f}) print(f 等待响应TTFB: {avg(wait_times):.2f}) print(f 接收数据: {avg(receive_times):.2f}) print(f\n慢请求1000ms数量: {len(slow_requests)}) for req in slow_requests[:5]: # 只显示前5个最慢的 print(f - {req[method]} {req[url]} ({req[status]}) - {req[total_time_ms]:.0f}ms) # 5. 找出可能阻塞页面渲染的关键请求 # 通常是第一个HTML文档以及后续阻塞渲染的JS/CSS print(f\n关键请求分析:) for entry in entries: request entry[request] if request[url].endswith(.html) or text/html in entry[response].get(content, {}).get(mimeType, ): print(f 主文档: {request[url]} - {entry[time]}ms) break # 可以进一步分析哪些JS/CSS是渲染阻塞的通过initiator和timing判断 # 使用函数 analyze_har(my_performance_test.har)这个脚本提供了什么宏观概览总请求数、总数据量让你对页面“重量”有直观认识。资源构成了解页面由多少图片、脚本、样式表组成优化时优先针对大户。耗时分解将总耗时拆解为DNS、连接、SSL、等待、传输等阶段。如果waitTTFB时间普遍很长可能是服务器响应慢或后端处理瓶颈如果receive时间长可能是资源太大或网络带宽不足。问题定位自动找出最慢的请求直接给出URL方便快速定位性能瓶颈。关键路径识别主文档请求这是性能优化的起点。5.3 可视化与持续监控对于一次性分析命令行输出足够。但对于长期监控或向团队报告可视化图表更有力。你可以将分析脚本得到的数据如各阶段时间、慢请求列表导入到matplotlib、plotly或seaborn库中生成图表。更进一步的你可以将这套“Selenium Wire捕获 - HAR分析 - 指标提取 - 报告生成”的流程集成到你的CI/CD流水线中。每次代码部署后自动运行一组关键用户旅程如首页加载、登录、核心交易的脚本生成HAR并分析核心性能指标如首屏时间、可交互时间对应的请求完成点与历史基线对比如果出现性能退化则自动告警。6. 常见问题排查与实战技巧实录在实际使用中你肯定会遇到各种“坑”。下面是我从大量实践中总结出的典型问题及其解决方案。6.1 证书错误与安全警告问题浏览器提示“您的连接不是私密连接”或“NET::ERR_CERT_AUTHORITY_INVALID”。原因Selenium Wire的自签名CA证书未被操作系统或浏览器信任。解决方案测试环境全局忽略使用ignore_certificate_errorsTrue选项。这是最快的方法。生产/长期信任证书运行一次脚本找到证书文件ca_cert driver.proxy.ca_cert打印路径。将证书.pem文件导入到系统的受信任根证书颁发机构。后续脚本使用ca_cert‘/path/to/cert.pem’选项。使用已配置好的浏览器用户数据目录先手动用浏览器访问一次添加证书例外然后Selenium Wire使用该用户数据目录启动。from selenium.webdriver.chrome.options import Options from seleniumwire import webdriver chrome_options Options() chrome_options.add_argument(r--user-data-dirC:\Path\To\Your\Chrome\Profile) driver webdriver.Chrome(seleniumwire_options{...}, optionschrome_options)6.2 捕获不到请求或请求不完整问题driver.requests列表为空或者HAR文件中entries为空。排查步骤检查代理是否生效在脚本开头添加print(driver.proxy)确认代理地址和端口。在浏览器中手动设置该代理看能否正常上网。确认disable_capture设置如果你设置了disable_captureTrue必须通过driver.scopes或custom_filter来启用对特定请求的捕获。检查你的过滤逻辑是否过于严格导致所有请求都被过滤掉。检查请求时机你是否在页面加载或操作完成前就获取了driver.har确保使用了正确的等待策略见4.3节。HTTPS请求捕获确认证书问题已解决见上一点。对于某些使用严格证书钉扎Certificate Pinning的网站Selenium Wire可能无法拦截其HTTPS流量。6.3 内存消耗过大或程序变慢问题长时间运行或访问复杂页面后程序内存占用飙升运行缓慢。原因Selenium Wire默认将所有请求/响应存储在内存中。优化方案启用请求过滤使用custom_filter只捕获必要的请求这是最有效的办法。设置存储上限使用request_storage_max_size限制内存中保留的请求数量。使用文件存储实验性Selenium Wire支持将请求存储到临时文件中但稳定性待验证。options { request_storage: file, request_storage_base_dir: /tmp/seleniumwire }定期清理在处理完一批请求后可以手动清理driver.requests和driver.har。# 保存当前HAR后清理 save_har_to_file(driver.har) del driver.requests driver.har {} # 重置HAR6.4 HAR文件中响应正文为乱码或为空问题HAR文件里response.content.text字段是乱码、binary data或者直接为空。原因与解决未启用正文捕获检查是否设置了capture_response_bodyTrue。未禁用编码检查是否设置了disable_encodingTrue。如果服务器返回gzip压缩的内容此选项为False会导致正文是压缩后的二进制数据。响应体过大检查是否超过了response_body_max_size的限制。非文本内容对于图片、视频等二进制内容HAR标准中text字段就是binary data这是正常的。原始数据在response.body中如果被捕获。6.5 与异步JavaScriptAJAX/SPA的兼容性问题问题在单页应用中页面初始化后通过JavaScript发起的请求没有被捕获。解决确保等待充分使用4.3节介绍的“网络空闲等待”函数。检查请求发起方有些请求可能由Web Worker或Service Worker发起Selenium Wire可能无法捕获取决于浏览器和代理配置。这是已知限制。使用浏览器开发者工具验证手动操作一遍在Network面板确认你期望的请求确实存在且能被观察到。6.6 在Docker或无头环境中运行在Docker或服务器上使用无头浏览器Headless Chrome时除了上述问题还需注意安装浏览器确保容器内安装了Chrome或Firefox以及对应的WebDriver。无头模式参数from selenium.webdriver.chrome.options import Options chrome_options Options() chrome_options.add_argument(--headless) # 无头模式 chrome_options.add_argument(--no-sandbox) # Docker中常需要 chrome_options.add_argument(--disable-dev-shm-usage) # 共享内存限制 driver webdriver.Chrome(seleniumwire_options{...}, optionschrome_options)证书信任在无头环境中方法一导入证书可能更复杂。通常采用ignore_certificate_errorsTrue更为直接但需评估安全影响。我个人在将这套方案集成到自动化测试平台时最大的体会是标准化和模块化。我将HAR捕获、等待策略、基础分析函数都封装成了独立的类或函数在不同的性能测试场景中复用。同时为每个重要的测试场景建立性能基线HAR文件及关键指标任何代码变更后都自动对比能非常敏锐地捕捉到因一个依赖库升级或一段新代码引入而导致的性能回退这在持续交付中价值巨大。最后记住HAR文件本身也是宝贵的测试数据可以考虑将其与测试报告一起归档为后续的问题回溯提供完整的数据上下文。