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 字典的值要么是文件对象(推荐),要么是包含 (文件名, 文件内容, ...) 的元组。
相关推荐
IT_陈寒2 小时前
Vue3性能优化实战:这5个技巧让我的应用加载速度提升了70%
前端·人工智能·后端
机器之心2 小时前
英伟达50亿美元入股英特尔,将发布CPU+GPU合体芯片,大结局来了?
人工智能·openai
新智元2 小时前
芯片大地震,黄仁勋355亿入股!英特尔要为老黄造CPU,股价狂飙30%
人工智能·openai
Juchecar2 小时前
NumPy编程:鼓励避免 for 循环
python
阿然1653 小时前
首次尝试,95% 的代码都是垃圾:一位工程师使用 Claude Code 六周的心得
人工智能·agent·ai编程
martinzh3 小时前
RAG系统优化大揭秘:让你的AI从学渣变学霸的进化之路
人工智能
Java陈序员3 小时前
直播录制神器!一款多平台直播流自动录制客户端!
python·docker·ffmpeg
c8i3 小时前
drf 在django中的配置
python·django
汀丶人工智能3 小时前
想成为AI绘画高手?打造独一无二的视觉IP!Seedream 4.0 使用指南详解,创意无界,效率翻倍!
人工智能