Python 文件上传:一个简单却易犯的错误及解决方案

vbnet 复制代码
{
'code': 7, 
'msg': "400 Bad Request: The browser (or proxy) sent a request that this server could not understand. ('audio',)"
}

在日常开发中,使用 Python 的 requests 库上传文件是一个非常常见的操作,语法简洁使用简单。然而,正是在这种简洁的背后,隐藏着一些细节,一旦忽略,就可能导致一个令人困惑的错误:400 Bad Request

这篇文章记录了一个真实的错误案例,并剖析问题根源,复盘这个文件上传中的"坑"。

一段看似无懈可击的代码

我需要向一个 API 上传一个参考音频文件和一段文本,用于克隆音色进行配音,很自然地写出下面的代码:

python 复制代码
import requests

# --- 问题代码 ---

# 准备数据
api_url = "http://127.0.0.1:9880/apitts"
data = {"text": "这是一段测试文本", "language": "zh"}
file_path = "my_audio.wav"

try:
    # 读取文件二进制内容
    with open(file_path, 'rb') as f:
        audio_chunk = f.read()

    # 构造 files 字典
    files = {"audio": audio_chunk}

    # 发送请求
    response = requests.post(api_url, data=data, files=files)
    print(response.json())
except Exception as e:
    print(f"请求失败: {e}")

这段代码逻辑清晰:准备数据、读取文件、构造 files 字典、发送请求。然而,当运行它时,收到了一个来自服务器的冰冷回应:

json 复制代码
{'code': 7, 'msg': "400 Bad Request: The browser (or proxy) sent a request that this server could not understand. ('audio',)"}

服务器抱怨说"无法理解这个请求",并且明确指向了 'audio' 部分。这是为什么呢?代码明明把文件的二进制数据传递过去了呀?

服务器到底需要什么?

这个问题的核心在于,对 requests 库如何构建 multipart/form-data 请求的理解出现了偏差。

当通过 HTTP 上传文件时,请求的格式通常是 multipart/form-data,可以把它想象成一个快递包裹,里面有好几个独立包装的物品。

  • data 字典里的内容,就像是包裹里贴着"文本信息"标签的小件。
  • files 字典里的内容,就像是包裹里贴着"文件"标签的大件。

关键在于,每一个"大件"(文件)不仅需要有内容本身(文件的二进制数据),还需要有一张"标签",上面至少要写着它的"文件名"(filename)。服务器需要根据这个文件名来识别和处理上传的文件。

在我错误的代码中:

python 复制代码
files = {"audio": audio_chunk}

只把文件的"内容"(audio_chunk)放了进去,却忘记了附上"文件名"这张标签。requests 库在缺少这个关键信息时,无法构建出一个完全符合 HTTP 规范的 multipart/form-data 请求。服务器收到这个"缺斤少两"的请求后,自然就感到困惑,于是返回了 400 Bad Request

两种正确的文件上传姿势

知道了问题所在,解决起来就非常简单了。只需要在构造 files 字典时,把文件名也一并提供给 requests 即可。

方案一:直接传递文件对象

这是最简洁、最高效的方式。不需要手动 read() 文件,直接把 open() 返回的文件句柄传给 requests 就行。

python 复制代码
import requests

api_url = "http://127.0.0.1:9880/apitts"
data = {"text": "这是一段测试音频", "language": "zh"}
file_path = "my_audio.wav"

try:
    with open(file_path, 'rb') as f:
        # 'audio' 是表单字段名,f 是文件对象
        # requests 会自动从 f 中提取文件名和内容
        files = {"audio": f}
        response = requests.post(api_url, data=data, files=files, timeout=60)
        
        response.raise_for_status() 
        print(response.json())

except requests.exceptions.RequestException as e:
    print(f"请求出错: {e}")
except FileNotFoundError:
    print(f"文件未找到: {file_path}")

优点:

  1. 代码简洁:省去了手动读写文件的步骤。
  2. 内存高效requests 会以数据流的方式读取文件,而不是一次性把整个大文件加载到内存中。这在上传大文件时至关重要。

