
简介面向Web自动化测试与接口调试场景资源演示了如何用Selenium封装POST参数并获取异步返回的JSON对象。通过WebDriver实例结合JavaScript的fetch请求模拟提交表单或AJAX调用适合需要扩展Selenium用途、解决异步请求返回值解析问题的开发与测试人员。该方案既可用于快速构造带参数的接口验证也能为UI自动化中需要与后端交互的用例提供思路。压缩包为rar格式共3个文件主要包含一个Java源码文件、一份说明文档及一个备份文件整体大小仅5KB结构轻量。目前已有3698人学习浏览。Java源码部分提供了具体处理逻辑配合说明文档可快速理解封装步骤代码里涉及构造JSON字符串、执行fetch、读取页面返回内容并解析等关键环节可作为二次开发的参考脚本。同时文末也提醒了在纯HTTP请求场景下专用库如requests会更高效灵活帮助读者选择合适的技术路径。1. 用Selenium封装POST参数提交先承认Selenium发不了原生POST第一次被人问到“Selenium能不能直接发POST”时我下意识想说“能页面上有表单就能提交”但问的人显然不是这个意思。他要的是已经拿到一个登录后的浏览器会话想让它像HTTP客户端那样往接口发POST数据而不是去点击、填表、等页面跳转。严格说Selenium不提供这样的能力WebDriver协议里没有“发送HTTP请求”这个原语。但围绕这个需求可以走三条路线JS注入fetch、CDP协议拦截、Selenium取状态后交给requests发送。这次拆解的是一个把这三条路线的可行部分落成可运行代码的示例工程名字就叫selenium-post-demo适合爬虫开发、自动化测试、需要带登录态POST的人。读完之后你能看到这个封装类怎么跑、踩过哪些坑、怎么改成自己的通用请求层。2. Selenium发POST的三条技术路线从JS注入到CDP协议再到混合模式2.1 为什么Selenium不提供post方法浏览器的执行模型先理清一个底层事实Selenium的核心能力是模拟用户操作浏览器它的命令集围绕“找到元素、点击、输入、读取页面状态”设计。WebDriver规范中确实没有POST这个动作因为浏览器本身就不是一个裸的HTTP客户端。页面里的AJAX请求、表单提交都是页面JavaScript通过fetch或XMLHttpRequest发出去的Selenium只是站在外面指挥浏览器并没有介入网络层的接口。所以想发POST思路只有两个方向要么“借道”页面里的JavaScript引擎用脚本调浏览器自带的网络能力要么绕过Selenium直接跟浏览器内核的调试协议对话。第三个方向更“野”既然Selenium控制着浏览器就可以让浏览器去登录、去种cookie然后把cookie交给外部的HTTP库去发请求。这三条路线对应了后面要讲的三套代码也对应了这套封装工程的三层设计。2.2 路线一execute_script注入fetch最短路径最常见的做法是用driver.execute_script在页面上下文里执行JavaScript脚本里调用浏览器原生的fetch。注意不是execute_script是execute_async_script因为fetch是异步的需要在回调里把结果送回Python端。这段代码是路线一的最小可运行版本from selenium import webdriver driver webdriver.Chrome() fetch_code const callback arguments[arguments.length - 1]; async function doPost(url, data, headers) { const resp await fetch(url, { method: POST, headers: Object.assign( {Content-Type: application/json}, headers || {} ), body: JSON.stringify(data) }); const text await resp.text(); callback(JSON.stringify({ status: resp.status, body: text })); } doPost(arguments[0], arguments[1], arguments[2]); # 先打开目标域名的任意页面保证同源 driver.get(http://127.0.0.1:8080/blank.html) result driver.execute_async_script( fetch_code, http://127.0.0.1:8080/api/echo, {name: selenium, source: fetch-inject}, {X-Trace: demo-001} ) print(result)逻辑说明execute_async_script会把脚本按顺序执行并自动把最后一个参数设成回调函数。脚本里const callback arguments[arguments.length - 1]取到的就是这个回调fetch请求结束后先把状态码和响应文本拼成JSON字符串再交给callback送回Python端。这里没有用Promise作为返回值因为WebDriver的异步脚本机制只认callback不认return。参数说明arguments[0]是目标URLarguments[1]是POST的JSON体arguments[2]是自定义请求头。脚本内部用Object.assign把默认的Content-Type和调用方传入的headers合并后者覆盖前者。resp.text()拿到原始响应文本而不是resp.json()目的是把解析工作留给Python端避免在JS里做二次编码。2.3 路线二CDP协议直接处理网络请求如果你想要的不是“发一个POST”而是“拦截浏览器里正在发出的POST并修改参数”那fetch注入就做不到了。改请求只能走CDP即Chrome DevTools Protocol。启动Chrome时加一个远程调试端口Python端通过WebSocket连接给Fetch域下发指令匹配到的请求会进入paused状态读取到postData改动后再用Fetch.continueRequest放行。依赖库是websocket-client先安装python -m pip install websocket-client下面是一个精简的拦截器只做了读取和放行import json from websocket import create_connection class CDPInterceptor: def __init__(self, page_ws_url): self.ws create_connection(page_ws_url) self.msg_id 0 def call(self, method, paramsNone): self.msg_id 1 self.ws.send(json.dumps({ id: self.msg_id, method: method, params: params or {} })) def watch_post(self): self.call(Fetch.enable, {patterns: [{ urlPattern: *, requestStage: Request }]}) while True: msg json.loads(self.ws.recv()) if msg.get(method) ! Fetch.requestPaused: continue params msg[params] request params[request] print(拦截到POSTbody, request.get(postData)) self.call(Fetch.continueRequest, { requestId: params[requestId] })逻辑说明Fetch.enable注册匹配规则urlPattern: *表示所有请求都会先暂停requestStage选择Request阶段这样停在请求发出前可以读取或改写postData。收到Fetch.requestPaused事件后打印原始body然后不修改任何内容直接continueRequest放行。真正改写时要把修改后的body通过postData参数传回去但要注意Content-Type如果原请求是JSON直接拼字符串会把JSON撑坏应该先json.loads再json.dumps。参数说明page_ws_url是浏览器的调试地址启动Chrome时用--remote-debugging-port9222然后访问http://127.0.0.1:9222/json拿页面列表取webSocketDebuggerUrl字段。CDP的id字段必须自增不能复用同一个id否则对端会认为是重复请求。2.4 路线三Selenium拿状态requests发请求第三条路线最不“黑科技”但实际项目里最常用。核心观点是POST本身不需要浏览器执行需要的是浏览器里的登录态。让Selenium去登录目标网站登录完成后用driver.get_cookies()导出cookie交给requests.Session然后直接请求毫秒级完成。import requests def post_with_requests(driver, url, payload): session requests.Session() for cookie in driver.get_cookies(): session.cookies.set( cookie[name], cookie[value], domaincookie.get(domain), pathcookie.get(path, /) ) session.headers.update({ User-Agent: driver.execute_script(return navigator.userAgent;) }) return session.post(url, jsonpayload)逻辑说明driver.get_cookies()返回的每个cookie字典里有name、value、domain、path等键session.cookies.set按相同的domain和path插入避免requests的cookie桶丢域。User-Agent直接从浏览器里取回防止服务端因为UA不一致弹出风控。这个方法的好处是请求走的是系统网络栈没有浏览器渲染开销也不会被devtools协议拖累。2.5 选型对比路线侵入性能否带完整登录态改参数自由度适用场景fetch注入低代码短能同源自动带cookie中等能改body和headers快速补发请求、接口测试CDP拦截高要管WebSocket能最高能改一切请求抓包改包、动态加参数、风控对抗requests混合低能但需要先登录高requests生态批量请求、数据采集、性能要求高表格说明fetch注入适合“我就想补一个POST不想引入额外依赖”的场景CDP适合“我拦不到包必须看到浏览器真实发出去的body”的场景requests混合适合“已经登录接下来要发几百个请求”的场景。selenium-post-demo把这三条路线拆成了三个独立模块互不依赖你自己用的时候选一个方向接进去就行。3. 实战封装selenium-post-demo的目录结构与核心类3.1 工程目录与依赖这个示例工程是单机可跑的文件不多。我把它们组织成这样selenium-post-demo/ ├── post_requester.py # 路线一fetch注入版封装类 ├── hybrid_post.py # 路线三requests cookie混合方案 ├── cdp_intercept.py # 路线二CDP拦截示例可选模块 ├── demo_run.py # 调用示例打开页面发POST打印返回 └── echo_server.py # 本地回显服务用来验证POST内容依赖只有三个selenium、requests、flask。前两个是发请求用的flask只在调试阶段起一个本地回显接口帮你确认参数有没有活着到你后端。装依赖用这一条python -m pip install selenium requests flask工程的核心思想是把“发POST”这个动作从浏览器操作里抽出来封装成一个类调用方只需要传URL、data、headers、超时时间不用关心内部是走了fetch还是走了requests。3.2 PostRequester类fetch注入版的完整实现进入post_requester.py这是这套封装里最值得细看的文件。完整核心代码import json from selenium.webdriver.chrome.webdriver import WebDriver class PostRequester: 基于 selenium fetch 注入的 POST 请求工具 def __init__(self, driver: WebDriver): self.driver driver def post(self, url: str, data: dict, headers: dict None, timeout: float 5.0) - dict: old_timeout self.driver.get_script_timeout() self.driver.set_script_timeout(timeout) fetch_code const callback arguments[arguments.length - 1]; async function doPost(url, data, headers) { const controller new AbortController(); setTimeout(() controller.abort(), arguments[3]); try { const resp await fetch(url, { method: POST, headers: Object.assign( {Content-Type: application/json}, headers || {} ), body: JSON.stringify(data), signal: controller.signal, credentials: include }); const text await resp.text(); callback(JSON.stringify({ status: resp.status, body: text, error: null })); } catch (err) { callback(JSON.stringify({ status: 0, body: null, error: String(err) })); } } doPost(arguments[0], arguments[1], arguments[2], arguments[3]); try: raw_result self.driver.execute_async_script( fetch_code, url, data, headers, int(timeout * 1000) ) return json.loads(raw_result) except Exception as e: return {status: 0, body: None, error: str(e)} finally: self.driver.set_script_timeout(old_timeout)逻辑说明post方法有两个层次的控制。第一层是Python端的set_script_timeout它保证WebDriver在timeout秒内必须等到callback否则抛异常第二层是JS端的AbortController它在同样的毫秒数后主动中断fetch请求。这两层是互相兜底的——WebDriver超时只是杀脚本无法取消已经发出去的请求所以要在脚本内部先给fetch挂上取消信号。credentials: include这一项很关键。fetch默认的credentials是same-origin跨域请求不会带cookie但实际项目里目标接口和页面往往不在一个域名不加这个参数POST到了后端也认不出登录态。加了这个参数同源跨域都会尝试携带cookie。返回值的结构固定成三个字段status、body、error。status是HTTP状态码请求没出去或被中断时是0body永远是字符串error是JS侧的异常文本。调用方判断成功与否看的应该是status 200 and status 300而不是error is None。参数说明timeout单位是秒进入方法内部转成毫秒传给JS。注意int(timeout * 1000)这一步如果传入0.5秒毫秒数是500没有精度丢失。headers是可选字典会通过Object.assign和默认头合并外层覆盖默认值。3.3 接口规范统一返回结构的意义这套封装里我做了一个取舍不管内部是fetch、CDP还是requests对外永远是同一个返回结构。字段类型含义statusintHTTP状态码0表示请求未完成bodystr/null响应体原始文本errorstr/null异常信息正常时为null这样做的直接好处是上层代码写业务时不用关心底层是浏览器发还是requests发。比如批量任务里收集失败只需要看status不需要catch异常。3.4 登录态与同源问题cookie怎么跟着走fetch注入版要注意一个前提脚本是在当前页面上下文跑的所以同源请求会自动带cookie跨域请求必须靠credentials: include。这个demo里demo_run.py被设计成先打开目标域名的空白页再发fetch保证第一跳同源from selenium import webdriver from post_requester import PostRequester driver webdriver.Chrome() driver.get(http://127.0.0.1:8080/blank.html) requester PostRequester(driver) resp requester.post( http://127.0.0.1:8080/api/echo, data{name: selenium, from: post}, headers{X-Trace: demo-001}, timeout3.0 ) print(resp[status]) print(resp[body])逻辑说明先访问一次blank.html让浏览器的同源策略不会拦截后续对127.0.0.1:8080的fetch请求。如果你要打的是线上接口就把driver.get换成那个域名的任意一个页面哪怕打不开只要状态码是DNS能解析、证书能过就行。3.5 混合方案封装HybridPosterhybrid_post.py里的封装更加工程化它把前面2.4的写法整理成一个类import requests class HybridPoster: def __init__(self, driver): self.driver driver self.session requests.Session() self._sync_cookie_and_ua() def _sync_cookie_and_ua(self): for cookie in self.driver.get_cookies(): self.session.cookies.set( cookie[name], cookie[value], domaincookie.get(domain), pathcookie.get(path, /) ) ua self.driver.execute_script(return navigator.userAgent;) self.session.headers.update({User-Agent: ua}) def post(self, url, data, headersNone): if headers: self.session.headers.update(headers) resp self.session.post(url, jsondata, timeout10) return {status: resp.status_code, body: resp.text, error: None} def refresh(self): self._sync_cookie_and_ua()逻辑说明refresh方法的存在是为了处理登录态过期。有些网站cookie半小时就失效频繁登录不现实正确姿势是每隔一段时间让Selenium重新走一遍登录流程然后调用refresh把新cookie同步给requests。同步时注意domain字段requests的cookie桶如果没有domain跨子域请求时不会携带这个细节容易出错。4. 避坑记录Selenium发POST踩过的五个坑4.1 execute_async_script永远返回空白等一场现象调用execute_async_script后Python端等了很久最后返回None响应结果拿不到。原因脚本里没有调用callback或者调用了回调但回调执行前脚本就抛了异常。还有一个常见原因set_script_timeout设置过小fetch还没回来WebDriver先超时杀了脚本。解决把callback作为脚本最后一个参数按“取到callback再发起异步操作最后callback(结果)”的顺序写。超时时间给到3到5秒不要在Python端用time.sleep硬等那会把脚本超时窗口堵死。4.2 CORS把请求拦了接口根本没收到包现象fetch注入版代码没问题但浏览器控制台报CORS policy错误后端的access log里找不到这条POST记录。原因fetch受同源策略限制。页面在A域请求打到B域的接口如果B域没有返回Access-Control-Allow-Origin响应头浏览器会在发出请求前直接拦截。注意这里的拦截发生在浏览器侧请求并没有真正到服务器。解决让页面先driver.get到目标域名的任意页面让当前页面的“源”和目标接口同域。实在无法同源时改用hybrid_post.py的requests方案requests不受浏览器同源策略限制。4.3 后端说参数是空的明明send了JSON现象POST返回200但后端拿到的body是空的或者后端解析JSON解析不出来。原因Content-Type设置成了application/json但后端期望的是application/x-www-form-urlencoded或者反过来。fetch的body如果是JSON.stringify(data)头必须是JSON如果用URLSearchParams头就是表单。混用会导致后端拿不到参数。解决在封装里增加一个formFalse参数为False时用JSON头和JSON.stringify为True时用表单头和URLSearchParamsbody json.dumps(data) if not form else new URLSearchParams(data)4.4 开了CDP的Fetch.enable整个页面像卡死一样现象启用Fetch.enable并设置urlPattern: *后页面上任何请求都暂停不发看起来像浏览器冻结了。原因这是机制问题。Fetch.enable让匹配到的请求一律先暂停等调用方continueRequest或failRequest才会继续。如果只enable不continue或者continue的请求id对不上请求就永远卡着。解决只匹配需要拦截的路径比如*api/*在finally里调用Fetch.disable防止拦截器退出后请求还被挂起。continueRequest的requestId必须来自requestPaused事件里的params.requestId不能用事件id这两个id经常被搞混。4.5 requests复用cookie依然报401现象Selenium登录成功cookie也同步过去了requests发POST还是401。原因有两层。第一层是cookie同步时丢了domainrequests在发请求时找不到匹配域第二层是服务端做了UA风控requests默认UA是python-requests一打就暴露身份。解决同步时带上domain和pathUA用driver.execute_script(return navigator.userAgent;)取回来更新到session。如果401是token过期而不是cookie问题那要从请求里看有没有Authorization头cookie同步不能解决token类的鉴权。5. 二次开发从单次POST到批量请求层的改造5.1 加超时与取消PostRequester目前只支持单次请求。批量场景里经常有一个接口拖住整个任务的情况。改造点有两个Python端把人家的超时时间收拢成一个常量JS端用AbortController实现真正的取消。注意顺序先给set_script_timeout再在fetch里挂signal否则出现“脚本等到了冻结请求JS还没取消”的中间状态。5.2 加批量与失败收集给PostRequester加一个post_batch方法传入任务列表按顺序逐个发失败的任务收集到一个独立列表外部统一补偿def post_batch(self, tasks, timeout5.0): results, failed [], [] for task in tasks: resp self.post( task[url], task[data], task.get(headers), timeout ) results.append(resp) if resp[status] 400 or resp[error]: failed.append({task: task, resp: resp}) return results, failed逻辑说明失败列表里保留原始task和响应快照这样补偿逻辑可以在拿到新cookie或修复参数后重新提交。这里的判断用status 400兜底不会把3xx跳转误判为失败。5.3 验证方法本地回显服务改完任何一行代码推荐先跑echo_server.py不要直接打线上接口。回显服务的核心逻辑是原样返回请求信息from flask import Flask, request, jsonify app Flask(__name__) app.route(/api/echo, methods[POST]) def echo(): return jsonify({ method: request.method, content_type: request.headers.get(Content-Type), headers: dict(request.headers), body: request.get_json() }) if __name__ __main__: app.run(port8080)逻辑说明把POST请求打到/api/echo看返回的content_type和body就知道封装有没有把参数发对。从这个使用习惯开始我每次动手前都先起一个本地回显服务把“能不能通”和“参数对不对”这两件事测完再对真实接口救过我好几次。这次拆完selenium-post-demo之后我甚至把这个习惯带进了所有涉及网络请求的封装里——先回显再上线。希望帮到你。本文还有配套的精品资源点击获取