
标准 selenium 并非为直接捕获 api 请求设计,但 `selenium-wire` 扩展了其功能,允许开发者在自动化测试中拦截、检查和分析浏览器与服务器之间的所有网络流量。本文将详细介绍如何利用 `selenium-wire` 捕获特定 api 请求及其 json 响应数据,从而在不改变页面 url 的情况下,获取前端与后端交互的深层信息,适用于需要监控或验证 api 通信的自动化场景。
理解 Selenium 在 API 捕获中的局限性
Selenium 主要是一个浏览器自动化工具,用于模拟用户与网页的交互,如点击、输入、导航等。它关注的是用户可见的界面行为和页面状态变化。虽然可以通过 WebDriver 的日志(如 performance 日志)或执行 CDP(Chrome DevTools Protocol)命令来尝试获取网络请求信息,但这些方法通常较为复杂、不够直观,且可能依赖于特定浏览器或版本,维护成本较高。
当需求是捕获由前端行为(例如点击按钮)触发的后端 API 请求及其响应数据,特别是当页面 URL 不变而仅有 API 端点发生交互时,标准 Selenium 的能力显得捉襟见肘。此时,我们需要一个更专业的工具来直接监听和解析网络流量。
引入 Selenium-Wire:网络流量的监听器
selenium-wire 是一个 Python 库,它通过在 Selenium WebDriver 和浏览器之间设置一个代理(Proxy),从而能够拦截和检查所有流经浏览器的网络请求和响应。这使得开发者可以轻松地访问请求头、请求体、响应头和响应体,并对它们进行过滤和分析。
Selenium-Wire 的核心优势:
- 透明代理: 无需额外配置浏览器代理,selenium-wire 会自动处理。
- 全面捕获: 能够捕获所有类型的网络请求,包括 XHR/Fetch 请求、图片、CSS、JS 等。
- 易于访问: 提供简洁的 API 来访问请求和响应的各个部分。
- 灵活过滤: 可以根据 URL、方法、头信息等条件过滤请求。
安装与配置
首先,需要安装 selenium-wire 库。
立即学习“前端免费学习笔记(深入)”;
pip install selenium-wire
安装完成后,可以在 Python 代码中引入并配置 selenium-wire 的 WebDriver。
from selenium_wire import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import json
import time
import logging
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def capture_api_response(start_url, element_id_to_click, api_url_part):
"""
使用 selenium-wire 捕获点击元素后触发的 API 请求及其 JSON 响应。
Args:
start_url (str): 浏览器起始访问的 URL。
element_id_to_click (str): 需要点击的元素的 ID。
api_url_part (str): 目标 API URL 中包含的特定字符串,用于过滤请求。
Returns:
dict: 如果找到目标 API 的 JSON 响应,则返回解析后的字典;否则返回 None。
"""
# 配置 WebDriver 选项
options = webdriver.ChromeOptions()
# 可以根据需要添加其他选项,例如无头模式
# options.add_argument('--headless')
# options.add_argument('--disable-gpu')
# 配置 selenium-wire 的 WebDriver
# service = Service(executable_path='/path/to/chromedriver') # 如果 chromedriver 不在 PATH 中,需要指定路径
driver = webdriver.Chrome(options=options)
driver.set_page_load_timeout(30) # 设置页面加载超时时间
found_data = None
try:
logger.info(f"正在访问 URL: {start_url}")
driver.get(start_url)
# 清除所有历史请求,确保只捕获本次操作产生的流量
driver.delete_all_requests()
logger.info("已清除所有历史网络请求。")
# 等待元素可点击并执行点击操作
wait = WebDriverWait(driver, 10)
element_to_click = wait.until(EC.element_to_be_clickable((By.ID, element_id_to_click)))
logger.info(f"找到元素 '{element_id_to_click}',准备点击。")
element_to_click.click()
logger.info(f"已点击元素 '{element_id_to_click}'。")
# 等待一段时间,让 API 请求有足够的时间完成并响应
# 实际项目中,可能需要更智能的等待机制,例如等待某个特定的 DOM 元素出现或消失
time.sleep(5)
logger.info("等待 5 秒以捕获网络请求。")
# 遍历所有捕获到的请求
for request in driver.requests:
# 确保请求有响应,并且响应 URL 包含目标 API 的部分
if request.response and api_url_part in request.url:
logger.info(f"捕获到潜在目标API请求: {request.url}")
# 检查响应内容类型是否为 JSON
content_type = request.response.headers.get('Content-Type', '')
if 'application/json' in content_type:
try:
# 获取响应体并解码为 UTF-8 字符串,然后解析为 JSON
body = request.response.body.decode('utf-8')
json_data = json.loads(body)
logger.info(f"成功捕获并解析目标API JSON响应: {request.url}")
logger.info(f"响应数据示例: {json_data}")
found_data = json_data
break # 找到目标数据后即可停止遍历
except json.JSONDecodeError as e:
logger.warning(f"警告: 捕获到 JSON 类型响应但解析失败,URL: {request.url}, 错误: {e}")
except Exception as e:
logger.error(f"处理响应体时发生未知错误,URL: {request.url}, 错误: {e}")
else:
logger.info(f"捕获到非 JSON 响应,URL: {request.url}, Content-Type: {content_type}")
if not found_data:
logger.info(f"未捕获到包含 '{api_url_part}' 的目标API请求及其 JSON 响应。")
except Exception as e:
logger.error(f"在执行过程中发生错误: {e}")
finally:
driver.quit()
logger.info("浏览器已关闭。")
return found_data
# --- 示例用法 ---
if __name__ == "__main__":
# 替换为你的实际场景信息
example_start_url = "https://www.example.com/data_page" # 假设你的页面URL
example_element_id = "loadDataButton" # 假设点击这个ID的按钮会触发API请求
example_api_url_part = "/api/v1/getData" # 假设你的API路径包含这个部分
# 模拟一个简单的 HTML 页面,用于测试
# 你需要手动创建一个这样的 HTML 文件或者有一个真实的页面
# 例如,创建一个 `test_page.html` 文件:
# ```html
# <!DOCTYPE html>
# <html>
# <head>
# <title>Test Page</title>
# <script>
# function loadData() {
# fetch('/api/v1/getData', { method: 'GET' })
# .then(response => response.json())
# .then(data => {
# document.getElementById('result').innerText = JSON.stringify(data, null, 2);
# console.log('Data loaded:', data);
# })
# .catch(error => console.error('Error:', error));
# }
# </script>
# </head>
# <body>
# <h1>API Data Loader</h1>
# <button id="loadDataButton" onclick="loadData()">Load Data</button>
# <pre id="result"></pre>
# </body>
# </html>
# ```
# 然后你可以用一个本地服务器(如 Python 的 http.server)来提供这个文件
# 并在 `example_start_url` 中使用 `http://localhost:8000/test_page.html`
# 注意:为了让此示例代码运行,你需要有一个真实的网页和 API 端点。
# 如果只是为了演示 selenium-wire 的功能,可以访问一个已知会触发 AJAX 请求的公共网站,
# 并调整 element_id_to_click 和 api_url_part。
# 假设我们有一个本地测试服务器运行在 8000 端口,提供 test_page.html
# example_start_url = "http://localhost:8000/test_page.html"
# example_element_id = "loadDataButton"
# example_api_url_part = "/api/v1/getData"
# 运行捕获函数
api_response_data = capture_api_response(
start_url=example_start_url,
element_id_to_click=example_element_id,
api_url_part=example_api_url_part
)
if api_response_data:
print("\n最终捕获到的 API 响应数据:")
print(json.dumps(api_response_data, indent=2, ensure_ascii=False))
else:
print("\n未能成功捕获到目标 API 响应数据。")
示例代码解析
- 导入必要的库: selenium_wire.webdriver 是核心,用于创建支持网络监听的 WebDriver 实例。其他如 By, WebDriverWait, EC 用于 Selenium 常见操作,json 用于处理 JSON 响应,time 用于简单的等待,logging 用于输出信息。
- capture_api_response 函数: 封装了整个捕获逻辑,提高了代码的复用性。
- WebDriver 初始化: driver = webdriver.Chrome(options=options) 创建了一个 selenium-wire 增强的 Chrome WebDriver 实例。所有通过此 driver 发起的请求都会被 selenium-wire 代理。
- 访问页面: driver.get(start_url) 导航到目标网页。
- 清除请求记录: driver.delete_all_requests() 是一个非常关键的步骤。在执行可能触发 API 请求的操作之前调用它,可以确保 driver.requests 列表中只包含后续操作产生的网络流量,避免混淆。
- 模拟用户操作: 使用 WebDriverWait 和 EC 确保元素加载并可点击,然后通过 element_to_click.click() 模拟用户点击操作,这个操作预期会触发一个 API 请求。
- 等待请求完成: time.sleep(5) 提供了一个简单的等待机制,确保浏览器有足够的时间来发送请求并接收响应。在实际应用中,更健壮的方法可能是等待某个特定的 DOM 元素出现或消失,或者使用 WebDriverWait 结合自定义的 expected_conditions 来判断 API 请求是否完成。
-
遍历并过滤请求:
- for request in driver.requests: 遍历 selenium-wire 捕获到的所有请求。
- if request.response and api_url_part in request.url: 这是一个重要的过滤条件。它首先检查请求是否有对应的响应(即请求已完成),然后检查请求的 URL 是否包含我们关注的 API 端点的一部分。
- if 'application/json' in content_type: 进一步过滤,确保响应是 JSON 格式。
- body = request.response.body.decode('utf-8') 获取响应体,它通常是字节流,需要解码成字符串。
- json_data = json.loads(body) 将字符串解析成 Python 字典。
- 错误处理与日志: 使用 try...except 块处理 JSON 解析错误或其他异常,并利用 logging 模块输出详细的运行信息,便于调试和监控。
- 关闭浏览器: driver.quit() 在操作完成后关闭浏览器实例,释放资源。
注意事项
- 异步性: 网络请求是异步的。在点击触发 API 的元素后,需要给予浏览器足够的时间来完成请求和响应。简单的 time.sleep() 并非最佳实践,应考虑更智能的等待策略,例如等待特定数据出现在页面上,或者等待 driver.requests 列表中出现符合条件的请求。
- 请求过滤: 根据实际需求,可以组合多种过滤条件(如 request.method, request.headers, request.body 等)来精确匹配目标 API 请求。
- 响应体编码: 响应体 request.response.body 是字节数据,需要根据实际编码(通常是 UTF-8)进行解码。
- 代理配置: selenium-wire 默认会为 WebDriver 配置一个代理。如果你的测试环境有特殊的网络代理需求,可以通过 selenium_wire.webdriver.Chrome(seleniumwire_options={'proxy': ...}) 来进行配置。
- 资源清理: 每次执行完测试后,务必调用 driver.quit() 关闭浏览器实例,防止资源泄露。
- 无头模式: 在 CI/CD 环境中,通常会使用无头模式 (options.add_argument('--headless')) 运行浏览器,这不会影响 selenium-wire 的功能。
总结
selenium-wire 极大地扩展了 Selenium 在网络流量监控方面的能力,使其成为一个强大的工具,不仅能模拟用户交互,还能深入洞察前端与后端之间的通信。通过本文介绍的方法,开发者可以轻松地在自动化测试中捕获和分析 API 请求及其响应,这对于验证数据流、检测后端问题或在复杂的单页应用中提取关键数据至关重要。掌握 selenium-wire 将使你的自动化测试和数据抓取能力提升到一个新的水平。








