☰
curl2py:将curl命令转换为可运行Python脚本的完整指南
2026/10/12 2:40:01 网站建设 项目流程

简介:curl2py 是一份面向 Python 开发者与运维人员的轻量工具脚本,用于将日常调试中常见的 curl 命令快速转换为可直接运行的 Python 脚本,省去手工改写请求头、参数与数据体的重复劳动,适合需要频繁对接 HTTP 接口、做接口调试或爬虫原型验证的初中级使用者。压缩包共 3 个文件,以 1 个 py 主脚本为核心,辅以 1 个 md 说明文档和 1 个 license 授权文件,整体仅 14KB,结构精简、开箱即用。脚本支持通过 -r 或 -raw 参数以原始格式输出,默认则采用 json pretty print 美化结果,便于阅读与二次修改。目前已有 1169 人学习下载,说明其在接口调试场景中具备一定实用价值。读者可借此获得一个可复用的转换脚本、清晰的使用说明与授权信息,快速把命令行请求迁移到 Python 代码中,减少手动转写成本,并为后续接口自动化与脚本化调试提供基础。

1. curl2py:把一条 curl 命令变成能跑、能改、能进版本库的 Python 脚本

调接口时最顺手的工具往往是 curl:从浏览器 DevTools 里右键复制一条 cURL,粘到终端里就能验证参数对不对。但这条命令一旦要进代码库、要加签名、要重试、要接日志,就得手工翻译成requests,翻译过程里最容易翻车的就是引号、转义和-d与--data-binary的区别。curl2py 要解决的就是这一段:把一条 curl 命令解析成结构化的请求对象,再生成一份可运行的 Python 脚本,而不是让你对着命令逐字抄。

它适合三类人:一是经常从抓包工具复制 cURL 做接口验证的后端和测试;二是要把第三方接口调用固化成脚本的数据工程同学;三是写爬虫或自动化任务、需要把浏览器请求快速落成代码的人。核心价值不在“转换”这个动作本身,而在于转换后得到的脚本能直接进项目、能改参数、能加异常处理。下面按“先讲清解析原理,再动手实现,最后说坑”的顺序展开,中间给出一份可以照着复现的最小实现。

2. 拆解 curl 命令:从字符串到请求对象的解析路径

2.1 为什么不能直接用 shlex.split 一把梭

很多人第一反应是shlex.split(cmd),把命令切成 token 列表,然后按位置取参数。这个做法在简单命令上能跑,但遇到真实场景立刻出问题。curl 的参数体系里,短选项和长选项混用、值可以紧跟也可以空格分隔、-H可以出现多次、--data-urlencode和-d语义不同,更麻烦的是 Windows 下复制出来的命令用^换行、PowerShell 用反引号换行,shlex默认按 POSIX 规则处理,会把反斜杠和引号吃掉。

我一般会分两步走:先做“续行符归一化”,把多行命令拼成单行;再做“token 化”,但不用shlex的默认模式,而是自己写一个状态机,明确区分“在引号内”和“在引号外”。这样做的另一个好处是能保留原始引号信息,后面判断某个值到底是被单引号包裹还是双引号包裹时不会丢线索。

def normalize_command(cmd: str) -> str: # 处理常见续行符:bash 的反斜杠、PowerShell 的反引号、Windows cmd 的 ^ cmd = cmd.replace("\\\n", " ").replace("`\n", " ").replace("^\n", " ") # 折叠多余空白,但不动引号内部 return " ".join(cmd.split())

这段代码只做归一化,不做语义解析。replace的顺序有讲究:先处理反斜杠续行,再处理反引号和^,因为反斜杠在 Windows 路径里也常见,放在最后容易误伤。" ".join(cmd.split())会把连续空格和制表符压成一个空格,这对后续 token 化是安全的,因为 curl 命令里引号内的空格不会被split()拆开——前提是引号成对。

2.2 手写 tokenizer:把引号状态管起来

