【免费下载链接】libtv-skills
在libtv-skills项目中,upload_file.py负责把本地的图片、视频文件上传到 LibTV 的 OSS 存储。它没有引入requests等第三方库,而是用 Python 标准库逐字节手写 multipart/form-data 请求体——这正是很多新手觉得"上传接口很神秘"的地方。本文带你拆解 upload_file.py 的完整源码,理解它如何构造边界(boundary)、拼装表单字段、发起 POST 请求并处理错误。
一、为什么选择手写 multipart/form-data?
很多人上传文件时习惯requests+files=参数,两行代码搞定。但 libtv-skills 有一个核心设计目标:仅使用 Python 标准库,零依赖安装。
这一点在技能描述文件 SKILL.md 中明确写着:
无需安装额外依赖,仅使用 Python 标准库。
因此整个scripts/目录(包括 create_session.py、query_session.py、download_results.py)都基于urllib.request实现 HTTP 请求。upload_file.py也不例外——它把multipart/form-data的"魔法"拆开了,让你看到请求体最原始的样子。
📌核心收益:
- ✅ 跨环境即用,不需要
pip install任何包 - ✅ 请求体结构完全可控、可调试
- ✅ 适合学习 HTTP 协议细节
二、源码结构总览
先建立全局视角,upload_file.py 整体只有约 120 行,职责非常清晰:
| 行号区间 | 内容 | 作用 |
|---|---|---|
| L14 | from _common import IM_BASE, ACCESS_KEY | 复用公共模块的环境变量配置 |
| L17 | ALLOWED_PREFIXES = ("image/", "video/") | 白名单:只允许图片、视频 |
| L20-L82 | upload_file(file_path) | 核心上传函数 |
| L36-L60 | 请求体拼装(boundary + 两个字段) | 手写 multipart 的关键区段 |
| L62-L82 | 发起 POST 并解析响应 | 网络层 |
| L85-L116 | main()命令行入口 | 参数解析 + 结果输出 |
环境变量配置来自公共模块 _common.py:IM_BASE默认指向https://im.liblib.tv,ACCESS_KEY从LIBTV_ACCESS_KEY读取(未设置时直接报错退出)。
三、第 1 步:上传前的双重校验
在动网络之前,代码先做两道"安检",见 upload_file.py:
if not os.path.isfile(file_path): print(f"错误:文件不存在: {file_path}", file=sys.stderr) sys.exit(1) # 检查 MIME 类型 mime_type, _ = mimetypes.guess_type(file_path) if mime_type and not any(mime_type.startswith(p) for p in ALLOWED_PREFIXES): print(f"错误:不支持的文件类型: {mime_type},仅支持图片和视频", file=sys.stderr) sys.exit(1)- 文件是否存在:
os.path.isfile判断路径,不存在则立刻退出,避免浪费一次网络请求。 - MIME 类型白名单:
mimetypes.guess_type根据扩展名猜测类型(如a.png→image/png),只有image/或video/前缀才放行。
💡 注意这个细节:if mime_type的判断意味着,当扩展名无法识别(MIME 为None)时不拦截,交给后面的Content-Type: application/octet-stream兜底,由服务端最终裁决。这是一种宽容式校验。
四、第 2 步:逐字节拼装请求体(核心)
这是全文最值得学习的部分。multipart/form-data 的本质是:用一串"边界"(boundary)把多个表单字段切成块,字段之间是头部 + 空行 + 二进制内容。
4.1 生成唯一的 boundary
boundary = f"----PythonUpload{uuid.uuid4().hex}"用uuid4保证每次请求的边界串都不同,避免与服务端解析冲突。这个 boundary 稍后还要写进请求头,两处必须严格一致。
4.2 拼装两个表单字段
请求体共包含两个字段:文本字段accessKey和文件字段file,对应源码 L39-L58:
body_parts = [] # accessKey 字段(普通文本字段) body_parts.append(f"--{boundary}\r\n".encode()) body_parts.append(b'Content-Disposition: form-data; name="accessKey"\r\n\r\n') body_parts.append(f"{ACCESS_KEY}\r\n".encode()) # file 字段(文件字段,带文件名和内容类型) content_type = mime_type or "application/octet-stream" body_parts.append(f"--{boundary}\r\n".encode()) body_parts.append( f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'.encode() ) body_parts.append(f"Content-Type: {content_type}\r\n\r\n".encode()) with open(file_path, "rb") as f: body_parts.append(f.read()) body_parts.append(b"\r\n") # 结束边界 body_parts.append(f"--{boundary}--\r\n".encode()) data = b"".join(body_parts)把body_parts拼接后的完整结构画出来就是这样({...}为占位内容):
--{boundary}\r\n Content-Disposition: form-data; name="accessKey"\r\n \r\n {ACCESS_KEY}\r\n --{boundary}\r\n Content-Disposition: form-data; name="file"; filename="{文件名}"\r\n Content-Type: image/png\r\n \r\n {文件二进制内容}\r\n --{boundary}--\r\n🔍逐行看懂这 4 个关键点:
- 每个字段都以
--{boundary}\r\n开头——边界行是该字段的"门牌号"。 - 文本字段没有 filename,也没有 Content-Type,头部和值之间只有一个空行(
\r\n\r\n)分隔。 - 文件字段必须带
filename=和Content-Type:,服务端靠这两项识别"这是一个文件"以及它的类型;类型猜不出时兜底为application/octet-stream(二进制流)。 - 结尾的
--{boundary}--注意多了两个短横线,表示"表单到此结束",这是新手最常漏写的地方。
4.3 为什么用b"".join而不是字符串拼接?
因为请求体里混着二进制文件数据,字符串无法安全承载任意字节。代码全程使用bytes列表(body_parts)再一次性b"".join(body_parts)拼合,是处理文件上传的标准姿势。
⚠️ 小提示:这里会把整个文件读进内存。对 200MB 以内的素材没问题,但若是超大文件,应改用分块流式上传。
五、第 3 步:请求头与 POST 发送
拼好的请求体交给urllib.request.Request,见 upload_file.py:
url = f"{IM_BASE.rstrip('/')}/openapi/upload" req = urllib.request.Request( url, data=data, method="POST", headers={ "Authorization": f"Bearer {ACCESS_KEY}", "Content-Type": f"multipart/form-data; boundary={boundary}", }, ) try: with urllib.request.urlopen(req, timeout=120) as resp: result = json.loads(resp.read().decode("utf-8")) return result.get("data", {})三个细节值得注意:
| 细节 | 说明 |
|---|---|
IM_BASE.rstrip('/') | 去掉末尾斜杠,防止拼出https://host//openapi/upload双斜杠 |
Content-Type必须带boundary= | 服务端靠它才知道边界串是什么,漏写会解析失败 |
timeout=120 | 视频可能较大,超时放宽到 120 秒(对比 _common.py 中 JSON 接口只有 30 秒) |
鉴权方式与整个项目一致:请求头Authorization: Bearer <LIBTV_ACCESS_KEY>,这一点在 README.md 的 API 参考部分有统一说明。
六、第 4 步:响应解析与错误处理
成功时,从 JSON 响应的data字段取出 OSS 地址;失败时按异常类型分别处理,见 upload_file.py:
except urllib.error.HTTPError as e: err_body = e.read().decode("utf-8") if e.fp else "" print(f"API 错误 {e.code}: {err_body}", file=sys.stderr) sys.exit(1) except urllib.error.URLError as e: print(f"网络错误: {e.reason}", file=sys.stderr) sys.exit(1)HTTPError(服务端返回 4xx/5xx):读出响应体原文打印,比如类型被拒、文件超限等,便于定位。URLError(连不上服务器):打印网络原因(DNS 失败、超时等)。- 所有错误信息统一写到
stderr,并以sys.exit(1)退出——这样 AI Agent 或非交互环境能可靠地感知失败。
七、实际使用:一条命令完成上传
配置好环境变量后,直接执行即可:
export LIBTV_ACCESS_KEY="your-access-key" # 上传图片 python3 skills/libtv-skill/scripts/upload_file.py /path/to/image.png # 上传视频 python3 skills/libtv-skill/scripts/upload_file.py /path/to/video.mp4成功时输出 JSON 格式(main()中只保留最精简的url字段):
{ "url": "https://libtv-res.liblib.art/claw/{projectUuid}/{uuid}.png" }这个 OSS 地址会在创作流程中派上用场:先上传参考图/视频,再把 URL 拼进消息发给后端,例如 SKILL.md 中"场景 2:用户提供图片/视频要求编辑修改"的典型工作流:
1. upload_file.py /path/to/video.mp4 → 拿到 OSS URL 2. create_session.py "把四周的纸船都换成白色的纸爱心 参考视频:{oss_url}" 3. 轮询 query_session.py 获取结果八、总结:手写 multipart 的 5 个要点
学完upload_file.py,你掌握了手写 multipart/form-data 上传的完整套路:
- boundary 全局唯一:生成一次,同时用于请求体和
Content-Type请求头。 - 每个字段 = 边界行 + 头部 + 空行 + 内容,头部用
Content-Disposition声明字段名。 - 文件字段额外带
filename和Content-Type,类型未知时用application/octet-stream兜底。 - 结尾必须写
--boundary--(注意双短横线)。 - 全程用
bytes拼接,二进制数据安全无损。
这套"零依赖 + 手写协议"的写法,让 upload_file.py 在任何有 Python 3.6+ 的机器上开箱即用,也是 libtv-skills 整个技能包保持一致风格的缩影。
📚延伸阅读:公共鉴权与 HTTP 封装见 _common.py,上传在整体工作流中的位置见 SKILL.md,所有脚本用法速查见 README.md。
【免费下载链接】libtv-skills
相关推荐
NSwag文件上传处理:multipart/form-data的OpenAPI描述
NSwag文件上传处理:multipart/form data的OpenAPI描述 文件上传是Web开发中的常见需求,而 multipart/form data
开发工具代码生成API设计5分钟掌握HTTPie CLI文件上传:multipart/form-data完全指南
5分钟掌握HTTPie CLI文件上传:multipart/form data完全指南 HTTPie CLI是一款现代化、用户友好的命令行HTTP客户端,特别适
CLI开发工具Guzzle文件上传终极指南:multipart/form-data完全解析
Guzzle文件上传终极指南:multipart/form data完全解析 在Web开发中,文件上传是常见需求,但处理 multipart/form data
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考