方案二:使用元组提供详细信息

如果确实需要先将文件内容读入内存(比如,文件内容来自网络下载,或者需要预处理),可以使用元组来手动指定文件名和文件内容。

元组的格式是:(filename, file_content)

python 复制代码
import requests

api_url = "http://127.0.0.1:9880/apitts"
data = {"text": "这是一段测试音频", "language": "zh"}
file_path = "my_audio.wav"

try:
    with open(file_path, 'rb') as f:
        audio_chunk = f.read()

    # 使用元组来指定文件名和文件内容
    files = {"audio": ('my_audio.wav', audio_chunk)}

    response = requests.post(api_url, data=data, files=files, timeout=60)
    response.raise_for_status()
    print(response.json())

except requests.exceptions.RequestException as e:
    print(f"请求出错: {e}")

这种方式同样能解决问题,并且提供了更高的灵活性。

让代码更健壮

掌握了正确的上传方法后,还可以做一些事情让代码变得更专业、更可靠。

  1. 明确指定 MIME 类型 : 有时候,服务器还需要知道文件的具体类型(MIME type),比如是 image/jpeg 还是 audio/wav。可以在元组中加入第三个元素来指定它。

    python 复制代码
    files = {"audio": ('my_audio.wav', audio_chunk, 'audio/wav')}
    files = {"avatar": ('user.jpg', image_bytes, 'image/jpeg')}
  2. 完善的错误处理 : 网络请求和文件操作都可能失败。一个健壮的程序应该考虑到各种异常情况,比如文件不存在 (FileNotFoundError)、请求超时、服务器错误等。使用 try...except 块并捕获 requests.exceptions.RequestException 是一个很好的实践。

  3. 使用 response.raise_for_status() : 在收到响应后,调用这个方法。如果响应的状态码是 4xx(客户端错误)或 5xx(服务器错误),它会主动抛出一个异常。这比自己写 if response.status_code != 200: 要方便得多。


requests 库为我们屏蔽了复杂的 HTTP 协议细节,但我们仍需理解其基本工作原理:向服务器发送数据时,不仅仅是数据本身,描述数据的元信息(如文件名、内容类型)同样至关重要。

一个简单的规则:

  • 当上传文件时,files 字典的值要么是文件对象(推荐),要么是包含 (文件名, 文件内容, ...) 的元组。
相关推荐
斐夷所非32 分钟前
人工智能 AI. 机器学习 ML. 深度学习 DL. 神经网络 NN 的区别与联系
人工智能
F_D_Z1 小时前
数据集相关类代码回顾理解 | StratifiedShuffleSplit\transforms.ToTensor\Counter
python·torchvision·transforms
Funny_AI_LAB2 小时前
OpenAI DevDay 2025:ChatGPT 进化为平台,开启 AI 应用新纪元
人工智能·ai·语言模型·chatgpt
深瞳智检2 小时前
YOLO算法原理详解系列 第002期-YOLOv2 算法原理详解
人工智能·算法·yolo·目标检测·计算机视觉·目标跟踪
tao3556672 小时前
【Python刷力扣hot100】283. Move Zeroes
开发语言·python·leetcode
深眸财经3 小时前
机器人再冲港交所,优艾智合能否破行业困局?
人工智能·机器人
小宁爱Python3 小时前
从零搭建 RAG 智能问答系统1:基于 LlamaIndex 与 Chainlit实现最简单的聊天助手
人工智能·后端·python
湖南人爱科技有限公司3 小时前
RaPhp和Python某音最新bd-ticket-guard-client-data加密算法解析(视频评论)
android·python·php·音视频·爬山算法·raphp
新知图书4 小时前
Encoder-Decoder架构的模型简介
人工智能·架构·ai agent·智能体·大模型应用开发·大模型应用
大模型真好玩4 小时前
低代码Agent开发框架使用指南(一)—主流开发框架对比介绍
人工智能·低代码·agent