HTTP下载非常大的文件

时间:2009-10-08 15:43:00

标签: python http download twisted

我正在使用Python / Twisted的Web应用程序。

我希望用户能够下载一个非常大的文件(> 100 Mb)。当然,我不想将所有文件加载到(服务器的)内存中。

服务器端我有这个想法:

...
request.setHeader('Content-Type', 'text/plain')
fp = open(fileName, 'rb')
try:
    r = None
    while r != '':
        r = fp.read(1024)
        request.write(r)
finally:
    fp.close()
    request.finish()

我希望这可行,但我有问题: 我正在使用FF进行测试......似乎浏览器让我等到文件下载完毕,然后我打开/保存对话框。

我立即预期对话框,然后是进度条...

也许我必须在Http标题中添加一些内容...类似于文件的大小?

4 个答案:

答案 0 :(得分:35)

您发布的示例代码存在两个大问题:它是非合作的,它会在发送之前将整个文件加载到内存中。

while r != '':
    r = fp.read(1024)
    request.write(r)

请记住,Twisted使用协作式多任务来实现任何类型的并发。所以这个代码片段的第一个问题是它是整个文件内容的一个while循环(你说它很大)。这意味着整个文件将被读入内存并在任何之前写入响应,否则可能会在此过程中发生。在这种情况下,碰巧“任何”还包括将字节从内存缓冲区推送到网络上,因此您的代码也会立即将整个文件保存在内存中并且只能开始获取当这个循环完成时摆脱它。

因此,作为一般规则,您不应编写代码以在基于Twisted的应用程序中使用,该应用程序使用这样的循环来完成大工作。相反,你需要以与事件循环合作的方式完成大工作的每一小部分。要通过网络发送文件,最好的方法是使用 producer consumer 。这是两个相关的API,用于使用缓冲区空事件移动大量数据,以便有效地执行它,而不会浪费不合理的内存量。

您可以在此处找到这些API的一些文档:

http://twistedmatrix.com/projects/core/documentation/howto/producers.html

幸运的是,对于这种非常常见的情况,还有一个已经编写的生产者可以使用,而不是实现自己的:

http://twistedmatrix.com/documents/current/api/twisted.protocols.basic.FileSender.html

您可能希望使用类似这样的内容:

from twisted.protocols.basic import FileSender
from twisted.python.log import err
from twisted.web.server import NOT_DONE_YET

class Something(Resource):
    ...

    def render_GET(self, request):
        request.setHeader('Content-Type', 'text/plain')
        fp = open(fileName, 'rb')
        d = FileSender().beginFileTransfer(fp, request)
        def cbFinished(ignored):
            fp.close()
            request.finish()
        d.addErrback(err).addCallback(cbFinished)
        return NOT_DONE_YET

您可以在我的博客http://jcalderone.livejournal.com/50562.html上阅读更多关于NOT_DONE_YET和其他相关提示的“60秒内的扭曲网络”系列(特别参见“异步响应”条目)。

答案 1 :(得分:3)

是的,Content-Length标题会为您提供所需的进度条!

答案 2 :(得分:3)

如果这确实是text/plain内容,那么每当客户表示可以处理它时,您应该认真考虑使用Content-Encoding: gzip发送它。你应该看到巨大的带宽节省。此外,如果这是一个静态文件,您真正想要做的是使用sendfile(2)。至于浏览器在下载内容方面没有达到预期效果,您可能需要查看Content-Disposition标题。无论如何,逻辑是这样的:

如果客户端表明他们可以通过gzip标头处理Accept-Encoding编码(例如Accept-Encoding: compress;q=0.5, gzip;q=1.0Accept-Encoding: gzip;q=1.0, identity; q=0.5, *;q=0或类似),则压缩文件,将压缩结果缓存到某处,为响应(Content-Encoding: gzipContent-Length: nContent-Type: text/plain等)编写正确的标题,然后使用sendfile(2)(但是可能已经提供或未提供您的环境)将打开的文件描述符中的内容复制到您的响应流中。

如果他们不接受gzip,请执行相同的操作,但不要首先进行gzipping。

或者,如果您在服务器前面使用Apache,Lighttpd或类似的透明代理,则可以使用X-Sendfile标头,这非常快:

response.setHeader('Content-Type', 'text/plain')
response.setHeader(
  'Content-Disposition',
  'attachment; filename="' + os.path.basename(fileName) + '"'
)
response.setHeader('X-Sendfile', fileName)
response.setHeader('Content-Length', os.stat(fileName).st_size)

答案 3 :(得分:0)

以下是使用urllib2以块的形式下载文件的示例,您可以在扭曲的函数调用中使用

import os
import urllib2
import math

def downloadChunks(url):
    """Helper to download large files
        the only arg is a url
       this file will go to a temp directory
       the file will also be downloaded
       in chunks and print out how much remains
    """

    baseFile = os.path.basename(url)

    #move the file to a more uniq path
    os.umask(0002)
    temp_path = "/tmp/"
    try:
        file = os.path.join(temp_path,baseFile)

        req = urllib2.urlopen(url)
        total_size = int(req.info().getheader('Content-Length').strip())
        downloaded = 0
        CHUNK = 256 * 10240
        with open(file, 'wb') as fp:
            while True:
                chunk = req.read(CHUNK)
                downloaded += len(chunk)
                print math.floor( (downloaded / total_size) * 100 )
                if not chunk: break
                fp.write(chunk)
    except urllib2.HTTPError, e:
        print "HTTP Error:",e.code , url
        return False
    except urllib2.URLError, e:
        print "URL Error:",e.reason , url
        return False

    return file