requests大文件上传
阅读本文,你将获得:
- requests大文件上传原理
- 大文件上传的方法
- 带进度的文件上传方法
为了更为方便的测试,我们现在本地docker搭建httpbin服务,httpbin
是一个测试http请求的服务,可以用来测试各种http请求,包括文件上传。
docker run -p 8888:80 -d kennethreitz/httpbin
通过httpbin
的/post
接口测试文件上传,httpbin
会将上传的文件返回,方便我们测试。
理解requests大文件上传原理
以往我们使用requests
上传文件,都是通过files
参数来上传文件,但是files
参数有个缺点,就是文件会被读取到内存中,如果文件过大,会导致内存溢出。
import requests
url = 'http://localhost:8888/post'
files = {'file': open('test.txt', 'rb')} # 读取文件到内存中
r = requests.post(url, files=files)
print(r.text)
为此我们需要像requests中stream
下载文件一样,使用"stream
"上传文件,这样文件就不会被读取到内存中。
通过阅读requests
源码,我发现当files
参数有值时,会进入_encode_files
方法。在这个方法里,会将文件一次性读取到内存中,再通过encode_multipart_formdata
方法写入body参数。
所以,我们的思考的方向就不应该使用files
字段处理文件上传。当然,这句话是基于大文件的前提。
除了files
字段,那就只剩data
字段了,继续阅读源码后,在prepare_body方法中检测了传入的data
字段,当它不满足以下条件时
is_stream = all([
hasattr(data, '__iter__'),
not isinstance(data, (basestring, list, tuple, Mapping))
])
且files
字段为空时,会进入_encode_params方法,检测如果data
存在read
方法时,就直接返回data
。
而在后续http
模块中的_send_output
方法会检测data
是否存在read
方法,如果存在就会调用read
方法,将数据yield
出去。
而在下游循环读取分片数据,发送sock请求。
看懂了它的原理,我们只需要假模假式地构造一个data
参数,让它满足read
方法即可。
import requests
class File:
def __init__(self, filename):
self.filename = filename
def read(self, size=-1):
with open(self.filename, 'rb') as file:
return file.read(size)
def __len__(self):
return 100
url = 'http://localhost:8888/post'
r = requests.post(url, data=File("train_data.json"))
print(r.text)
debugger模式下,我们可以看到数据正在被分片读取,发送到sock中。
理解了原理,自己造轮子实现未免太麻烦,我们可以使用requests-toolbelt
中的MultipartEncoder
来实现。
使用requests-toolbelt实现大文件上传
而反观requests-toolbelt
的源码,它的实现原理也是一样的。
具体的代码示例文档有很多,本文不做大幅度的展示。
from requests_toolbelt import MultipartEncoder
import requests
url = 'http://127.0.0.1:8888/post'
encoder = MultipartEncoder(
fields={'file': ("train_data.json", open("train_data.json", 'rb'), 'application/octet-stream')}
)
headers = {'Content-Type': encoder.content_type}
response = requests.post(url, data=encoder, headers=headers)
response.raise_for_status()
print(response.json())
带进度的文件上传
MultipartEncoderMonitor
可以通过回调函数来监控文件上传进度,我们可以通过它来实现带进度的文件上传。计算进度的方法很简单,就是当前已读取的字节数除以文件总字节数。
class MultipartEncoderMonitor(object):
...
def read(self, size=-1):
# 调用原始的read方法,读取分片数据
string = self.encoder.read(size)
# 累加已读取的字节数
self.bytes_read += len(string)
# 调用回调函数
self.callback(self)
return string
具体实现:
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
import requests
url = 'http://127.0.0.1:8888/post'
def callback(m):
progress = (m.bytes_read / m.len) * 100
print("\r 文件上传进度:%d%%(%d/%d)" % (progress, m.bytes_read, m.len), end=" ")
encoder = MultipartEncoder(
fields={'file': ("train_data.json", open("train_data.json", 'rb'), 'application/octet-stream')}
)
monitor = MultipartEncoderMonitor(encoder, callback)
headers = {'Content-Type': monitor.content_type}
response = requests.post(url, data=monitor, headers=headers)
response.raise_for_status()
print(response.json())
总结
本文介绍了requests大文件上传的原理,理解为何不采用files
参数来直接上传文件,理解了requests
是如何通过data
参数来实现大文件分片读取上传的,以及如何使用requests-toolbelt
实现大文件上传,以及如何实现带进度的文件上传。