免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Selenium封装POST请求全指南:两种方案与避坑实战

Selenium封装POST请求全指南:两种方案与避坑实战 简介面向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 查询串里的形如?page1size20服务端从request.query里读取。POST 的参数有三种常见形态第一种是表单格式application/x-www-form-urlencoded参数形如a1b2服务端从request.form读取第二种是 JSON 格式application/jsonbody 是一段 JSON 字符串服务端从request.json读取第三种是multipart/form-data常用于文件上传参数会按 multipart 分块编码。封装的时候你必须在代码里明确告诉 requests 或 fetch 你要用哪一种格式否则就会出现“参数传过去了但后端读不到”的诡异问题。这里做一个小表格方便对照参数形态Content-Typerequests 写法fetch 写法URL Query不需要设置params{page: 1}URL 字符串拼接Formapplication/x-www-form-urlencodeddata{a: 1}URLSearchParamsJSONapplication/jsonjson{a: 1}JSON.stringify(body)Multipartmultipart/form-datafiles{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() # 同步 cookieselenium 的 Cookie 字典结构转成 requests 需要的格式 for cookie in driver.get_cookies(): session.cookies.set( cookie[name], cookie[value], domaincookie.get(domain), pathcookie.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, timeout10): resp self.session.post(url, jsonpayload, timeouttimeout) resp.raise_for_status() return resp def post_form(self, url, data, timeout10): resp self.session.post(url, datadata, timeouttimeout) resp.raise_for_status() return resp def get(self, url, paramsNone, timeout10): resp self.session.get(url, paramsparams, timeouttimeout) 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 的默认行为不用手动设置但如果你用datajson.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, timeout15): 在浏览器页面上下文中发送 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(ffetch_post failed: status{result[status]}, text{result[text]})使用方法和方案一类似但调用的是driver.execute_async_script而不是 requestsdriver.get(https://example.com/dashboard) # 登录成功后直接在当前页面调后端接口 res fetch_post( driver, https://api.example.com/submit, {task_id: 1001, config: {retry: 3, visible: False}}, timeout15, ) 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, payloadNone, modeauto, timeout15): 统一的 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, jsonpayload, timeouttimeout) 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(ffetch 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}, modefetch, ) print(like_result) # 发评论 comment_result client.post( https://community.example.com/api/comment/add, {post_id: 2048, content: 封装之后的 POST 请求非常稳}, modefetch, ) 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(scopesession) 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}, moderequests, ) 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 匹配规则比浏览器严格有些浏览器能带上的 cookierequests 会丢弃这种情况我建议你优先抓包确认实际发出的请求头里缺少了哪个 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 里少走我走过的弯路。本文还有配套的精品资源点击获取
返回列表