如何通过 HTTP 下载二进制文件?

Posted

技术标签:

【中文标题】如何通过 HTTP 下载二进制文件?【英文标题】:How do I download a binary file over HTTP? 【发布时间】:2011-01-16 20:07:42 【问题描述】:

如何使用 Ruby 通过 HTTP 下载和保存二进制文件?

网址是http://somedomain.net/flv/sample/sample.flv

我在 Windows 平台上,我不想运行任何外部程序。

【问题讨论】:

我的解决方案强烈基于snippets.dzone.com/posts/show/2469,它出现在我在 FireFox 地址栏中键入 ruby 文件下载 之后出现......所以你在询问之前是否在互联网上进行了任何研究这个问题? @Dejw:我做了研究,在这里找到了一个已回答的问题。基本上使用您给我的相同代码。 resp.body 部分让我感到困惑,我认为它只会保存响应的“正文”部分,但我想保存整个/二进制文件。我还发现rio.rubyforge.org 可能会有所帮助。此外,对于我的问题,没有人能说这样的问题还没有得到回答:-) 正文部分就是整个文件。响应是从标头(http)和正文(文件)创建的,因此当您保存正文时,您保存了文件;-) 还有一个问题...假设文件有 100MB 大,下载过程在中间被中断。会有什么得救吗?我可以恢复文件吗? 很遗憾没有,因为http.get('...') 调用发送请求并接收响应(整个文件)。要分块下载文件并同时保存,请参阅下面我编辑的答案;-) 恢复并不容易,也许您计算字节数您保存然后在重新下载文件时跳过它们(file.write(resp.body) 返回写入的字节数)。 【参考方案1】:

最简单的方法是平台特定的解决方案:

 #!/usr/bin/env ruby
`wget http://somedomain.net/flv/sample/sample.flv`

您可能正在搜索:

require 'net/http'
# Must be somedomain.net instead of somedomain.net/, otherwise, it will throw exception.
Net::HTTP.start("somedomain.net") do |http|
    resp = http.get("/flv/sample/sample.flv")
    open("sample.flv", "wb") do |file|
        file.write(resp.body)
    end
end
puts "Done."

编辑:已更改。谢谢。

Edit2:下载时保存部分文件的解决方案:

# instead of http.get
f = open('sample.flv')
begin
    http.request_get('/sample.flv') do |resp|
        resp.read_body do |segment|
            f.write(segment)
        end
    end
ensure
    f.close()
end

【讨论】:

是的,我知道。这就是为什么我说它是a platform-specific solution 更多特定于平台的解决方案:GNU/Linux 平台提供wget。 OS X 提供curl (curl http://oh.no/its/pbjellytime.flv --output secretlylove.flv)。 Windows 有一个等效的 Powershell (new-object System.Net.WebClient).DownloadFile('http://oh.no/its/pbjellytime.flv','C:\tmp\secretlylove.flv')。 wget 和 curl 的二进制文件也通过下载存在于所有操作系统中。我仍然强烈建议您使用标准库,除非您编写代码只是为了自己的爱好。 如果使用开放块形式,则不需要开始...确保...结束。打开 'sample.flv' 做 |f| .... f.write 段 非文本文件到达时已损坏。 我使用Net::HTTP进行分块下载。我收到了文件的一部分,但得到了回复Net::HTTPOK。有什么方法可以确保我们完全下载了文件?【参考方案2】:

Ruby 的net/http documentation 中的示例 3 显示了如何通过 HTTP 下载文档,并输出文件而不是仅仅将其加载到内存中,用二进制写入文件替换 puts,例如如 Dejw 的回答所示。

更复杂的案例在同一个文档中进一步显示。

【讨论】:

+1 用于指向现有文档和更多示例。 这里是具体的链接:ruby-doc.org/stdlib-2.1.4/libdoc/net/http/rdoc/Net/…【参考方案3】:

扩展 Dejw 的答案(edit2):

File.open(filename,'w') |f|
  uri = URI.parse(url)
  Net::HTTP.start(uri.host,uri.port) |http| 
    http.request_get(uri.path) |res| 
      res.read_body |seg|
        f << seg
#hack -- adjust to suit:
        sleep 0.005 
      
    
  

其中filenameurl 是字符串。

sleep 命令是一种在网络成为限制因素时可以显着降低 CPU 使用率的 hack。 Net::HTTP 不会等待缓冲区(v1.9.2 中为 16kB)在屈服之前填满,因此 CPU 忙于自己移动小块。休眠片刻让缓冲区有机会在写入之间填充,CPU 使用率与 curl 解决方案相当,在我的应用程序中相差 4-5 倍。一个更强大的解决方案可能会检查 f.pos 的进度,并将超时调整为目标,例如,缓冲区大小的 95%——事实上,这就是我在示例中得到 0.005 数字的方式。

抱歉,我不知道让 Ruby 等待缓冲区填满的更优雅的方法。

编辑:

这是一个自动调整自身以使缓冲区保持在容量或低于容量的版本。这是一个不优雅的解决方案,但它似乎和卷曲一样快,并且使用的 CPU 时间也一样少。

它分三个阶段工作。带有故意长睡眠时间的短暂学习期确定了完整缓冲区的大小。丢弃周期通过将其乘以一个更大的因子来快速减少每次迭代的睡眠时间,直到它找到一个未填充的缓冲区。然后,在正常期间,它会上下调整一个较小的系数。

我的 Ruby 有点生锈了,所以我相信这可以改进。首先,没有错误处理。另外,也许它可以被分离成一个对象,远离下载本身,这样你就可以在你的循环中调用autosleep.sleep(f.pos)?更好的是,Net::HTTP 可以更改为在产生之前等待一个完整的缓冲区:-)

