【问题标题】:HTTP Download very Big FileHTTP 下载非常大的文件
【发布时间】:2010-12-05 01:09:15
【问题描述】:

我正在使用 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-16k 左右的值来找到最适合您的情况的值。
  • 您愿意接受其中一个答案吗?

标签: python http download twisted


【解决方案1】:

这是一个使用 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

【讨论】:

    【解决方案2】:

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

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

    请记住,Twisted 使用协作式多任务处理来实现任何类型的并发。所以这个 sn-p 的第一个问题是它是整个文件内容的 while 循环(你说它很大)。这意味着整个文件将在进程中发生任何事情之前被读入内存并写入响应。在这种情况下,“anything”还包括将内存缓冲区中的字节推送到网络上,因此您的代码也会一次将整个文件保存在内存中,然后才开始获取当这个循环完成时摆脱它。

    因此,作为一般规则,您不应该编写用于基于 Twisted 的应用程序的代码,该应用程序使用这样的循环来完成大量工作。相反,您需要以与事件循环合作的方式完成大工作的每一小部分。对于通过网络发送文件,最好的方法是使用 producers 和 consumers。这是两个相关的 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 秒内的Twisted Web”系列(特别是“异步响应”条目)。

    【讨论】:

    • 感谢生产者/消费者的提示。
    【解决方案3】:

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

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

    如果他们不接受gzip,做同样的事情,但不要先压缩。

    或者,如果您在服务器前使用 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)
    

    【讨论】:

      【解决方案4】:

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

      【讨论】:

      • 由于我只向浏览器发送文件的内容,Content-Lenth 正是文件的字节大小?
      猜你喜欢
      • 2012-03-28
      • 2016-03-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-07
      • 1970-01-01
      • 2020-12-16
      • 1970-01-01
      相关资源
      最近更新 更多