Como faço para baixar um arquivo binário via HTTP?

131

Como faço para baixar e salvar um arquivo binário em HTTP usando Ruby?

A URL é http://somedomain.net/flv/sample/sample.flv.

Estou na plataforma Windows e prefiro não executar nenhum programa externo.

Radek
fonte
Minha solução é fortemente baseada em snippets.dzone.com/posts/show/2469, que apareceu depois que eu digitei o download do arquivo ruby na barra de endereços do FireFox ... você fez alguma pesquisa na Internet antes de fazer esta pergunta?
Dawid
@ Dejw: Eu pesquisei e encontrei uma pergunta respondida aqui. Basicamente, com o mesmo código que você me deu. A resp.bodyparte está me confundindo. Pensei que salvaria apenas parte do 'corpo' da resposta, mas quero salvar o arquivo inteiro / binário. Também achei que o rio.rubyforge.org poderia ser útil. Além disso, com a minha pergunta ninguém pode dizer que tal questão não foi respondida ainda :-)
Radek
3
A parte do corpo é exatamente o arquivo inteiro. Response é criado a partir de cabeçalhos (http) e do corpo (o arquivo), então quando você salva o corpo que você salvou o arquivo ;-)
Dawid
1
mais uma pergunta ... digamos que o arquivo tenha 100 MB e o processo de download seja interrompido no meio. Haverá algo salvo? Posso retomar o arquivo?
Radek
Infelizmente não, porque a http.get('...')chamada envia uma solicitação e recebe resposta (o arquivo inteiro). Para baixar um arquivo em pedaços e salvá-lo simultaneamente, veja minha resposta editada abaixo ;-) Retomar não é fácil, talvez Você conte os bytes que você salvou e, em seguida, pule-os ao baixar novamente o arquivo ( file.write(resp.body)retorna o número de bytes gravados).
Dawid

Respostas:

143

A maneira mais simples é a solução específica da plataforma:

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

Provavelmente você está procurando:

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."

Editar: alterado. Obrigado.

Edit2: A solução que salva parte de um arquivo durante o download:

# 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
Dawid
fonte
15
Sim eu conheço. Por isso eu disse que é a platform-specific solution.
Dawid
1
Mais soluções específicas da plataforma: as plataformas GNU / Linux fornecem wget. O OS X fornece curl( curl http://oh.no/its/pbjellytime.flv --output secretlylove.flv). O Windows possui um equivalente do PowerShell (new-object System.Net.WebClient).DownloadFile('http://oh.no/its/pbjellytime.flv','C:\tmp\secretlylove.flv'). Existem binários para wget e curl para todos os sistemas operacionais também por download. Eu ainda recomendo usar a biblioteca padrão, a menos que seu código de escrita seja apenas para seu próprio amor.
FNY
1
o início ... garantir ... o fim não é necessário se o formulário de bloco aberto for usado. abra 'sample.flv' do | f | ... segmento f.write
lab419
1
O arquivo não-texto chega corrompido.
Paul
1
Eu uso o download em pedaços usando Net::HTTP. E recebo a parte do arquivo, mas recebo resposta Net::HTTPOK. Existe alguma maneira de garantir o download completo do arquivo?
Nickolay Kondratenko
118

Sei que essa é uma pergunta antiga, mas o Google me jogou aqui e acho que encontrei uma resposta mais simples.

No Railscasts # 179 , Ryan Bates usou a classe padrão Ruby OpenURI para fazer muito do que foi solicitado assim:

( Aviso : código não testado. Pode ser necessário alterá-lo / ajustá-lo.)

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
kikito
fonte
9
open("http://somedomain.net/flv/sample/sample.flv", 'rb')abrirá o URL no modo binário.
Zoli
1
alguém sabe se o open-uri é inteligente em preencher o buffer como o @Isa explicou?
Gdelfino 26/10/12
1
@gildefino Você receberá mais respostas se abrir uma nova pergunta para isso. É improvável que muitas pessoas leiam isso (e também é a coisa apropriada a se fazer no Stack Overflow).
Kikito
2
Impressionante. Eu tive problemas com HTTP=> HTTPSredirecionamento, e descobriu como resolvê-lo usando open_uri_redirectionsGem
mathielo
1
FWIW, algumas pessoas pensam que a uri aberta é perigosa porque monitora todo o código, incluindo o código da biblioteca, que usa opencom uma nova habilidade que o código de chamada pode não prever. Você não deve confiar na entrada do usuário transmitida de openqualquer maneira, mas precisa ser duplamente cuidadoso agora.
método
42

Aqui está o meu http Ruby para arquivar usando open(name, *rest, &block).

require "open-uri"
require "fileutils"

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

A principal vantagem aqui é concisa e simples, porque openfaz grande parte do trabalho pesado. E não lê toda a resposta na memória.

O openmétodo transmitirá respostas> 1kb para a Tempfile. Podemos explorar esse conhecimento para implementar esse método lean de download para arquivo. Veja a OpenURI::Bufferimplementação aqui.

Tenha cuidado com a entrada fornecida pelo usuário! open(name, *rest, &block)é inseguro se namevier da entrada do usuário!

Overbryd
fonte
4
Essa deve ser a resposta aceita, pois é concisa e simples e não carrega o arquivo inteiro na memória ~ + desempenho (estimativa de estimativa aqui).
Nikkolasg
Eu concordo com Nikkolasg. Eu apenas tentei usá-lo e funciona muito bem. Eu o modifiquei um pouco, embora, por exemplo, o caminho local seja deduzido automaticamente da URL fornecida, por exemplo, "path = nil" e, em seguida, verificando se há nada; se for nulo, uso File.basename () no URL para deduzir o caminho local.
shevy
1
Esta seria a melhor resposta, mas open-uri NÃO carregar o arquivo inteiro na memória stackoverflow.com/questions/17454956/...
Simon Perepelitsa
2
@SimonPerepelitsa hehe. Eu a revisei mais uma vez, agora fornecendo um método conciso de download para arquivo que não lê toda a resposta na memória. Minha resposta anterior teria sido suficiente, porque, openna verdade, não lê a resposta na memória, ela a lê em um arquivo temporário para quaisquer respostas> 10240 bytes. Então você estava certo, mas não. A resposta revista limpa este mal-entendido e espero que serve como um grande exemplo do poder do Ruby :)
Overbryd
3
Se você receber um EACCES: permission deniederro ao alterar o nome do arquivo com o mvcomando é porque é necessário fechar o arquivo primeiro. Sugira que mude essa parte paraTempfile then io.close;
David Douglas
28