def http_to_file(filename,url,opt=)
  opt = 
    :init_pause => 0.1,    #start by waiting this long each time
                           # it's deliberately long so we can see 
                           # what a full buffer looks like
    :learn_period => 0.3,  #keep the initial pause for at least this many seconds
    :drop => 1.5,          #fast reducing factor to find roughly optimized pause time
    :adjust => 1.05        #during the normal period, adjust up or down by this factor
  .merge(opt)
  pause = opt[:init_pause]
  learn = 1 + (opt[:learn_period]/pause).to_i
  drop_period = true
  delta = 0
  max_delta = 0
  last_pos = 0
  File.open(filename,'w') |f|
    uri = URI.parse(url)
    Net::HTTP.start(uri.host,uri.port) |http|
      http.request_get(uri.path) |res|
        res.read_body |seg|
          f << seg
          delta = f.pos - last_pos
          last_pos += delta
          if delta > max_delta then max_delta = delta end
          if learn <= 0 then
            learn -= 1
          elsif delta == max_delta then
            if drop_period then
              pause /= opt[:drop_factor]
            else
              pause /= opt[:adjust]
            end
          elsif delta < max_delta then
            drop_period = false
            pause *= opt[:adjust]
          end
          sleep(pause)
        
      
    
  
end

【讨论】:

【参考方案4】:

如果文件包含德语变音符号 (ä,ö,ü),我遇到了问题。我可以通过以下方式解决问题:

ec = Encoding::Converter.new('iso-8859-1', 'utf-8')
...
f << ec.convert(seg)
...

【讨论】:

【参考方案5】:

我知道这是一个老问题,但谷歌把我扔到这里,我想我找到了一个更简单的答案。

在 Railscasts #179 中,Ryan Bates 使用 Ruby 标准类 OpenURI 完成了很多这样的要求:

警告:未经测试的代码。您可能需要更改/调整它。)

require 'open-uri'

File.open("/my/local/path/sample.flv", "wb") do |saved_file|
  # the following "open" is provided by open-uri
  open("http://somedomain.net/flv/sample/sample.flv", "rb") do |read_file|
    saved_file.write(read_file.read)
  end
end

【讨论】:

open("http://somedomain.net/flv/sample/sample.flv", 'rb') 将以二进制模式打开 URL。 任何人都知道 open-uri 是否像@Isa 解释的那样聪明地填充缓冲区? @gildefino 如果您为此打开一个新问题,您将获得更多答案。很多人不太可能会读到这篇文章(这也是 Stack Overflow 中合适的做法)。 太棒了。我遇到了HTTP => HTTPS 重定向的问题,发现how to solve it 使用open_uri_redirections Gem FWIW 一些人认为 open-uri 很危险,因为它会对所有使用 open 的代码(包括库代码)进行猴子补丁,并提供调用代码可能无法预料的新功能。无论如何,您不应该相信传递给open 的用户输入,但现在您需要加倍小心。【参考方案6】:

有比Net::HTTP更多的api友好库,例如httparty:

require "httparty"
File.open("/tmp/my_file.flv", "wb") do |f| 
  f.write HTTParty.get("http://somedomain.net/flv/sample/sample.flv").parsed_response
end

【讨论】:

【参考方案7】:

以下解决方案将首先将整个内容读入内存,然后再将其写入磁盘(要获得更高效的 i/o 解决方案,请查看其他答案)。

你可以使用open-uri,这是一个单行

require 'open-uri'
content = open('http://example.com').read

或者通过使用 net/http

require 'net/http'
File.write("file_name", Net::HTTP.get(URI.parse("http://url.com")))

【讨论】:

这会将整个文件读入内存,然后再将其写入磁盘,所以......这可能很糟糕。 @kgilpin 两种解决方案? 也就是说,如果您对此表示满意,则可以使用较短的版本(假设 url 和文件名分别在变量 urlfile 中),使用 open-uri,如第一个: File.write(file, open(url).read)... 非常简单,对于琐碎的下载案例。【参考方案8】:

这是我使用 open(name, *rest, &amp;block) 的 Ruby http 文件。

require "open-uri"
require "fileutils"

def download(url, path)
  case io = open(url)
  when StringIO then File.open(path, 'w')  |f| f.write(io.read) 
  when Tempfile then io.close; FileUtils.mv(io.path, path)
  end
end

这里的主要优点是简洁明了,因为open 做了很多繁重的工作。 并且它不会读取内存中的整个响应。

open 方法会将大于 1kb 的响应流式传输到 Tempfile。我们可以利用这些知识来实现​​这种精益下载到文件的方法。 请在此处查看OpenURI::Buffer implementation。

请注意用户提供的输入! 如果name 来自用户输入,open(name, *rest, &amp;block) 是不安全的!

使用OpenURI::open_uri 避免从磁盘读取文件:

...
case io = OpenURI::open_uri(url)
...

【讨论】:

这应该是公认的答案,因为它简洁且不会将整个文件加载到内存中〜+性能(在这里猜测)。 我同意 Nikkolasg。我只是尝试使用它并且效果很好。我对其进行了一些修改,例如,本地路径将自动从给定的 URL 中推导出来,所以 e。 G。 "path = nil" 然后检查 nil;如果是 nil,那么我在 url 上使用 File.basename() 来推断本地路径。 这将是最好的答案,但是 open-uri DOES 将整个文件加载到内存中 ***.com/questions/17454956/… @SimonPerepelitsa 呵呵。我再次对其进行了修改,现在提供了一种简洁的下载到文件方法,该方法不会读取内存中的整个响应。我之前的回答就足够了,因为open 实际上并没有读取内存中的响应,它会将其读入一个临时文件以获取任何大于 10240 字节的响应。所以你是对的,但不是。修改后的答案消除了这种误解,并希望成为 Ruby 强大功能的一个很好的例子:) 如果您在使用mv 命令更改文件名时收到EACCES: permission denied 错误,这是因为您必须先关闭文件。建议将该部分更改为Tempfile then io.close;【参考方案9】:

如果您正在寻找一种方法来下载临时文件,做一些事情并删除它,试试这个 gem https://github.com/equivalent/pull_tempfile

require 'pull_tempfile'

PullTempfile.transaction(url: 'https://mycompany.org/stupid-csv-report.csv', original_filename: 'dont-care.csv') do |tmp_file|
  CSV.foreach(tmp_file.path) do |row|
    # ....
  end
end

【讨论】:

以上是关于如何通过 HTTP 下载二进制文件?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 MicroPython 中下载带有 urequests 的二进制文件?

如何使用C / C ++套接字从HTTP读取二进制文件

Android下载二进制文件问题

下载前如何获取文件大小

(0.2.2)如何下载mysql数据库(二进制RPM源码YUM源)

使用 Java 从 Github 下载二进制文件