
干这行越久我越发觉得一个规律很多看起来“挺高级”的任务拆开之后都是纸老虎。就拿“根据接口文档写GET和POST请求脚本”这件事来说说穿了就是三张表——URL参数表、Header参数表、返回参数表。我见过太多人卡在第一步拿着文档不知道重点看哪几行或者文档看懂了落到代码里又不知道怎么拼参数、怎么处理那些文档里没写的暗坑。这篇东西我打算写给两类人看一是刚入门想做接口联调、写自动化脚本的新人二是被领导甩了一句“照着文档把接口调通”就赶鸭子上架的半路出家人。你不需要精通TCP/IP也不用把HTTP协议背得滚瓜烂熟只要会一点Python或者会敲命令行跟着我的思路走拿着文档就能写出能跑、能上线、不至于三天两头出问题的GET和POST请求脚本。下面我会用Python为主、curl为辅来演示因为这两样在几乎所有环境里都能直接跑。1. 先把接口文档拆明白五个必看字段我复盘过自己踩过的所有坑结论是脚本调不通八成不是代码问题而是文档没看全。一份接口文档再乱再长核心信息也就五块——请求URL、请求方法、请求头、请求参数、返回结果。把这五块信息一一对应着填进脚本事情就完成了一大半。1.1 请求URL和请求方法先圈出来再动手合格一点的接口文档每个接口都会给一个类似这样的说明GET /api/v1/users 获取用户列表POST /api/v1/users 创建用户POST /api/v1/users/status 批量变更用户状态我的习惯是拿到文档第一件事先把Method和URL用高亮标出来后面写的每一个代码片段都围着这两个字段转。别觉得这是小题大做我实际见过不少人把POST接口当成GET来调结果返回一个“405 Method Not Allowed”然后在签名、鉴权、Header里翻来翻去找原因折腾半天才发现是最基础的地方错了。再说说GET和POST的区别。对写脚本的人来说最关键的区别不在语义而在参数放哪GET的参数拼在URL后面用问号开始、连接长得像/api/v1/users?page1size20POST的参数放在请求体body里有JSON、表单、multipart等几种形态。理解到这一层就足够动手了协议层的细枝末节可以在遇到具体问题时再补。千万别自己发挥“这个接口用GET还是POST都行”文档怎么写你就怎么调很多系统在网关层就做了方法校验改动词法哪怕参数完全一样请求照样进不去。1.2 Content-TypePOST脚本里最容易写错的地方Content-Type这个请求头GET基本用不上但POST一写错服务器要么直接返回“415 Unsupported Media Type”要么明明收到body却解析不出任何参数。常见的一共就三种application/jsonbody是一段JSON文本现在的新接口绝大多数是这种application/x-www-form-urlencodedbody是键值对长得像a1b2老系统、老接口很常见multipart/form-data传文件的时候用表单里可以混着普通字段和文件域判断文档要求哪种最快的方法是看“请求示例”那一节它给你展示的是缩进的JSON结构还是fieldvalue的串还是带文件路径的表单示意。如果文档连Content-Type都没写我一般默认按JSON先试调通了万事大吉调不通再翻历史报文或者问对接方要示例。这个默认决策帮我省掉了很多纠结时间。1.3 Header、鉴权和返回结构决定脚本能不能活过第一轮很多接口不是裸奔的文档里会有一个“请求头”或者“公共参数”章节常见的字段有Authorization、X-Auth-Token、timestamp、nonce等。这里必须提醒一个坑鉴权Header的值很多不是写死的常量而是要求脚本每次动态算出来。比如文档给你一个“签名算法”让你把appSecret、timestamp、随机串拼起来做MD5写成header传过去。这时候就必须把“看文档”变成“翻译文档”把算法描述翻译成真实代码。返回结构同样要提前看。绝大多数接口会返回一个统一包装类似{ code: 0, message: success, data: { id: 1001 } }你的脚本第一步应该是校验code而不是直接去解析data。我把这步叫做“成功校验”所有正式脚本都必须有这一环code不对就把message和原始返回完整打出来排查成本直接降一半。没有这个习惯的人经常出现“脚本跑完了但业务没生效”的情况因为HTTP状态码是200业务code却是失败他压根没看body。2. GET请求脚本从冒烟验证到能直接用的版本GET脚本是所有接口脚本里最基础的一种。它逻辑简单但细节一点也不少尤其是URL编码、超时、分页这几件事处理不好照样翻车。2.1 先别写代码用curl做一次冒烟验证我的开发流程从来都是拿到文档先用curl把请求打一遍让接口先“响”起来。这步叫冒烟验证目的只有一个——确认地址、参数、鉴权这些大方向没跑偏。curl的好处是零依赖一条命令就能测改起来也没心理负担。curl -G http://api.example.com/api/v1/users \ --data-urlencode page1 \ --data-urlencode size20 \ -H Authorization: Bearer xxxxxx \ -w \nHTTP状态码: %{http_code}\n注意我用了-G和--data-urlencode而不是把参数直接拼在URL里。原因非常实际参数里一旦带了中文、空格、加号、这些特殊字符直接拼URL很容易被服务器解析错。--data-urlencode会自动做URL编码把中文和特殊字符转成%XX形式这个细节能排掉一大批GET请求的诡异问题。curl通了再把这个请求等价翻译成Python脚本顺序千万别反过来——反了的话你会在“代码写错了”和“接口认知错了”之间来回折腾调试效率极低。2.2 用Python requests写一个可用的GET脚本用Python写GET请求核心就是requests库的params参数。它相比手动拼URL最大的优势同样是自动处理URL编码你只管按字典传值。import requests url http://api.example.com/api/v1/users headers { Authorization: Bearer xxxxxx, Content-Type: application/json } params { page: 1, size: 20, keyword: 接口文档 测试 } resp requests.get(url, paramsparams, headersheaders, timeout10) print(resp.status_code) print(resp.text)这套代码里我养成了几个固定习惯每个都是踩过坑才定下来的。第一个timeout必须写。不写timeout的requests请求在接口假死时会一直傻等后面每行代码都像被按了暂停键。第二个先打印resp.text别急着resp.json()。先看原始文本能立刻区分“接口返回不是合法JSON”和“接口返回了JSON但解析报错”这两种完全不同的情况。第三个响应状态码和业务code分开看2xx只代表“HTTP层通了”不代表业务成功。2.3 分页和循环列表接口别只取第一页列表型GET接口基本都有分页参数文档里叫page/size也好pageNum/pageSize也好意思都一样。我见过不少同学取回第一页就宣布“数据拿到了”直到做数据核对时才发现漏了九成。正确做法是写一个循环逐页把数据拼起来。像Kettle这类ETL工具在“调用GET接口分页抽取数据”时也是这个思路无非是循环里多维护一个页码变量。all_data [] page 1 while True: resp requests.get( url, params{page: page, size: 100}, headersheaders, timeout10 ).json() if resp.get(code) ! 0: print(接口错误:, resp.get(message)) break data resp.get(data, {}) items data.get(list, []) all_data.extend(items) total data.get(total, 0) if page * 100 total: break page 1 print(累计拉取:, len(all_data))写这种分页循环时我还会加一个兜底判断如果接口连续返回空列表超过N次直接跳出循环防止因为返回结构变化导致死循环。另外循环里对每次请求的结果做一次code校验别假设“第一页能通后面每一页都能通”接口在翻页过程中突然报错是常事。3. POST请求脚本body是灵魂格式差一点都不行POST脚本比GET多了一个“构造body”的步骤这也是新手翻车重灾区。我把常见的三种body形态分开讲大家直接对着文档类型抄作业就行。3.1 JSON格式的POST省心但要注意两点现在的新接口绝大多数是JSON请求体。这种在requests里非常简单直接传一个字典给json参数import requests url http://api.example.com/api/v1/users payload { name: 张三, age: 28, tags: [vip, new], address: { city: 北京, street: xx路 } } resp requests.post(url, jsonpayload, headers{Authorization: Bearer xxxxxx}, timeout10) print(resp.status_code, resp.text)这里有两个关键点要说透。第一用了json这个参数后requests会自动把字典序列化成JSON字符串并在Header里自动带上Content-Type: application/json。如果你在headers里手动写了Content-Type又传了json两边容易打架可能造成重复头或者类型不匹配。我的规矩是JSON请求一律用json让requests自己管。第二面对嵌套结构——比如文档里的address是对象、tags是数组——直接用Python字典写就行序列化时requests会处理完全不用手动拼JSON字符串。我特别不建议把body写成一个长字符串再传一旦里面某个引号、空格跟文档对不齐签名校验和参数解析都会出问题而且极难排查。3.2 表单和multipart老接口和文件上传场景遇到老接口Content-Type是application/x-www-form-urlencodedrequests里要用data而不是jsonpayload { username: admin, password: 123456 } resp requests.post(url, datapayload, timeout10)传data时requests会把字典自动编码成usernameadminpassword123456这种键值对串。而multipart/form-data是另一回事它需要传文件或者混合字段要用files参数files { file: (report.xlsx, open(report.xlsx, rb), application/vnd.ms-excel), note: (None, 这是备注) } resp requests.post(url, filesfiles, timeout30)这种格式我在实际项目里踩过最大的坑是老系统的接口文档经常不写Content-Type只留一个“请求示例”。这时候就得靠眼睛判断——示例是a1b2就用data示例里带着文件路径或者文件说明就用files示例是一段缩进整齐的JSON就用json。判断准确率能做到九成以上剩下的靠试一次不行换另一种不要在一个方案上死磕。3.3 批量提交和并发提高效率但别把接口打挂了业务场景经常是“批量提交”——一次要处理1000个手机号、100个订单每个参数还不一样。新手最容易写成一个for循环单线程跑1000个请求跑下来要十几分钟慢得让人怀疑人生。我在对接短信批量发送这类场景时一般用Python的线程池或者干脆用JMeter做并发验证。Python方案如下from concurrent.futures import ThreadPoolExecutor, as_completed def send_one(item): resp requests.post( url, json{mobile: item[mobile], content: item[content]}, timeout10 ) return item[id], resp.status_code, resp.text with ThreadPoolExecutor(max_workers10) as executor: futures [executor.submit(send_one, item) for item in items] for future in as_completed(futures): print(future.result())如果你用JMeter思路也完全一致线程组里设置10个线程每个线程从CSV数据文件里读不同的参数就能模拟“十个参数不同的POST请求并发打过去”。但不管用哪种方式有两个底线必须守住。第一并发数别一味贪大很多接口文档会写明QPS限制没有写的我习惯先按目标上限的50%跑观察响应时间和错误率再慢慢加。第二重试一定要加退避策略失败后等一两秒再重发别一失败就立刻原样再打一遍——接口本来就已经扛不住了你再加速重试等于火上浇油很容易把整个服务搞雪崩。4. 实战复盘照着短信平台接口文档做完一个发送脚本理论讲半天不如完整跑一遍。我拿一个真实常见场景来串对接一个短信平台的HTTP接口类似云MAS平台那种HTTP短信接口文档要求用POST提交手机号和短信内容并且每个请求都要带签名。我把从读文档到跑通的全过程拆给大家看。4.1 先把文档信息整理成一张“接口要素表”我的习惯是动手前先把文档里的关键信息抄下来整理成一张表。短信接口文档的核心内容通常长这样项目内容请求地址http://api.example.com/sms/send请求方法POSTContent-Typeapplication/json请求参数appId、mobile、content、timestamp、sign返回结构{code:0,message:success,data:{msgId:xxx}}签名算法sign MD5(appSecret timestamp content) 转大写这张表就是我写代码的全部依据。后面出了问题我只在这个表里找原因不东猜西猜。大家可以把这个方法用到任何接口上先做信息整理再写代码。字段一多“照表编码”比“看着文档漫天想”可靠得多也方便你发现文档里自相矛盾的地方。4.2 签名鉴权怎么实现短信接口最常见的鉴权方式是MD5签名把appSecret、timestamp、content拼成一个字符串做MD5再转大写。原理不复杂难在拼接顺序、字段大小写和编码必须和文档严丝合缝。import hashlib import time import requests app_id 你的appId app_secret 你的appSecret mobile 13800000000 content 你的验证码是123456请在5分钟内使用。 timestamp str(int(time.time())) raw_string app_secret timestamp content sign hashlib.md5(raw_string.encode(utf-8)).hexdigest().upper() payload { appId: app_id, mobile: mobile, content: content, timestamp: timestamp, sign: sign } resp requests.post(http://api.example.com/sms/send, jsonpayload, timeout10) print(resp.status_code, resp.text)这里有一个非常容易踩的坑MD5的输入字符串用什么编码。Python3里字符串是Unicode对象必须先encode再算MD5我统一用UTF-8。如果你从文件、数据库或命令行读参数一定要确保它们也被转成UTF-8别在中间混进GBK。很多短信接口中文乱码、签名验证失败最后查下来都是编码问题——你算出的签名和服务器算出的签名用的是两种编码方式结果当然对不上。4.3 运行脚本并校验返回结果脚本第一次跑我通常会遇到两类结果。第一类HTTP返回200但业务code不是0比如“1001余额不足”“1002内容含敏感词”。第二类直接连接超时、连接失败。所以脚本必须把HTTP状态码和业务code分开打印不要只盯着一个看。我在调试期习惯把返回的原始文本完整打出来在日志里留上原文确认跑通没问题之后再改精简模式。这个习惯能让你在被接口坑的时候少死很多脑细胞。我还建议大家给脚本加一层简单的工程化包装用logging记录请求参数和返回结果、失败时分等级重试、把成功和失败的手机号分别写成文件或入库。别小看这一步口头上“脚本跑通了”和“脚本可以上生产”是两回事。比如“设备老化测试全自动执行脚本”这类需求本质上也是先跑通单个请求再套上调度、日志、异常处理的外壳。没有日志的自动化脚本一旦在凌晨的定时任务里挂掉第二天你连它为什么挂都无从查起。4.4 现场复盘一次真实调试过程我拿一次真实调试经历收个尾。当时文档给的签名算法是“appSecret mobile content”拼串MD5我按这个写完第一次跑返回“签名错误”。我先核对了一遍拼串顺序没问题接着打印timestamp发现是动态生成的也没问题最后把原始请求体完整打出来才看到content里有中文而文档示例里的中文内容用的是英文引号风格。我把所有字符串统一走UTF-8编码后再次运行就通了。整个排查过程不到十分钟靠的就是“先打原始报文再逐项对照要素表”而不是拍脑袋瞎试。5. 常见问题与排查技巧实录这章是我整理的一份“踩坑速查表”每一条都是真实遇到且反复被问过的。你看到类似报错可以直接对照查。5.1 接口返回“签名错误”或“参数错误”这两个错误是POST脚本最常遇见的。签名错误的排查顺序是这样的第一核对拼接串和文档是否一致包括字段顺序、分隔符、大小写差一个字母都是天壤之别第二核对timestamp是不是动态生成的手动写死的签名过了有效期照样失败第三核对编码中文内容务必UTF-8GBK和UTF-8算出来的MD5完全不同第四核对请求体里有没有多出来的空格或换行有些语言序列化JSON时会自动带缩进拼到签名串里也会改变结果。参数错误的排查顺序第一参数名是否和文档完全一致包括大小写和一下划线的位置第二参数类型对不对文档要求Integer你传了字符串部分严格接口会直接拒绝第三是否有必填参数漏传了有些必填项藏在文档的“备注”或“注意”里眼睛不仔细就会漏。记住一个原则先看原始请求体和文档的差异别先去怀疑接口有bug。接口大概率是对的你的请求大概率才是错的。5.2 中文乱码、编码不对国内环境里编码问题出现频率极高。GET请求参数带中文必须做URL编码requests的params参数会自动处理但如果你是在手动拼URL别忘了先用URL编码工具处理中文。POST参数中文乱码绝大多数是编码不一致导致的统一UTF-8能解决95%的问题。再有就是响应返回乱码requests可以通过resp.encoding utf-8或resp.content.decode(utf-8, errorsignore)来指定解码方式。短信内容、用户昵称这类含中文的字段最容易在这种地方翻车处理前先明确系统全链路是什么编码。5.3 请求超时、连接失败、请求“假死”超时问题的根子就是前面反复强调的timeout必须写而且要结合业务场景设置合理的值。连接失败类的报错本质上是“你的机器到目标服务器之间的链路不通”常见原因包括对方接口只允许特定网段访问你的IP不在白名单本机防火墙拦了出站连接DNS解析不到域名目标服务端口根本没对外开放。排查时先用ping测网络通断再用telnet或nc测端口连通性把问题定位在“网络层”还是“应用层”再往下一步处理。顺带说一句你有时候会看到类似error response from daemon: get https://registry-1.docker.io/v2/: net/http这样的报错这是本机访问外部服务时网络链路不通的典型表现和我们脚本里遇到的连接失败本质上是一回事——都是机器到目标地址的通路出了问题。排查思路也一样从链路连通性开始逐层查别一上来就怀疑代码。5.4 命令找不到、脚本闪退、环境变量问题这类问题虽然不是接口本身的问题但遇到的人特别多。Windows上运行python提示“不是内部或外部命令”大概率是Python没加入PATH类似“pnpm无法识别”“claude无法识别”这类提示都是可执行文件所在目录没有加进环境变量。解决办法是重装并勾选Add to PATH或者手动把安装目录写进系统环境变量。Linux环境下shell脚本写了if判断和for循环但运行报错大多数是文件没有执行权限chmod x script.sh就好。Windows下双击bat脚本窗口一闪而过多半是脚本里某个命令执行报错在文件末尾加一行pause或者用命令行窗口运行把错误输出留下来再排查。5.5 Postman和JMeter的正确用法很多人习惯先用Postman把接口调通再写脚本。这个思路我非常支持Postman的Code功能可以直接生成Python requests代码省掉不少手写功夫。但这里必须泼一盆冷水Postman生成出来的代码只适合当“脚手架”它通常不包含timeout、重试、错误处理这些工程化内容直接搬去生产环境早晚出事。JMeter同理用来做压测和并发验证非常顺手比如设置线程数去模拟不同参数的POST并发请求这正是它的强项。但JMeter脚本本身不适合承载复杂的业务逻辑和动态签名计算要落地到生产还是得回到代码里。5.6 接口文档不完整甚至缺失怎么办最后讲一个现实问题不是所有“接口文档”都配叫文档有些就是几行字甚至是别的项目留下的过时草稿。这时候别慌还有几条路可走。第一找对接方要Postman集合或抓包示例这是最省事的方式。第二用浏览器的开发者工具F12自己触发一次业务操作把发出的请求翻出来Method、URL、Header、Payload全部抄下来这就是一份一手文档。第三如果对方有Swagger或类似的在线调试页面直接在网页上点一遍观察请求格式和参数结构比自己猜强得多。但只要没有完整文档就属于“摸黑走路”上线前一定要对关键字段做充分验证尤其是鉴权方式、必填参数、返回码含义和幂等性。别等到生产环境出了事故才发现自己从一开始就没看清这个接口的脾气。最后说点我个人在实际操作中的体会。很多人以为“根据接口文档写脚本”是纯技术活其实它更像一个“信息还原”的活——文档里没写全的地方要靠经验去补文档里写了但不对的地方要靠测试去证。我带过的每一个新人我都会让他先把接口文档读三遍再动手写代码。这不是保守是因为读三遍之后你会在写代码之前就发现文档里的矛盾之处这比写完了再回头调试省十倍时间。如果你照着这些步骤写出来的脚本还是调不通十有八九是文档和真实接口有出入。这时候别怀疑自己回到要素表逐项核对抓一次真实请求对照着改。这事没什么捷径但也没那么难祝你一把过。