tokenizer 的目标是把归一化后的字符串切成 token,同时记录每个 token 是否来自引号包裹。状态机只有两个状态:IN_QUOTE和OUT_QUOTE,遇到单引号或双引号切换状态,遇到空格且不在引号内就切分。

def tokenize(cmd: str): tokens, buf, quote = [], [], None for ch in cmd: if quote: if ch == quote: quote = None # 闭合引号 else: buf.append(ch) else: if ch in ("'", '"'): quote = ch # 进入引号状态 elif ch.isspace(): if buf: tokens.append("".join(buf)) buf = [] else: buf.append(ch) if buf: tokens.append("".join(buf)) return tokens

逻辑说明:quote为None表示在引号外,遇到引号字符就进入对应状态;在引号内时,只有匹配的引号才闭合,其他字符原样进buf。这样-H 'Content-Type: application/json'会被切成["-H", "Content-Type: application/json"],冒号和空格都保留。参数说明:这个实现不处理转义引号(比如\"),因为 curl 命令里转义引号出现频率低,真遇到可以在quote分支里加一个prev == "\\"的判断。

2.3 把 token 映射成请求字段

有了 token 列表,接下来按 curl 的参数表做映射。核心映射关系如下:

curl 参数含义映射到 requests
-X/--requestHTTP 方法method
-H/--header请求头,可多次headers字典
-d/--data请求体,默认 POSTdata
--data-binary二进制体,不处理换行data(bytes)
--data-urlencodeURL 编码后的体data(需编码)
-u/--userBasic Authauth
--compressed请求压缩响应headers["Accept-Encoding"]
-k/--insecure跳过证书校验verify=False
-L/--location跟随重定向allow_redirects=True

映射时要注意:-d出现多次时,curl 会用&拼接,而requests的data传字典会自动编码,传字符串则原样发送。我一般统一转成字符串再交给requests,避免字典编码顺序和 curl 不一致导致签名失败。

def parse_tokens(tokens): req = {"method": None, "headers": {}, "data": None, "auth": None, "verify": True, "allow_redirects": False} i = 0 while i < len(tokens): t = tokens[i] if t in ("-X", "--request"): req["method"] = tokens[i + 1]; i += 2 elif t in ("-H", "--header"): k, _, v = tokens[i + 1].partition(":") req["headers"][k.strip()] = v.strip(); i += 2 elif t in ("-d", "--data", "--data-binary", "--data-urlencode"): req["data"] = tokens[i + 1]; i += 2 elif t in ("-u", "--user"): req["auth"] = tuple(tokens[i + 1].split(":", 1)); i += 2 elif t in ("-k", "--insecure"): req["verify"] = False; i += 1 elif t in ("-L", "--location"): req["allow_redirects"] = True; i += 1 else: i += 1 if req["method"] is None: req["method"] = "POST" if req["data"] else "GET" return req

这段代码里partition(":")只切第一个冒号,保证Authorization: Bearer xxx这种值里带冒号的情况不被切坏。-d多次出现时这里只保留最后一次,真实场景需要改成列表再拼接,我在下一章会补上。方法推断逻辑是 curl 的默认行为:有 body 就是 POST,没有就是 GET。

3. 生成可运行的 Python 脚本:模板、参数与边界处理

3.1 脚本模板长什么样

转换的终点不是打印一个字典,而是生成一份能直接python run.py执行的脚本。模板要包含四部分:导入、请求参数、请求发送、响应处理。我习惯把参数抽成模块级常量,方便改;把请求包在函数里,方便复用;响应处理里加状态码判断和异常捕获,避免脚本一跑就抛栈。

TEMPLATE = '''import requests URL = "{url}" HEADERS = {headers} DATA = {data} AUTH = {auth} def main(): resp = requests.request( method="{method}", url=URL, headers=HEADERS, data=DATA, auth=AUTH, verify={verify}, allow_redirects={allow_redirects}, timeout=15, ) print(resp.status_code) print(resp.text[:2000]) if __name__ == "__main__": main() '''

逻辑说明:requests.request是统一入口,方法作为参数传入,比requests.get/requests.post更灵活。timeout=15是硬编码的默认值,生成后可以改。resp.text[:2000]只打印前 2000 字符,避免大响应刷屏。参数说明:verify和allow_redirects直接来自解析结果,AUTH为None时requests会自动忽略。

3.2 用 repr 而不是 json.dumps 来序列化参数

生成脚本时,HEADERS和DATA要写成 Python 字面量。很多人用json.dumps,结果None变成null、True变成true,脚本直接语法错误。正确做法是用repr(),它输出的就是合法的 Python 字面量。

def render(req, url): return TEMPLATE.format( url=url, headers=repr(req["headers"]), data=repr(req["data"]), auth=repr(req["auth"]), method=req["method"], verify=req["verify"], allow_redirects=req["allow_redirects"], )

repr对字典的输出是{'Content-Type': 'application/json'},对None输出None,对元组输出('user', 'pass'),全部合法。注意url这里没有用repr,因为模板里已经加了引号,如果 URL 里本身有引号会出问题,稳妥做法是 URL 也用repr并在模板里去掉引号。

3.3 处理 -d 多次出现和 --data-urlencode

curl 允许-d a=1 -d b=2,实际发送的是a=1&b=2。--data-urlencode则会对值做 URL 编码。这两点在解析阶段就要合并,不能留到生成阶段。

def merge_data(parts, urlencode_flags): merged = [] for p, enc in zip(parts, urlencode_flags): if enc: k, _, v = p.partition("=") merged.append(f"{k}={quote(v, safe='')}") else: merged.append(p) return "&".join(merged)

逻辑说明:parts是-d系列参数的值列表,urlencode_flags标记每个值是否来自--data-urlencode。quote(v, safe='')对值做全编码,safe=''表示连/也编码,符合 curl 的行为。参数说明:如果-d的值本身是 JSON 字符串,不要走这个合并逻辑,直接原样发送,否则会把 JSON 里的{}编码掉。

3.4 一个完整的转换入口

把前面的函数串起来,就是一个最小可用的 curl2py:

def curl2py(cmd: str) -> str: cmd = normalize_command(cmd) tokens = tokenize(cmd) # 提取 URL:第一个不以 - 开头的 token url = next(t for t in tokens if not t.startswith("-")) req = parse_tokens(tokens) return render(req, url) if __name__ == "__main__": import sys print(curl2py(sys.stdin.read()))

用法:echo "curl -X POST https://example.com/api -H 'Content-Type: application/json' -d '{\"a\":1}'" | python curl2py.py。输出就是一份可运行的脚本。注意 URL 提取逻辑假设 URL 是第一个非选项 token,这在绝大多数 curl 命令里成立,但如果命令里先出现--url参数就会取错,稳妥做法是显式处理--url。

4. 避坑与排查:转换脚本最容易翻车的五个地方

4.1 现象:生成的脚本报JSONDecodeError,但 curl 能跑通

原因通常是-d的值在 tokenize 阶段被引号处理吃掉了转义。比如-d '{"a": "b"}'在 shell 里单引号内的双引号是字面量,但如果你从 DevTools 复制的是双引号包裹、内部双引号转义的版本,tokenizer 会把\"当成两个字符。解决:在 tokenizer 的引号分支里加转义判断,遇到\时把下一个字符原样入buf并跳过状态切换。

4.2 现象:请求头少了Content-Type,服务端返回 415

原因是-H的值里冒号后面有空格,partition(":")后v.strip()去掉了,但有些服务端要求Content-Type:application/json不带空格。更隐蔽的情况是-H出现多次且键名大小写不一致,字典覆盖后只剩一个。解决:headers 用CaseInsensitiveDict或在解析时统一小写键名,同时保留原始值不做 strip。

4.3 现象:--compressed没生效,响应是乱码

--compressed在 curl 里是请求压缩响应,对应到requests是设置Accept-Encoding: gzip, deflate。但requests默认就会带这个头并自动解压,所以多数情况下不用管。真正会出问题的是服务端返回了Content-Encoding: br(Brotli),而本地没装brotli库,requests解不了。解决:要么装brotli,要么在 headers 里显式声明只接受 gzip。

4.4 现象:-k转换后verify=False,但脚本仍然报 SSL 错误

verify=False只跳过证书校验,不跳过 SNI 和协议版本协商。如果服务端只支持 TLS 1.3 而本地 OpenSSL 版本低,照样握手失败。另外verify=False会触发InsecureRequestWarning,脚本里最好加urllib3.disable_warnings()。解决:确认本地 OpenSSL 版本,必要时升级requests和urllib3。

4.5 现象:URL 里带&,tokenize 后被截断

如果 URL 没有用引号包裹,&在 shell 里是后台执行符,但复制到 Python 字符串里&本身不是分隔符。真正会截断的是 tokenizer 把 URL 里的空格当分隔符——URL 里不该有空格,但编码后的%20不会触发。如果 URL 里真的有未编码空格,tokenize 会把它切开。解决:在 normalize 阶段检测 URL 段并做一次quote,或者要求用户给 URL 加引号。

5. 进阶:把转换器接进工作流,而不是每次手工跑

5.1 做成命令行工具,支持管道和文件输入

最小实现是sys.stdin.read(),但真实使用里更顺手的是支持-f读文件、-o写输出、--no-verify覆盖verify。用argparse包一层即可,核心转换逻辑不变。这样就能在 CI 里批量转换一批 curl 命令,生成对应的冒烟测试脚本。

import argparse def cli(): ap = argparse.ArgumentParser() ap.add_argument("-f", "--file", help="curl 命令文件") ap.add_argument("-o", "--output", help="输出脚本路径") ap.add_argument("--timeout", type=int, default=15) args = ap.parse_args() cmd = open(args.file).read() if args.file else sys.stdin.read() script = curl2py(cmd) if args.output: open(args.output, "w").write(script) else: print(script)

参数说明:--timeout透传到模板里的timeout字段,需要在render里加一个占位符。-f和标准输入二选一,-o不传就打到终端。这个工具本身不发起请求,只做转换,所以可以安全地放进任何环境。

5.2 用真实响应做回归验证

转换出来的脚本对不对,不能只看语法。我的习惯是:对同一个接口,先跑 curl 拿到响应,再跑生成的脚本,对比状态码和响应体的关键字段。可以写一个简单的对比脚本:

import subprocess, json, requests def compare(curl_cmd, py_script): curl_out = subprocess.run(curl_cmd, shell=True, capture_output=True, text=True).stdout py_out = subprocess.run(["python", py_script], capture_output=True, text=True).stdout # 只对比状态码和 JSON 顶层键 return curl_out[:100] == py_out[:100]

这个对比很粗糙,但能快速发现方法、URL、body 是否一致。更严格的做法是把 curl 的-w "%{http_code}"输出和脚本里的resp.status_code对齐,再对响应体做 JSON 归一化后 diff。

5.3 一个容易被忽略的技巧:保留原始 curl 命令作为注释

生成的脚本头部加一行# source: curl ...,把原始命令以注释形式保留。这样做的好处是半年后回头看脚本,能立刻知道它对应哪条命令,改参数时也有参照。实现上就是在render的模板开头拼一行,注意把命令里的换行替换成空格,避免注释跨行。

我自己的习惯是:任何从 curl 转出来的脚本,第一行必须是原始命令注释,第二行是转换时间。这个习惯帮我省过好几次“这个 header 到底哪来的”的排查时间。转换器本身不复杂,难的是把边界情况一条条覆盖到,上面这些坑都是我实际踩过之后才补进解析逻辑的。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询