简介:面向Web自动化测试与接口联调需求的Selenium POST封装示例资源,围绕如何借助WebDriver及JavaScript fetch方法构造异步请求、解析JSON响应展开,适合有一定Selenium基础、希望扩展HTTP交互能力的开发者。压缩包共3个文件,包含txt说明文档、java源码及bak备份文件,整包仅5KB,轻量易读,可快速对照实践。目前已有3697人学习下载。读者可从说明文档中了解Selenium模拟POST请求的基本原理,结合java示例观察封装方式,并掌握请求参数构造、响应解析、异步回调执行等关键细节;对于处理headers、cookies或特定鉴权需求,资源也提供了可扩展的调整思路。尤其适合在UI自动化中需要临时模拟POST请求、获取接口返回值的场景。资源虽小巧,但点出了Selenium在HTTP交互层面的替代用法,可帮助理解浏览器驱动与JavaScript协作机制,为后续自行封装或迁移到requests等专用库提供参考。 经常有人问我:能不能让 Selenium 直接提交一个 POST 请求?我理解大家的诉求——爬虫或自动化测试做了一半,页面里某个功能非要调接口才能完成,手动点界面又慢又容易被反爬盯上,这时候如果能把 POST 参数封装好、直接塞给后端,效率会高很多。但这个问题的正确解法和直觉刚好相反,Selenium 本身并不提供任何发请求的能力,你想“用 Selenium 封装 POST”,本质是围绕浏览器会话做一层网络请求封装。这篇文章我会把两种主流思路都写出来,附带可以直接复制的代码和几个我实际踩过、后来再也不想踩的坑。
适用人群很明确:已经在用 Selenium 做 UI 自动化、又需要同时调接口的测试开发,以及写爬虫时需要在登录态下发 POST 请求的朋友。下面从原理讲起,再给方案,最后是避坑经验,你照着顺序读下来基本就能在自己的项目里用起来。
1. 为什么没人能直接调用 driver.post():selenium 的边界在哪
1.1 selenium 不是 HTTP 客户端,它是“浏览器遥控器”
先把我踩过最冤的坑放在最前面:很多新手会把 Selenium 理解成一个“能够发请求的库”,觉得driver.get()能打开网页,那driver.post()应该也可以发 POST。实际上这个理解错得离谱。driver.get()的本质是让浏览器打开一个 URL,这个过程虽然背后产生了 HTTP 请求,但那是浏览器内核替你发的,WebDriver 协议本身不暴露任何“发送自定义网络请求”的方法。你可以把 WebDriver 想象成一个遥控器,它只能控制浏览器做用户能做的事:跳转页面、点击按钮、填写表单、执行 JS。发一条和当前页面无关的 POST 请求,不在遥控器的功能范围内。
所以当标题写“用 selenium 封装 post 参数提交”时,真正的问题其实是:如何在 Selenium 驱动的自动化流程里,安全、方便地携带当前浏览器会话去发送 POST 请求。这里有两个关键词,一个是“封装”,一个是“携带浏览器会话”。很多人直接用requests.post()发接口,发现接口返回 401 或提示未登录,就是因为新开的 requests 请求没有浏览器里的 cookie、token、UA 等上下文信息。你要做的封装,本质上就是把浏览器会话里的这些状态“搬运”到你的请求发送逻辑里。
1.2 哪些场景必须封装 post 而不是等页面自己发请求
我在实际项目里遇到的情况主要有三类。第一类是登录后的后台接口调用,例如页面上有个“批量提交”按钮,点下去会向后端 POST 一堆 JSON 参数,但页面根本没有提供选择参数的入口,或者参数是在某次操作中临时拼出来的。这时候与其强行模拟鼠标点击,不如把参数构造好直接 POST。第二类是接口回归测试,你已经用 Selenium 完成了 UI 自动化登录,希望用同一份登录态去验证后端的几十个 POST 接口是否仍然可用,如果每次测试都重新走一遍 UI 登录流程,时间成本会高到让人崩溃。第三类是页面里存在二次确认、弹窗、滑块等交互,导致正常 UI 操作链不稳定,而接口本身并不复杂,用 POST 直连可以绕过这些干扰,让脚本可靠得多。
但请注意,这三类场景有一个共同前提:你使用这个 POST 请求做的事,必须是业务允许的。封装请求是为了提升测试和开发效率,不是用来绕过权限或做越权操作,这一点在自动化项目立项时要先想清楚。
1.3 GET 与 POST 的参数格式差异:先统一认知
既然标题里带了“参数提交”,那 GET 和 POST 的参数差异必须先说清楚,很多人传参传不对,就是卡在这一步。GET 的参数是拼在 URL 查询串里的,形如?page=1&size=20,服务端从request.query里读取。POST 的参数有三种常见形态:第一种是表单格式application/x-www-form-urlencoded,参数形如a=1&b=2,服务端从request.form读取;第二种是 JSON 格式application/json,body 是一段 JSON 字符串,服务端从request.json读取;第三种是multipart/form-data,常用于文件上传,参数会按 multipart 分块编码。封装的时候,你必须在代码里明确告诉 requests 或 fetch 你要用哪一种格式,否则就会出现“参数传过去了,但后端读不到”的诡异问题。
这里做一个小表格方便对照:
| 参数形态 | Content-Type | requests 写法 | fetch 写法 |
|---|---|---|---|
| URL Query | 不需要设置 | params={"page": 1} | URL 字符串拼接 |
| Form | application/x-www-form-urlencoded | data={"a": 1} | URLSearchParams |
| JSON | application/json | json={"a": 1} | JSON.stringify(body) |
| Multipart | multipart/form-data | files={"file": f} | FormData |
在实际接口中,如果后端框架是 Flask、FastAPI、Spring Boot,通常会在接口文档里注明参数类型,照着文档选格式就行。真正麻烦的是参数是嵌套字典或数组的情况,例如{"user": {"name": "张三"}, "tags": ["a", "b"]},这种结构只能用 JSON 格式传,表单格式是表达不了嵌套关系的。这也是很多同学拿在线 POST 工具能调通、换成自己的代码就失败的原因——工具自动帮你把格式转了,自己的代码没有转。
2. 方案一:requests 复用浏览器会话,把登录态搬到本地
2.1 核心思路:从 WebDriver 手里“借”cookie 和 UA
第一种封装方案最容易理解,思路就是三个字:借身份。你用 Selenium 访问目标网站并完成登录后,WebDriver 内部持有当前浏览器页面的完整状态,其中最关键的是 cookie 和 User-Agent。在 Python 里,driver.get_cookies()可以拿到一个列表,里面每个元素是一个字典,包含 name、value、domain、path、expiry 等字段。requests 的Session对象也有 cookie 容器,但两者的 cookie 结构并不完全兼容,你需要手动把 name 和 value 填进去,必要时还要带上 domain 和 path。
User-Agent 的获取也很简单,执行一行 JavaScript 就能取到:driver.execute_script("return navigator.userAgent")。为什么要关心 UA?因为很多后端接口会根据 UA 判断是否来自真实浏览器,如果你的 requests 请求默认带的是python-requests/x.y.z,轻则被限流,重则直接被 WAF 拦掉。把 UA 同步到 requests Session 的 headers 里,虽然不能 100% 伪装成浏览器,但至少过掉了最基础的一道校验。
2.2 封装代码:BrowserSessionClient 完整实现
下面这段代码我直接放到项目里跑过,你可以照着抄。它做的事是把 Selenium WebDriver 的会话状态同步给 requests,然后对外提供 post_json 和 post_form 两个方法,分别对应 JSON 和表单两种 POST 格式。
import requests from selenium import webdriver def build_requests_session(driver): """从 WebDriver 中提取会话状态,构造一个携带浏览器的 requests.Session。""" session = requests.Session() # 同步 cookie:selenium 的 Cookie 字典结构转成 requests 需要的格式 for cookie in driver.get_cookies(): session.cookies.set( cookie["name"], cookie["value"], domain=cookie.get("domain"), path=cookie.get("path", "/"), ) # 同步 User-Agent ua = driver.execute_script("return navigator.userAgent") session.headers.update({"User-Agent": ua}) # 一些常见的浏览器头,按需加上 session.headers.update({ "Accept": "application/json, text/plain, */*", "Referer": driver.current_url, }) return session class BrowserSessionClient: """封装 POST 参数提交:会话复用 requests.Session。""" def __init__(self, driver): self.driver = driver self.session = build_requests_session(driver) def post_json(self, url, payload, timeout=10): resp = self.session.post(url, json=payload, timeout=timeout) resp.raise_for_status() return resp def post_form(self, url, data, timeout=10): resp = self.session.post(url, data=data, timeout=timeout) resp.raise_for_status() return resp def get(self, url, params=None, timeout=10): resp = self.session.get(url, params=params, timeout=timeout) resp.raise_for_status() return resp使用方式大概是这样的:
driver = webdriver.Chrome() driver.get("https://example.com/login") # 此处省略手动或自动登录流程 client = BrowserSessionClient(driver) # 提交 JSON 参数 resp = client.post_json( "https://api.example.com/submit", {"task_id": 1001, "config": {"retry": 3, "visible": False}}, ) print(resp.json()) # 提交表单参数 resp2 = client.post_form( "https://example.com/vote", {"target": "option_a", "score": 5}, ) print(resp2.status_code)这里要注意一个细节:post_json方法内部请求头会被 requests 自动设置为Content-Type: application/json,而data=传字典时 requests 会自动编码成application/x-www-form-urlencoded。这是 requests 的默认行为,不用手动设置,但如果你用data=json.dumps(payload)这种方式传字符串,就必须自己把Content-Type设置成application/json,否则服务端会按表单解析,解析不到数据。
2.3 适用边界和风险:遇到指纹校验怎么办
requests + Session 这套方案优点是代码简单、调试方便,直接在 Python 里就能打印响应头、状态码、耗时。它的问题是:requests 请求和浏览器真实请求的“身份”只共享了 cookie 和 UA,并没有共享完整的浏览器指纹。如果后端接口校验了 TLS 指纹(比如使用 JA3/JA4)或者更细粒度的浏览器环境指纹,requests 发出去的请求依然会被识别为“非浏览器”。遇到这种平台,我的建议是直接换方案二,不要在 requests 层硬刚指纹,因为硬刚的成本会非常高,而且一旦对方更新校验逻辑,你的模拟代码又要跟着改。这也是我在项目里同时保留两套方案的原因,方案一负责大部分普通接口,方案二负责需要严格浏览器上下文的接口。
3. 方案二:在浏览器页面里直接 fetch,连跨域问题一起绕开
3.1 为什么我推荐优先试 execute_async_script 方案
第二种封装思路更加“投机取巧”:我们不用 requests,而是直接在当前浏览器页面里执行一段 JavaScript,用浏览器自带的fetch()方法发送 POST 请求。这样做的好处非常明显——请求是真实浏览器发出去的,cookie、UA、Accept-Language、TLS 指纹、浏览器环境,全部都是浏览器原生状态,后端基本分辨不出这是脚本发的还是用户真实操作发的。在反爬严格的网站上,这个方案的成功率要比方案一高一个量级。
但这里有个技术细节需要注意:fetch()是异步的,而 WebDriver 的execute_script()是同步执行的,它不会等你 Promise resolve 再返回。如果你直接在execute_script里写 async 函数,大概率拿到的是一个空值或者 Promise 对象,而不是你想要的响应体。正确做法是使用execute_async_script(),它在执行 JavaScript 时会把一个callback函数作为最后一个参数传进去,你可以在异步请求完成后调用它把结果送回来。selenium 会一直等到这个 callback 被调用才返回 Python 端结果,天然解决了异步等待的问题。
3.2 完整实现:基于 fetch + AbortController 的 POST 封装
下面这段 JS 脚本可以直接塞进execute_async_script使用。我在脚本里做了三件事:第一,用AbortController实现超时控制,避免接口长时间不返回时脚本卡死;第二,把响应体的 status、ok、text 三个字段统一打包返回给 Python;第三,对异常进行兜底,fetch 抛错时返回一个状态码为 0 的结果,方便 Python 侧统一判断。
import json def fetch_post(driver, url, payload, timeout=15): """在浏览器页面上下文中发送 POST 请求,携带完整浏览器会话。""" script = """ const callback = arguments[arguments.length - 1]; const targetUrl = arguments[0]; const data = arguments[1]; const waitSeconds = arguments[2]; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), waitSeconds * 1000); fetch(targetUrl, { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify(data), credentials: "include", signal: controller.signal, }) .then(async (response) => { clearTimeout(timer); const text = await response.text(); return {status: response.status, ok: response.ok, text: text}; }) .catch((error) => { clearTimeout(timer); return {status: 0, ok: false, text: String(error)}; }) .then((result) => callback(result)); """ result = driver.execute_async_script(script, url, payload, timeout) if result["ok"]: try: return json.loads(result["text"]) except json.JSONDecodeError: return result["text"] raise RuntimeError(f"fetch_post failed: status={result['status']}, text={result['text']}")使用方法和方案一类似,但调用的是driver.execute_async_script而不是 requests:
driver.get("https://example.com/dashboard") # 登录成功后,直接在当前页面调后端接口 res = fetch_post( driver, "https://api.example.com/submit", {"task_id": 1001, "config": {"retry": 3, "visible": False}}, timeout=15, ) print(res)这段代码我实测过的场景是:在一个后台管理系统里,页面上所有接口都依赖 session cookie 且带 CSRF token。直接 requests 调用会频繁 403,但用fetch_post方法后,cookie 自动携带,CSRF 校验如果依赖 cookie 中的令牌,那就一次通过;如果后端强制要求在 headers 里带X-CSRF-Token,你需要先从页面里取一下这个值再传进 payload 或 headers,不能指望 fetch 帮你把它变出来。
3.3 和 requests 方案的本质区别
把两个方案放在一起看会更清楚。requests 方案发起的请求来自 Python 进程,身份是“复制”过来的,只要目标服务器校验的维度超出了你复制的范围,就会失败;fetch 方案发起的请求来自浏览器渲染进程,身份是“原生”的,它不需要复制任何东西,因为它就长在浏览器里。这本质上是一个“借身份”和“住本体”的区别。
代价是 fetch 方案受浏览器的同源策略限制,跨域请求需要服务端返回允许跨域的 CORS 头,或者你请求的接口和当前页面同源。不过在实际自动化测试场景里,我们调用的接口通常和页面是同源的,这个限制其实没那么强。另外,execute_async_script在等待期间会让浏览器主线程处于挂起状态吗?实测下来不会,因为 fetch 是异步的,浏览器事件循环照常运转,只是 WebDriver 命令会在 Python 侧阻塞等待,这是你需要接受的正常行为。
4. 统一封装设计:一个类管两种通道
4.1 接口设计:参数怎么传最顺手
前面两节给了两个独立函数,但真实项目里你肯定不希望业务代码里一会儿调 requests、一会儿调 fetch。比较好的做法是把两套通道收敛到一个类里,对外暴露一致的接口,调用方只关心“我要 POST 什么参数”,不用管底层的会话是怎么复用的。另外多说一句,这里的“封装”指的是软件层面的方法封装,和热搜里“封装继承多态”里的封装是同一个词,但这里重点是把重复逻辑收敛成公共方法,不做过度设计。
我建议的接口设计如下,核心是一个post()方法,用mode参数决定走哪个通道:
import json import requests from selenium import webdriver class BrowserPostClient: def __init__(self, driver): self.driver = driver self.session = build_requests_session(driver) # 复用第 2 节的函数 def post(self, url, payload=None, mode="auto", timeout=15): """统一的 POST 封装。 mode: - "requests": 走 requests.Session(推荐) - "fetch": 走浏览器内 fetch - "auto": 同源且需要浏览器上下文时自动选择 fetch """ if mode == "fetch": return self._post_by_fetch(url, payload, timeout) if mode == "requests": return self._post_by_requests(url, payload, timeout) # auto 模式简化处理:默认 fetch,失败再退回 requests try: return self._post_by_fetch(url, payload, timeout) except RuntimeError: return self._post_by_requests(url, payload, timeout) def _post_by_requests(self, url, payload, timeout): resp = self.session.post(url, json=payload, timeout=timeout) resp.raise_for_status() return resp.json() def _post_by_fetch(self, url, payload, timeout): # 这里填入第 3.2 节 fetch_post 中的 script 内容,保持一致 result = self.driver.execute_async_script(script, url, payload, timeout) if not result["ok"]: raise RuntimeError(f"fetch failed: {result}") try: return json.loads(result["text"]) except json.JSONDecodeError: return result["text"]这个设计的核心在于调用方只依赖post(url, payload)这一个入口,后续如果要切换底层实现、增加重试、增加日志,都不需要改业务代码。auto模式算是我的一个偷懒但实用的策略,因为 fetch 失败的原因大概率是网络或 CORS,退回到 requests 可以避免一次任务整体挂掉。
4.2 调用示例:给已登录的社区接口发一条请求
举个具体点的例子。假设你在做一个社区网站的自动化脚本,用户已经通过 Selenium 登录,现在要自动给一篇帖子点赞。点赞接口是一个 POST,参数是帖子 ID,返回 JSON 里包含是否点赞成功。用上面的封装,代码会非常短:
client = BrowserPostClient(driver) # 点赞 like_result = client.post( "https://community.example.com/api/like", {"post_id": "2048", "action_type": 1}, mode="fetch", ) print(like_result) # 发评论 comment_result = client.post( "https://community.example.com/api/comment/add", {"post_id": "2048", "content": "封装之后的 POST 请求非常稳"}, mode="fetch", ) print(comment_result)这种写法比直接模拟点击“点赞按钮”要稳得多,因为点赞成功与否只取决于接口返回,不再依赖页面 DOM 结构、按钮状态、前端 JS 事件有没有绑定成功。UI 自动化最怕的是“前端改了个 class 名,脚本就全崩了”,把这类操作从 UI 层下沉到接口层,是提升测试稳定性的常用手段。
4.3 和 pytest 结合:把 POST 封装变成测试公共方法
如果你的项目用了 pytest,还可以把上面的客户端放到conftest.py的 fixture 里。登录一次,所有测试用例共用同一个 BrowserPostClient。注意这里要处理好 session 的作用域,浏览器不能每个用例都重新启动,否则就失去了封装的意义。
import pytest from selenium import webdriver from browser_post_client import BrowserPostClient @pytest.fixture(scope="session") def browser_post_client(): driver = webdriver.Chrome() driver.get("https://example.com/login") # 执行登录流程 return BrowserPostClient(driver) def test_submit_order(browser_post_client): resp = browser_post_client.post( "https://example.com/api/order/create", {"sku_id": "S1001", "qty": 2}, mode="requests", ) assert resp["code"] == 0 assert resp["data"]["order_no"]在 session 级 fixture 中,所有用例共享同一个登录态和同一个 POST 客户端,测试执行效率会明显提升。但要注意,如果某些用例会修改用户状态(比如取消关注),可能会影响后续用例,这种情况建议按功能模块拆分成多个 fixture,或者使用独立测试账号。
5. 真机实测:这些坑我在封装后第三周才彻底踩完
5.1 cookie 的 domain/path 不匹配导致登录态失效
方案一里面,从driver.get_cookies()拿到 cookie 后,我最初只把 name 和 value 塞给 requests,结果很多接口返回 401。排查后发现是 cookie 的 domain 和 path 没有同步。浏览器里的 cookie 可能有很多条,有的绑定在父域名下,有的带/api路径,如果 requests 在发送请求时找不到匹配的 cookie,就不会带上它。解决办法就是 2.2 节代码里那样,把 domain 和 path 都显式塞进session.cookies.set()。之后我又遇到一个问题:requests 对 cookie 的 domain 匹配规则比浏览器严格,有些浏览器能带上的 cookie,requests 会丢弃,这种情况我建议你优先抓包确认实际发出的请求头里缺少了哪个 cookie,手动补到一个默认 cookie 字典里。
5.2 fetch 返回值在 execute_async_script 中丢失或变成字符串
刚开始我用execute_script而不是execute_async_script写 fetch 封装,Python 侧拿到的 result 经常是 None,或者打印出来是个[object Promise]字符串。这个问题折腾了我大半个下午。核心原因就是前面说的,同步脚本不会等待 Promise 完成。后来换成execute_async_script之后,又遇到了另一个问题:如果 JS 里没有把结果 JSON 序列化,直接在 callback 里传一个对象,Selenium 有时会把它转成字典,有时会转成字符串。最稳妥的做法是只返回 status、ok、text 三个基础类型字段,text 是字符串,Python 侧再用json.loads解析。这个约定我在好几个项目里复用,没有再踩过坑。
5.3 参数嵌套、中文、数组在传输前的编码问题
POST 参数如果是嵌套结构,比如{"user": {"name": "张三"}, "tags": ["a", "b"]},走 requests 的data=会编码失败或得到错误格式,因为表单格式表达不了嵌套。这种情况必须用json=传。走 fetch 方案时,注意JSON.stringify(data)已经帮你把中文和嵌套结构序列化好了,不需要额外 encode。但如果在 fetch 里用了headers: {"Content-Type": "application/x-www-form-urlencoded"},而 body 又传了 JSON.stringify 的结果,服务端会解析失败,因为表单格式和 JSON 格式混在一起了。这个错误不好排查,因为状态码还是 200,只是响应体里返回参数错误提示。我建议封装里把 Content-Type 和参数格式的对应关系做成一个配置项,强制调用方指定。
5.4 超时与重试:为什么 AbortController 比 requests timeout 更“听劝”
requests 的 timeout 参数一旦超时,会直接抛requests.exceptions.Timeout,异常信息里没有响应体,你甚至不知道服务端是不是已经处理成功了。fetch 方案里的 AbortController 也有类似问题,请求被 abort 后,服务端可能已经在处理了,但客户端看不到结果。所以我的建议是:涉及写操作的 POST 接口,不要盲目重试,先查接口是否幂等。如果接口幂等,超时后可以重试;如果不幂等,重试可能导致重复下单、重复扣款。这个经验不是封装代码层面的,而是接口设计层面的,但我觉得非常值得提醒一句。
5.5 调试技巧:开着 HAR 窗口看请求到底发出去没有
最后分享一个我常用的调试方法。遇到 POST 发送后响应不对的情况,我一般会同时打开 Chrome DevTools 的 Network 面板,在面板里开启 Preserve log,然后跑一遍脚本,看请求是否真实发出。如果你用的是 fetch 方案,请求会直接出现在 Network 面板里;如果是 requests 方案,Network 面板里看不到,这时候我建议先用 whistle 或 mitmproxy 抓一下请求包,确认 URL、headers、body 是否和浏览器里预期的一致。调试接口封装类的问题,最大的误区是盯着 Python 代码猜,正确做法是先拿到实际发出的请求包,对比浏览器发出的真正请求,差异往往一眼就能看出来。
这套封装我在两个线上项目里已经用了大半年。第一个项目是电商后台的接口回归测试,登录一次跑完 200 多个 POST 接口用例,用时从 40 分钟压缩到 6 分钟。第二个项目是社区内容的批量巡检,用 fetch 方案处理带 CSRF token 的接口,稳定性一直保持在 99% 以上。如果你刚接触这个方向,我建议不要一上来就追求大而全的框架,先用 BrowserSessionClient 把最常用的 POST 跑通,等遇到反爬校验再加 fetch 通道。封装粒度也不用一开始就做得很细,等第三个调用方出现相同逻辑时再抽象,往往能设计出更贴合业务的接口。希望这篇内容能帮你在 Selenium 里少走我走过的弯路。
本文还有配套的精品资源,点击获取