Python:如何使用流式传输方式Post一个大文件

18

我正在将可能很大的文件上传到网络服务器。目前我正在这样做:

import urllib2

f = open('somelargefile.zip','rb')
request = urllib2.Request(url,f.read())
request.add_header("Content-Type", "application/zip")
response = urllib2.urlopen(request)

然而,这会在将文件上传到服务器之前将整个文件的内容读入内存中。我该如何使其将文件流式传输到服务器?


相关:使用生成器进行WSGI文件流传输 - Piotr Dobrogost
相关链接:https://dev59.com/w3E95IYBdhLWcg3wApBU - Christophe Roussy
6个回答

30

通过阅读systempuntoout提供的邮件列表主题,我找到了解决方案的线索。

mmap模块允许您打开像字符串一样的文件。文件的部分内容按需加载到内存中。

这是我现在使用的代码:

import urllib2
import mmap

# Open the file as a memory mapped string. Looks like a string, but 
# actually accesses the file behind the scenes. 
f = open('somelargefile.zip','rb')
mmapped_file_as_string = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)

# Do the request
request = urllib2.Request(url, mmapped_file_as_string)
request.add_header("Content-Type", "application/zip")
response = urllib2.urlopen(request)

#close everything
mmapped_file_as_string.close()
f.close()

请问您能否确认以下代码是否正确:request = urllib2.Request(url, mmapped_file_as_string) - Ayyappan Anbalagan

5
文档中没有提到这一点,但是urllib2(和httplib)的代码接受具有read()方法的任何对象作为数据。因此,使用一个打开的文件似乎就可以了。
您需要自己设置Content-Length头。如果未设置,urllib2将调用数据的len(),而文件对象不支持此操作。
import os.path
import urllib2

data = open(filename, 'r')
headers = { 'Content-Length' : os.path.getsize(filename) }
response = urllib2.urlopen(url, data, headers)

这是处理您提供的数据的相关代码。它来自于Python 2.7中的httplib.py中的HTTPConnection类:
def send(self, data):
    """Send `data' to the server."""
    if self.sock is None:
        if self.auto_open:
            self.connect()
        else:
            raise NotConnected()

    if self.debuglevel > 0:
        print "send:", repr(data)
    blocksize = 8192
    if hasattr(data,'read') and not isinstance(data, array):
        if self.debuglevel > 0: print "sendIng a read()able"
        datablock = data.read(blocksize)
        while datablock:
            self.sock.sendall(datablock)
            datablock = data.read(blocksize)
    else:
        self.sock.sendall(data)

urllib2.urlopen(url, data, headers)不接受headers作为参数,因此该行代码response = urllib2.urlopen(url, data, headers)将无法工作。我在下面的答案中提供了可行的代码。 - Sergey Nudnov
使用requests模块可以实现这个吗?我需要分块发送文件(10MB),但不想将整个10MB读入内存,而是想读取一些字节(8192)并发送到requests,直到完成10MB。 - Simplecode

2
你尝试过使用Mechanize吗?
from mechanize import Browser
br = Browser()
br.open(url)
br.form.add_file(open('largefile.zip'), 'application/zip', 'largefile.zip')
br.submit()

或者,如果您不想使用multipart/form-data,请查看this旧帖子。
它提供了两个选项:
  1. Use mmap, Memory Mapped file object
  2. Patch httplib.HTTPConnection.send

1
我不想发送编码为“multipart/form-data”的文件。这似乎可以实现。我只是想要一个原始的POST请求。 - Daniel Von Fange
在Python 2.7中,选项#2已经被添加并修补了,块大小为8192,我想知道为什么...嗯。这方面的规范/标准是什么? - MistahX

1
使用requests库,您可以执行以下操作。
with open('massive-body', 'rb') as f:
    requests.post('http://some.url/streamed', data=f)

如下所述:

正如在他们的文档这里提到的。


仍然适用8K块大小,因为调用httplib.py,send() L#869。 - paul_h

1

尝试使用pycurl。我没有任何设置可以接受一个不在multipart/form-data POST中的大文件,但这里有一个简单的示例,按需读取文件。

import os
import pycurl

class FileReader:
    def __init__(self, fp):
        self.fp = fp
    def read_callback(self, size):
        return self.fp.read(size)

c = pycurl.Curl()
c.setopt(pycurl.URL, url)
c.setopt(pycurl.UPLOAD, 1)
c.setopt(pycurl.READFUNCTION, FileReader(open(filename, 'rb')).read_callback)
filesize = os.path.getsize(filename)
c.setopt(pycurl.INFILESIZE, filesize)
c.perform()
c.close()

1
谢谢JimB。我本来会用这个的,但是我有一些使用Windows的人,我不想让他们再安装其他东西了。 - Daniel Von Fange

0
以下是适用于Python 2 / Python 3的工作示例:
try:
    from urllib2 import urlopen, Request
except:
    from urllib.request import urlopen, Request

headers = { 'Content-length': str(os.path.getsize(filepath)) }
with open(filepath, 'rb') as f:
    req = Request(url, data=f, headers=headers)
    result = urlopen(req).read().decode()

requests模块非常好用,但有时您无法安装任何额外的模块...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接