简介:针对Selenium自动化测试中模拟POST请求的需求,这份资源面向需要参考接口请求封装实现的Web自动化开发者,解决了如何借助WebDriver执行脚本构造并提交POST参数、再解析异步返回JSON的问题。资源包共3个文件,包含Java源文件、txt说明文档及bak备份文件三类,整体仅5KB,结构轻量,便于直接阅读或修改复用。目前已有3698人学习下载,适合作为接口联调与自动化测试的参考片段。内容不仅提供可运行的封装示例,还通过文本资料对异步请求触发、响应获取及JSON解析等关键点进行说明,bak文件则可用于对比版本调整。对于想理解“用UI自动化工具发送POST请求”这一非典型用法的开发者,能有效节省自行摸索的时间。
1. Selenium 传 POST 参数:别把它当 requests,先想清楚要的是哪个层
直接给结论:Selenium 本身并没有提供类似driver.post(url, data)的接口,想用它直接提交 POST 参数,本质上是在跟浏览器的自动化协议较劲。很多人一上来就写driver.execute_script("$.post(...)"),结果页面跳转了、cookie 没同步、前端框架拦截了请求,折腾半天还是在原地打转。我拆过不少模拟项目 X 的登录模块,最后发现真正靠谱的路径只有两条:要么把 POST 请求从 Selenium 里剥离出来,交给 requests 这类 HTTP 库去发,再做 cookie 或 token 的桥接;要么停留在浏览器层面,用 CDP(Chrome DevTools Protocol)拦截或构造请求。这篇笔记就是把这两条路拆开讲清楚,包括参数怎么封装、cookie 怎么同步、以及我踩过的几个坑,适合正在做自动化登录、接口联调或爬虫落地的人。
2. 为什么 Selenium 不能直接发 POST:协议层的边界与三条变通思路
2.1 WebDriver 协议设计里就没有发请求的语义
Selenium 的核心是 WebDriver 协议,它定义的操作对象是浏览器,包括元素查找、点击、输入、执行脚本、切换窗口等,但从来没有send_request这类接口。你可以把它理解成遥控器上的按键集合,按键再多也变不出一根网线。POST 提交属于网络层的动作,浏览器没有暴露给 WebDriver 一个「发 HTTP 请求」的入口,这是设计使然,不是遗漏。
那为什么有人觉得能发?其实是绕了路。比如execute_script里跑fetch或者XMLHttpRequest,这确实能发出 POST 请求,但它是在页面上下文里发的——要受同源策略约束、要处理跨域、要处理前端框架的拦截器,而且这个请求的响应你是拿不回来的(除非再做一次回传)。如果你只是想触发某个接口,那没问题;但如果你需要拿到响应体、需要设 headers、需要处理重定向,这条路就走不通了。
2.2 三条变通路径的选型判断
我在实际拆资源时,一般会根据场景把方案分成三档,供你按需取舍。
| 需求场景 | 推荐方案 | 理由 |
|---|---|---|
| 登录接口需要拿到 token/cookie,再让浏览器复用 | requests 独立发 POST + cookie 注入 Selenium | 响应处理灵活,cookie 可控 |
| 页面内部有 AJAX POST,希望它被「自然」触发 | 正常操作页面,或用 execute_script 调用页面的方法 | 浏览器行为最真实,反爬最少 |
| 需要拦截/改写浏览器发出的某个 POST 请求 | CDP 的 Fetch domain 拦截并改写 | 不影响页面逻辑,能精确控制请求头 |
绝大多数我见到的情况是第一种。比如某个跨平台系统的登录,接口是/api/user/login,前端拿到 token 后存 localStorage,Selenium 直接访问业务页面会因为没有 token 而跳回登录页。这时候你在浏览器里填表单走登录流程也能过,但速度慢、且经常被图形验证码卡住。用 requests 先把登录接口调通,拿到 token 再注入浏览器,整个自动化流程能快十倍。
2.3 一个常见的翻车写法:execute_script 里 setTimeout
有人会在execute_script里写setTimeout配合fetch发 POST,然后立刻用sleep等请求完成。这里有两个问题:第一,execute_script默认只会执行到脚本的最后一行,异步回调不会等你,sleep是玄学,网络慢一点就取不到结果;第二,fetch 的响应体在页面上下文里,Selenium 侧拿不到,除非再写一套回调把数据塞回 DOM。我建议直接放弃这个思路,回到 requests 方案。
3. 把 POST 参数封装成独立函数:requests 版登录模块实战
3.1 封装思路:一个函数只管一个接口
我先说结论:用 requests 封装 POST 提交,重点不是把参数写进一个函数,而是把「登录接口的调用」做成独立模块,让返回值同时包含可用 cookie 和登录后的状态数据。这样 Selenium 侧只用做一步——把 cookie 注入浏览器,剩下的验证码识别、失败重试、token 刷新都在 requests 层处理。
下面是一个我常用的最小封装:
import requests def post_login(base_url, username, password, extra=None): session = requests.Session() login_url = f"{base_url}/api/user/login" payload = { "username": username, "password": password, "remember": False, } if extra: payload.update(extra) headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Content-Type": "application/json", "Origin": base_url, } resp = session.post(login_url, json=payload, headers=headers, timeout=10) resp.raise_for_status() data = resp.json() if data.get("code") != 0: raise RuntimeError(f"login failed: {data}") # 返回 session 和解析后的用户信息 return session, data这段代码的要点有几个:
- 用
requests.Session()而不是每次新建requests.post,是因为 session 会自动保存服务端下发的 cookie,后续请求能直接复用。 json=payload会把参数序列化成application/json格式,如果你的接口是表单提交(application/x-www-form-urlencoded),就改成data=payload,这两个不能混用,后端解析方式不一样。Origin头不是所有接口都校验,但登录接口常见校验。如果漏了,服务端可能返回 403。timeout=10必须写,不写的话未知网络环境下请求可能挂死,这是血泪经验。resp.raise_for_status()会把非 2xx 状态码转成异常,便于定位问题。
3.2 验证码与动态参数的处理
很多登录接口不只是用户名和密码,还可能带验证码、时间戳、签名等动态参数。我的处理方式是把它做成参数化字段,而不是写死在函数里:
def post_login_with_captcha(base_url, username, password, captcha_id, captcha_text): session = requests.Session() # 第一步:获取验证码 ID code_url = f"{base_url}/api/captcha" code_resp = session.get(code_url, timeout=10) captcha_info = code_resp.json() # 第二步:提交登录 login_url = f"{base_url}/api/user/login" payload = { "username": username, "password": password, "captchaId": captcha_id, "captcha": captcha_text, "timestamp": captcha_info.get("timestamp"), } resp = session.post(login_url, data=payload, timeout=10) return session, resp这个例子想说明的是:POST 参数的封装不是简单地把字段塞进 dict,而是要考虑请求之间的依赖关系。你先请求验证码接口拿到 id 和时间戳,再组装登录参数,这中间的顺序不能乱。否则登录接口会一直报「验证码过期」,而你根本看不出来是验证码错还是时间戳错。
参数说明我列一下:
captcha_id不是用户输入的验证码内容,是服务端返回的验证码编号,常用于校验来源。timestamp如果接口校验时间窗口(比如 5 分钟内有效),必须在请求发出前实时获取,不能缓存。data=和json=的选择,要看后端接口文档。通常 Java 系的接口更常见data=表单格式,Go 和 Node 系更喜欢json=。不确定的时候,先抓包看请求体格式。
4. 让 Selenium 用上 POST 的成果:cookie 注入与 localStorage 同步
4.1 从 session 到 WebDriver 的 cookie 迁移
这是整个封装里最容易翻车的一步。requests 的 session 保存的 cookie,和 Selenium 的 driver 默认的 cookie 机制不是一回事。requests 的 cookie 是字典形态,Session.cookies可以直接遍历;Selenium 的add_cookie要求 dict 里必须有name和value,而且域名必须是当前访问的域名,不然会被 WebDriver 拒绝。
我一般这样迁移:
from selenium import webdriver def inject_cookies(driver, session, domain): driver.get(f"http://{domain}") # 先打开一次页面,让 driver 处于该域下 driver.delete_all_cookies() for cookie in session.cookies: driver.add_cookie({ "name": cookie.name, "value": cookie.value, "domain": domain, "path": "/", })这里有个关键细节:必须先driver.get(url)一次,哪怕这个页面加载失败也没关系,只要浏览器地址栏在那个域名下,add_cookie才会生效。如果你在空白页about:blank状态下执行添加 cookie,会直接报InvalidCookieDomainException,这是很多新手反复踩的坑。
4.2 localStorage 的同步:比 cookie 更隐蔽的登录态
我遇到过一个某图像处理 Demo 的控制台,登录成功后不写 cookie,而是把 token 放到localStorage里,前端 axios 拦截器每次请求从localStorage.getItem("token")取。这种场景下你光加 cookie 没用,页面依然认为未登录。需要在 Selenium 里执行 JS 来写入:
token = login_response_data["data"]["token"] script = f"localStorage.setItem('token', '{token}');" driver.execute_script(script)注意 token 字符串里的引号,如果 token 本身包含特殊字符,建议用json.dumps来生成安全的 JS 字符串:
import json token = login_response_data["data"]["token"] safe_token = json.dumps(token) driver.execute_script(f"localStorage.setItem('token', {safe_token});")这一步往往是整个流程里最容易被忽略的。cookie 注入后页面还是跳回登录页,不是 cookie 坏了,是因为前端根本不看 cookie。排查的时候先打开 DevTools 看一下 Application 面板里到底存了什么。
4.3 完整流程串起来
把上面的模块串成一个可运行的流程,大致长这样:
def login_with_selenium(base_url, username, password): # Step 1: requests 发 POST 拿登录态 session, login_data = post_login(base_url, username, password) # Step 2: 启动 Selenium options = webdriver.ChromeOptions() options.add_argument("--disable-blink-features=AutomationControlled") driver = webdriver.Chrome(options=options) # Step 3: 打开目标域 driver.get(base_url) # Step 4: 注入 cookie 或 localStorage inject_cookies(driver, session, "example.com") token = login_data.get("data", {}).get("token") if token: safe_token = json.dumps(token) driver.execute_script(f"localStorage.setItem('token', {safe_token});") # Step 5: 刷新页面,验证登录态 driver.refresh() return driver这段代码的隐含假设是:登录接口的响应里有data.token字段。如果你的接口结构不同,对应的解析要改。但整体流程是通用的:requests 拿状态 → 启动浏览器 → 注入状态 → 刷新验证。任何登录态的自动化都逃不出这个框架。
5. 常见问题排查:cookie 注入失败、跨域拦截、请求被前端改写
5.1 现象:add_cookie 报 InvalidCookieDomainException
这是最常见的错误。原因是 WebDriver 的 cookie 校验机制要求:domain必须是当前页面 URL 的主机名或父域,且不能带端口。比如你页面 URL 是http://localhost:8080,那 cookie 的 domain 应该写localhost,而不是http://localhost:8080,也不能写127.0.0.1。
解决方法是先driver.get(page_url),然后从driver.current_url解析出主机名再设置:
from urllib.parse import urlparse parsed = urlparse(driver.current_url) domain = parsed.hostname driver.add_cookie({"name": "sessionid", "value": "xxx", "domain": domain, "path": "/"})5.2 现象:POST 请求发出去了,但接口返回 403 或跨域错误
如果你是在execute_script里用fetch发的 POST,跨域是必然要面对的问题。浏览器会先发一个OPTIONS预检请求,如果后端没配 CORS 允许头,请求直接在预检阶段被拦截,页面控制台里能看到报错,但 Selenium 本身不会抛异常。
解决:优先改用 requests 方案,requests 不受 CORS 限制。如果非要留在浏览器里发,那需要启动 Chrome 时加--disable-web-security参数,但这个参数对跨域处理并不彻底,而且会引入新的安全风险,我不推荐。
5.3 现象:接口返回 400,参数没问题却一直报错
这种往往不是参数本身的问题,是 Content-Type 不对。前端FormData提交和application/json提交,后端接收方式完全不同。你用 requests 的data=发表单格式,但后端期待的是 JSON,就会报参数缺失;反过来也是。建议先把接口的请求体用抓包工具看一遍,确认 Content-Type 再决定用json=还是data=。
5.4 现象:登录成功了,但 Selenium 打开页面依然跳转登录页
这是典型的 localStorage 没同步,原因见 4.2。还有一种可能:登录态有效期依赖 cookie 的过期时间,而你在 requests 层登录后,session 的 cookie 可能设置了HttpOnly,这本来是安全行为,但 Selenium 无法通过get_cookies()拿到 HttpOnly 的 cookie,只能等页面自身携带。解决办法是在 requests 层配置trust_env=True,确保 session 确实拿到了完整的 Set-Cookie 响应头,再手动解析。
5.5 现象:Chrome 被识别为自动化环境,接口风控直接拒绝
这是 Selenium 老生常谈的问题。POST 提交成功后,浏览器侧的后续请求会带上webdriver标记,敏感接口会直接拒绝。我一般会在启动参数里加两行:
options.add_argument("--disable-blink-features=AutomationControlled") options.add_experimental_option("excludeSwitches", ["enable-automation"])第一行是禁用自动化受控特征,第二行是去掉 Chrome 右上角的「Chrome 正受到自动测试软件控制」提示。加了这两行以后,大多数基于navigator.webdriver的检测能绕过。但高级风控还会检查navigator.plugins的长度、window.chrome是否存在、鼠标轨迹等,这类需要配合 stealth 插件,不在本次封装范围内。判断是否需要走这条路,看接口报错是 403 还是业务错误码即可。
6. 验证封装结果的技巧:用响应断言替代肉眼判断
封装完成以后,很多人验证的方式是看一眼页面有没有跳回登录页,这太粗糙了。我一般会让 requests 层和 Selenium 层各做一道断言,确保「POST 参数封装正确」这件事有据可依。
requests 层断言很简单:
session, data = post_login(base_url, username, password) assert data["code"] == 0, f"login failed: {data}" assert session.cookies.get("sessionid") is not None第一行验证业务状态码,第二行验证服务端下发 cookie 了。这两个断言只要有一行过不去,后面 Selenium 那边怎么折腾都没用。
Selenium 层呢,光刷新还不够,我习惯加一个「登录态探针」接口:
driver.get(base_url + "/api/user/me") body = driver.find_element("tag name", "pre").text assert "username" in body如果/api/user/me返回的是 JSON,浏览器会以纯文本形式展示,直接断言文本里的关键字段。这样比看它跳没跳转更可靠,因为有些页面登录失效后不会立刻跳转,而是先渲染一个空壳页面,然后再异步跳走。
如果页面是 SPA 并且接口响应被框架吞掉了,那就退回到driver.execute_script("fetch('/api/user/me').then(r=>r.json()).then(d=>window.__me=d)"),然后轮询window.__me是否被赋值。
我把这个技巧记成了一条习惯:从那以后,我做任何 Selenium 相关封装,都会强制走一遍「requests 层断言 → 浏览器层探针断言 → 页面跳转确认」三步检查,缺一步都不算验证通过。这一步看起来多花几秒,实际上能省掉后面大量的定位时间,希望帮到你。
本文还有配套的精品资源,点击获取