O exemplo 3 da documentação net / http do Ruby mostra como fazer o download de um documento por HTTP e, como resultado, em vez de apenas carregá-lo na memória, o substituto coloca uma gravação binária em um arquivo, por exemplo, como mostrado na resposta de Dejw.

Casos mais complexos são mostrados mais abaixo no mesmo documento.

Arkku
fonte
+1 para apontar para a documentação existente e mais exemplos.
semper
1
Aqui está o link especificamente: ruby-doc.org/stdlib-2.1.4/libdoc/net/http/rdoc/Net/…
kgilpin 29/04
26

Você pode usar o open-uri, que é um liner

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

Ou usando net / http

require 'net/http'
File.write("file_name", Net::HTTP.get(URI.parse("http://url.com")))
KrauseFx
fonte
10
Isso lê o arquivo inteiro na memória antes de gravá-lo no disco, então ... isso pode ser ruim.
kgilpin
@kgilpin ambas as soluções?
KrauseFx #
1
Sim, ambas as soluções.
Eltiare 17/05
Dito isto, se você concorda com isso, uma versão mais curta (assumindo que URL e nome do arquivo estão em variáveis urle file, respectivamente), usando open-uricomo no primeiro: File.write(file, open(url).read)... Simples, para o caso trivial de download.
Lindes
17

Expandindo a resposta de 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 
      }
    }
  }
}

onde filenameeurl são strings.

O sleepcomando é um hack que pode reduzir drasticamente o uso da CPU quando a rede é o fator limitante. O Net :: HTTP não espera que o buffer (16kB na v1.9.2) seja preenchido antes de render, então a CPU se ocupa movendo pequenos pedaços. Dormir por um momento dá ao buffer a chance de preencher entre gravações e o uso da CPU é comparável a uma solução de curl, diferença de 4-5x no meu aplicativo. Uma solução mais robusta pode examinar o progresso def.pos e ajustar o tempo limite para atingir, digamos, 95% do tamanho do buffer - na verdade, foi assim que obtive o número 0,005 no meu exemplo.

Desculpe, mas não conheço uma maneira mais elegante de fazer com que Ruby aguarde o buffer preencher.

Editar:

Esta é uma versão que se ajusta automaticamente para manter o buffer igual ou inferior à capacidade. É uma solução deselegante, mas parece ser tão rápida e usar tão pouco tempo de CPU, como está chamando a curl.

Funciona em três etapas. Um breve período de aprendizado com um tempo de sono deliberadamente longo estabelece o tamanho de um buffer completo. O período de queda reduz o tempo de suspensão rapidamente a cada iteração, multiplicando-o por um fator maior, até encontrar um buffer insuficiente. Então, durante o período normal, ele se ajusta para cima e para baixo por um fator menor.

Meu Ruby está um pouco enferrujado, então tenho certeza de que isso pode ser melhorado. Primeiro de tudo, não há tratamento de erros. Além disso, talvez ele possa ser separado em um objeto, longe do próprio download, para que você apenas chame autosleep.sleep(f.pos)no seu loop? Melhor ainda, o Net :: HTTP pode ser alterado para aguardar um buffer completo antes de produzir :-)

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
É um
fonte
Eu gosto do sleephack!
Radek
13

Existem mais bibliotecas compatíveis com API do que Net::HTTP, por exemplo, activationparty :

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
fguillen
fonte
3

Eu tive problemas, se o arquivo continha trema alemão (ä, ö, ü). Eu poderia resolver o problema usando:

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

se você estiver procurando uma maneira de baixar um arquivo temporário, faça coisas e exclua-o, tente esta gema 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
equivalente8
fonte