
简介这份资源面向希望借助 Selenium 处理 HTTP POST 请求的自动化测试与爬虫开发者重点解决通过浏览器驱动封装异步表单提交并获取 JSON 返回值的问题。资源包共 3 个文件大小仅 5KB包含 txt 说明文档、bak 备份文件及一个 Java 源文件其中 Java 代码对应文中 Python 示例的工程化改写思路txt 文档则梳理了核心封装逻辑与常见注意点便于快速上手。当前已有 3699 人学习适合具备 Selenium 基础、需要模拟登录或接口联调场景的初中级开发者。通过阅读这份资料读者可直接获得 post 参数组装、fetch 异步请求触发、响应 JSON 解析等关键代码并了解 headless 配置、认证头携带、执行异步脚本等扩展技巧可显著减少在测试环境中自行摸索的时间。1. 用 Selenium 封装 POST 参数提交先绕过浏览器 UI再把会话交还给 HTTP接手那个后台管理模拟项目X的自动化时我被一个问题卡了好几天登录态在 Selenium 里保持得好好的但要批量提交 POST 参数给某个数据维护接口总不能每次都去硬点页面表单。表单一多就重定向重定向就丢参数最后彻底分不清是页面逻辑的锅还是脚本的锅。后来我把思路倒过来才想通Selenium 只适合管浏览器会话和页面交互POST 请求应该交给页面内 fetch、CDP 网络域或直接复用 Cookie 的 HTTP 会话去发。按场景选一种封装成统一接口参数、Header、Cookie 各归各管问题一下少了大半。这篇文章就是那套封装思路的完整记录含三套实现、参数说明和最常见的翻车现场适合正在写登录态接口脚本、表单自动化或爬虫脚手架的开发者直接抄。2. 先搞清楚 Selenium 的 POST 边界浏览器会话与 HTTP 请求是两个层面的事2.1 为什么不能用 driver.get 发 POSTHTTP 方法被浏览器 UI 锁死了很多第一次接触这个需求的人第一反应是翻 WebDriver 的文档找有没有driver.post()之类的方法。找一圈会发现没有而且不是版本不够新是协议层面就没设计这个东西。Selenium 的本质是替你操纵浏览器它发的指令都是“打开这个 URL”“点击这个元素”“执行这段脚本”地址栏导航只支持 GET这是浏览器行为不是 Selenium 的限制。就算你用driver.get()往 URL 后面拼参数那也只会触发一个 GET 请求。有些页面后端对 GET 有请求体长度限制参数一长直接截断有些接口干脆在路由上就区分方法GET 走过去就是 405。我见过有同学在 URL 里塞 JSON 字符串结果特殊字符被地址栏转义后端收到的参数完全变形。这不是玄学是 HTTP 方法在浏览器导航层根本没给你选择的余地。所以结论很直接Selenium 原生的页面交互能力再强也发不出一个自定义的 POST 请求体。要发 POST必须绕到另外的通道里让请求的“发出者”不再是浏览器导航动作而是页面脚本或独立的 HTTP 客户端。想清楚这一点后面封装才有讨论的基础。2.2 两条可行路径页面内 fetch 与 CDP 网络层拦截既然 Selenium 自己不能发 POST那就让能发的东西去发最终有三条常见通道。它们的本质差别在于“请求从哪一层发出去”以及“能不能带着当前浏览器的登录态”。通道请求发出者跨域限制自定义 Header响应读取复杂度execute_script fetch页面 JavaScript受 CORS 限制受 CORS 限制JS 返回值低CDPNetwork域注入 Header浏览器网络栈同样要过 CORS可全局注入仍需页面脚本配合中requests 复用浏览器 CookiePython 进程无跨域概念完全自由直接返回响应中第一条路径最直观让页面自己去 fetch 当前站点的接口浏览器自动带上同源 Cookie登录态天然一致。第二条路径是在第一条之上加 CDP 手段解决自定义 Header 在页面跳转后被浏览器重写的问题。第三条路径最“离经叛道”但实际工程里最常用浏览器只负责登录和拿 CookiePOST 用 requests 发速度和可控性都上了一个台阶。三条路径不是互斥的我一般会在同一套代码里三个都保留用一个 mode 参数来回切。同一站点、同源接口用 fetch登录后大量灌数据用 requests需要在页面渲染前把 Token 塞进后续所有请求时用 CDP。下面一章就按这个顺序把三套实现逐个铺开。2.3 封装前的准备确定数据格式与 Cookie 来源动手前有两件事必须先定下来不然封装到一半准会返工。第一是请求体格式。后端接口如果是表单式接收Content-Type用application/x-www-form-urlencodedbody 像a1b2这样拼如果是纯 JSON 接口Content-Type用application/jsonbody 要序列化成 JSON 字符串。两种格式在页面 fetch 里的序列化方式完全不同混着用最容易踩 415。第二是 Cookie 从哪来。Selenium 的driver.get_cookies()返回的是浏览器当前所有域的 Cookie 列表每项里有name、value、domain、path。把这份列表喂给 requests 时不能直接拼字符串要用session.cookies.set()逐条灌进去因为 requests 对 domain 和 path 的匹配规则跟浏览器并不完全一致。这条后面避坑章会专门展开。准备阶段做好这两件事封装就是流水线作业了。3. 写一个可复用的 POST 封装从 fetch 到 CDP 再到会话共享三套实现3.1 封装入口用 execute_script fetch 发同源 POST第一种实现是往页面里注入一段异步脚本用浏览器自己的 fetch 接口发 POST。这样发出的请求带着当前页面的全部上下文包括 Cookie、Origin、Referer登录态一点都不用额外处理。import json def post_with_fetch(driver, url, data, headersNone): script async function sendPost(url, data, headers) { const contentType (headers headers[Content-Type]) || application/x-www-form-urlencoded; charsetUTF-8; let body; if (contentType.includes(application/json)) { body JSON.stringify(data); } else { body new URLSearchParams(data).toString(); } const opts { method: POST, headers: Object.assign({}, headers), body: body }; const resp await fetch(url, opts); const text await resp.text(); return JSON.stringify({status: resp.status, ok: resp.ok, body: text}); } return sendPost(arguments[0], arguments[1], arguments[2]); raw driver.execute_script(script, url, data, headers) return json.loads(raw)这段代码的关键点有两个。一是execute_script的额外参数会映射到脚本里的arguments[0]到arguments[2]所以 Python 这边传 url、data、headers 过去JS 那边位置必须一一对应顺序错了会直接报类型错误。二是异步函数返回的是 PromiseSelenium 拿不到 Promise 对象本身所以脚本内部必须 await 完再返回字符串。我这里把响应包成 JSON 字符串再返回是因为execute_script对复杂 JS 对象的序列化有时会走样字符串最保险。参数上要注意data 必须是扁平 dict不能有嵌套对象。URLSearchParams只处理一层嵌套值会被序列化成[object Object]后端拿到直接解析失败。headers 里的Content-Type决定走 JSON 还是表单式分支没传就默认表单式。url 可以传绝对地址也可以传相对路径/api/save相对路径会基于当前页面域名解析跨域场景下会被 CORS 拦这一点后面避坑章细说。3.2 进阶封装走 CDP 的 Network 域把 Header 全局注入同源 fetch 有个尴尬场景接口要求带X-Signature这类自定义 Header但前端页面里根本没有这个值或者这值是后端在特定时机才动态下发的。你可以在 fetch 里硬写进 headers但一旦页面发生 302 跳转这个 header 在跳转后的新请求里就会被丢弃。这时候要上 CDPChrome DevTools Protocol通过 Selenium 的execute_cdp_cmd给整个浏览器网络层注入规则。def post_with_cdp_headers(driver, url, data, extra_headersNone): driver.execute_cdp_cmd(Network.enable, {}) driver.execute_cdp_cmd( Network.setExtraHTTPHeaders, {headers: extra_headers or {}} ) try: return post_with_fetch( driver, url, data, headers{Content-Type: application/x-www-form-urlencoded; charsetUTF-8} ) finally: driver.execute_cdp_cmd(Network.setExtraHTTPHeaders, {headers: {}})Network.setExtraHTTPHeaders设置的是浏览器网络层的额外请求头它对设置之后发起的所有请求生效包括页面导航、静态资源、fetch 请求而不仅仅是你脚本里那一个 fetch。这正是它跟普通 headers 参数的本质区别。所以函数末尾必须用finally把注入的 Header 清空否则页面后续的任何跳转都会带着X-Signature到处跑服务端可能因此拒绝正常页面请求这是典型的“越权污染”。这个方案的定位不是替代 fetch而是给 fetch 补一层全局能力。实际效果是你 fetch 里带的 header 会被 CDP 注入的 header 合并。遇到跳转时fetch 里的会丢CDP 里的会跟到下一跳。生产里我更常把它用在“登录跳转链路上需要持续带签名”的场景一次注入整条链路都干净。3.3 场景补充requests 复用 Selenium 登录态适合大 payload 和跨域前两条路径都在浏览器内部打转第三条直接把请求挪出浏览器。浏览器登录后用driver.get_cookies()拿到 Cookie 列表同步给 requests 的 Session之后所有 POST 都由 requests 发出。这个方案不受 CORS 限制、没有页面渲染开销、在大 payload 场景下速度快得多也不要担心页面脚本把 Header 重写。import requests def clone_cookies_to_session(driver, session): for c in driver.get_cookies(): session.cookies.set( c[name], c[value], domainc.get(domain), pathc.get(path, /) ) return session session requests.Session() clone_cookies_to_session(driver, session) resp session.post( https://api.example.com/internal/save, data{project: demo, status: running}, headers{X-Requested-With: XMLHttpRequest} ) print(resp.status_code, resp.text)session.cookies.set这一步是关键。driver.get_cookies()返回的 domain 通常带前导点比如.example.comrequests 会把这个当作域名后缀匹配来处理这个行为基本符合预期但要注意如果某个 Cookie 的 domain 字段为空说明它是 host-only Cookie只对该精确域名生效。这时传给domainNonerequests 会自动按当前请求域名匹配你要是硬塞一个空字符串进去requests 会拿它匹配不上任何域名Cookie 就等于没同步。我一般会在封装里保留这个函数独立暴露因为它不只是给 requests 用后续场景里 requests 的响应如果想回灌给页面展示也需要重新同步一次。它相当于浏览器会话和 Python HTTP 会话之间的桥梁所有跨通道操作都从这里过。3.4 统一接口把三种方式收进一个 Poster 类三套实现都写完后别急着复制粘贴到处用。把它们收进一个类用 mode 参数切换调用方只需要记住一个post方法。这也是封装的意义——业务代码不该关心底层走的是页面脚本还是 Python 进程。import requests class Poster: def __init__(self, driver, sessionNone): self.driver driver self.session session or requests.Session() self._sync_cookies() def _sync_cookies(self): for c in self.driver.get_cookies(): self.session.cookies.set( c[name], c[value], domainc.get(domain), pathc.get(path, /) ) def post(self, url, data, modesession, headersNone): if mode fetch: return post_with_fetch(self.driver, url, data, headers) if mode cdp: return post_with_cdp_headers(self.driver, url, data, headers) return self.session.post(url, datadata, headersheaders) def post_json(self, url, payload, headersNone): self._sync_cookies() return self.session.post(url, jsonpayload, headersheaders)参数取值行为说明modesession默认模式requests 发请求自带当前登录态 Cookiemodefetch页面内 fetch 发请求适用同源且需带上页面实时 JS 变量的场景modecdpCDP 注入 Header fetch适用跳转链路需要持续带自定义 Header 的场景headersdict透传给底层通道fetch 模式受 CORS 限制datadict表单式提交JSON 提交走post_json不建议混用这里要强调一个容易混的边界modefetch和modecdp本质都是页面内请求CDP 只是多了一层网络 Header 注入并不改变请求的发出者modesession才是真正脱离浏览器请求从 Python 进程发出响应体也是纯 HTTP 响应没有页面上下文。选错模式最典型的症状是session 模式下接口返回 200但后端记录的用户 IP 和浏览器不一致因为请求确实不是从浏览器出去的。4. 避坑记录POST 封装最容易翻车的五个现场4.1 POST 发出去没反应fetch 被 CORS 拦了现象post_with_fetch调用后Python 端不报错但返回的 body 是空串或者页面控制台抛TypeError: Failed to fetch等一会儿接口那边数据也没写进去。原因目标接口和当前页面不同源浏览器对跨域 fetch 做了 CORS 拦截。JS 的身份是“页面脚本”它只能请求同源接口或服务端明确允许的跨域地址。Object.assign里带的自定义 Header 如果不在服务端允许列表里会直接触发 preflight 请求服务端不响应预检真实 POST 根本发不出去。解决同源场景才用 fetch跨域场景直接切modesession。requests 没有同源策略Cookie 同步好后想发给哪个域名都行。如果接口同源但偶尔跨域我一般会在 Poster 里加一个自动降级fetch 返回okFalse且状态为 0 时自动用同一个 data 再发一次 session 请求重试一次的成本远低于排查半天的成本。4.2 自定义 Header 在重定向后消失了现象第一次登录请求带了X-Token服务端返回 302跳转到第二个地址后第二个请求的响应里出现签名校验失败。单独用 requests 复现却发现 requests 能成功。原因fetch 的 headers 参数作用域只是当前这一次请求。浏览器收到 302 后跳转是由浏览器发起的全新请求页面脚本没有能力给“下一个请求”延续 header。服务端在跳转后的站点校验签名自然拿不到。解决用 CDP 的Network.setExtraHTTPHeaders做全局注入就像 3.2 节那样跳转链路里每个请求都会带上。注意 CDP 注入对所有请求生效包括静态资源所以用完一定重置。另一种做法是改造接口让 token 走 CookieCookie 会跟随跳转自动带上这是后端可控场景下的首选。4.3 Cookie 没带上domain 和 path 双重过滤现象session 模式发起 POST服务端返回 401 未认证。用浏览器登录后执行同样的请求却正常。查看driver.get_cookies()列表数据都在但 requests 就是“看不到”。原因session.cookies.set时如果传了 domain 且带了前导点.example.comrequests 会把它当作域名后缀匹配如果某条 Cookie 的 domain 字段缺失而你传了空字符串它匹配不上任何 host还有 path 如果传了具体路径/adminrequests 对/api/save的请求就认为这个 Cookie 不适用直接不发送。解决同步时对 domain 做空值判断空值就不传 domain 参数让 requests 自动按请求域名匹配path 如果没有特别需求统一用c.get(path, /)只有明确限定目录域的 Cookie 才保留原 path。封装函数里这两行是踩过坑后改的最初直接session.cookies.update(driver.get_cookies())看着省事实际丢 Cookie 丢得莫名其妙。4.4 execute_script 拿不到返回值Promise 没等现象调用post_with_fetch返回 None或者抛javascript error: async *sendPost* is not a function。也有时不报错但返回一个带奇怪结构的对象。原因JS 里async function返回的是 Promise 对象execute_script是同步执行脚本它不会自动等待 Promise resolve。Selenium 拿到 Promise 后直接序列化失败。返回字符串里的{status:...}被包在 Promise 里Python 端自然解不开。解决脚本内部做完 await 再返回这就是 3.1 节写的await resp.text()的原因。如果你确实想用execute_async_script的 callback也可以但要改一套写法我建议直接在 JS 里结束异步段。判断标准很简单看 curl 里能不能复现能复现就说明 JS 脚本本身没问题问题出在 execute 的执行模式。4.5 请求体 415URLSearchParams 和 JSON 的 Content-Type 没对上现象fetch 或 session 模式请求发出后服务端返回415 Unsupported Media Type或者后端日志显示收到的 body 是空字符串。原因Content-Type声明了application/x-www-form-urlencodedbody 却是JSON.stringify出来的{a:1}反过来声明 JSONbody 却用URLSearchParams拼成了a1b2。fetch 内部我的封装会根据 Content-Type 自动选分支但如果你在外面手动构造过 body两处很容易打架。requests 的 session 模式里data参数会按表单式编码但你同时传了jsonrequests 会忽略data这也是一种隐性错位。解决Poster 里把数据格式和 Content-Type 绑死或者统一走post_json。我在类里加了专门方法post_json内部用self.session.post(url, jsonpayload)由 requests 自动设置 JSON Content-Type。业务代码只需要回答一个问题后端接口是表单式接收还是 JSON 接收不要试图让一套代码两用。5. 最后再塞一个技巧把 POST 封装接进登录流程做会话预热最后一章分享一个我现在几乎每个项目都用的习惯登录后立刻做一次“会话预热”用封装好的 Poster 把登录 Cookie 同步到 requests 会话然后批量接口请求全部走 session 模式浏览器只负责登录和首屏渲染。def warmup_and_bulk_post(driver, base_url, tasks): p Poster(driver) # 初始化时就会同步 Cookie results [] for task in tasks: resp p.post( base_url /api/batch/update, datatask[payload], modesession, headers{X-Operation: task[op]} ) results.append(resp.json()) return results这个技巧能解决一个很实际的效率问题Selenium 每执行一次页面操作都要调度浏览器开销大且不稳定但接口批量提交恰恰是高并发、高频次的活。让浏览器只做它擅长的——登录、拿 Cookie、保持会话——然后把请求交给 requests 去发速度能差一个数量级。项目里 500 条数据逐条用页面表单提交要跑十几分钟改成预热后 session 批量发几十秒收工。使用时机上要注意登录后必须先调一次_sync_cookies()再开始批量请求。登录过程如果涉及多步跳转或扫码Cookie 是逐步完善的太早同步会漏掉关键会话 Cookie。我一般会在登录页稳定后 sleep 到页面出现“欢迎登录”之类标识再调同步方法宁可多等一秒不要拿不完整的 Cookie 去撞 401。从那以后每套 Selenium POST 的脚本我都强制走一遍三条检查跨域先降级、CDP Header 用完就重置、Cookie domain 空值不硬传。这三条看着不起眼但每一条都曾让我排查到深夜。把这套封装按上面 3.4 节的类结构整理成独立文件替换掉你项目里所有裸写driver.get祈求页面自己提交的地方希望帮到你。本文还有配套的精品资源点击获取