Come faccio a scaricare un file binario su HTTP?


131

Come faccio a scaricare e salvare un file binario su HTTP utilizzando Ruby?

L'URL è http://somedomain.net/flv/sample/sample.flv.

Sono sulla piattaforma Windows e preferirei non eseguire alcun programma esterno.


La mia soluzione è fortemente basata su snippets.dzone.com/posts/show/2469 che è apparso dopo aver digitato il download di file ruby nella barra degli indirizzi di FireFox ... quindi hai fatto qualche ricerca su Internet prima di porre questa domanda?
Dawid,

@Dejw: ho fatto delle ricerche e ho trovato una domanda con risposta qui. Fondamentalmente con lo stesso codice che mi hai dato. La resp.bodyparte mi confonde, ho pensato che avrebbe salvato solo la parte "body" della risposta, ma voglio salvare l'intero file / binario. Ho anche scoperto che rio.rubyforge.org potrebbe essere utile. Inoltre con la mia domanda nessuno può dire che a tale domanda non sia stata ancora data risposta :-)
Radek,

3
La parte del corpo è esattamente l'intero file. La risposta viene creata dalle intestazioni (http) e dal corpo (il file), quindi quando salvi il corpo hai salvato il file ;-)
Dawid,

1
un'altra domanda ... diciamo che il file è grande 100 MB e il processo di download viene interrotto nel mezzo. Ci sarà qualcosa di salvato? Posso fare riprendere il file?
Radek,

Sfortunatamente no, perché http.get('...')call invia una richiesta e riceve risposta (l'intero file). Per scaricare un file in blocchi e salvarlo contemporaneamente vedi la mia risposta modificata di seguito ;-) Riprendere non è facile, forse conti i byte salvati e poi saltali quando scarichi di nuovo il file ( file.write(resp.body)restituisce il numero di byte scritti).
Dawid,

Risposte:


143

Il modo più semplice è la soluzione specifica per la piattaforma:

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

Probabilmente stai cercando:

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

Modifica: modificato. Grazie.

Edit2: la soluzione che salva parte di un file durante il 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

15
Si, lo so. Ecco perché ho detto che lo è a platform-specific solution.
Dawid,

1
Soluzioni più specifiche per piattaforma: piattaforme GNU / Linux forniscono wget. OS X fornisce curl( curl http://oh.no/its/pbjellytime.flv --output secretlylove.flv). Windows ha un equivalente di Powershell (new-object System.Net.WebClient).DownloadFile('http://oh.no/its/pbjellytime.flv','C:\tmp\secretlylove.flv'). I binari per wget e curl esistono anche per tutto il sistema operativo tramite download. Consiglio vivamente di utilizzare la libreria standard a meno che il tuo codice di scrittura non sia solo per il tuo amore.
fny

1
l'inizio ... assicurarsi che ... fine non sia necessario se si utilizza il modulo di blocco aperto. apri 'sample.flv' do | f | .... f.write segment
lab419

1
Il file non di testo arriva danneggiato.
Paul,

1
Uso il download in blocco utilizzando Net::HTTP. E ricevo la parte del file ma ottengo una risposta Net::HTTPOK. C'è un modo per garantire che abbiamo scaricato completamente il file?
Nickolay Kondratenko il

118

So che questa è una vecchia domanda, ma Google mi ha lanciato qui e penso di aver trovato una risposta più semplice.

In Railscasts # 179 , Ryan Bates ha usato la classe standard Ruby OpenURI per fare gran parte di ciò che è stato chiesto in questo modo:

( Avviso : codice non testato. Potrebbe essere necessario modificarlo / modificarlo.)

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

9
open("http://somedomain.net/flv/sample/sample.flv", 'rb')aprirà l'URL in modalità binaria.
zoli,

1
qualcuno sa se open-uri è intelligente nel riempire il buffer come ha spiegato @Isa?
gdelfino,

1
@gildefino Riceverai più risposte se apri una nuova domanda per questo. È improbabile che molte persone lo leggano (ed è anche la cosa appropriata da fare in Stack Overflow).
kikito,

2
Eccezionale. Ho avuto problemi con HTTP=> HTTPSreindirizzamento e ho scoperto come risolverlo usando open_uri_redirectionsGem
mathielo

1
FWIW alcune persone pensano che open-uri sia pericoloso perché monkeypatch tutto il codice, incluso il codice della libreria, che utilizza opencon una nuova capacità che il codice chiamante potrebbe non prevedere. Non dovresti fidarti che l'input dell'utente sia passato opencomunque, ma ora devi essere doppiamente attento.
metodo

42

Ecco il mio ruby ​​http per file 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

Il vantaggio principale qui è conciso e semplice, perché openfa gran parte del sollevamento pesante. E non legge l'intera risposta in memoria.

Il openmetodo eseguirà lo streaming delle risposte> 1kb a a Tempfile. Siamo in grado di sfruttare questa conoscenza per implementare questo metodo di download snello su file. Vedi l' OpenURI::Bufferimplementazione qui.

Si prega di fare attenzione con l'input fornito dall'utente! open(name, *rest, &block)non è sicuro se nameproviene dall'input dell'utente!


4
Questa dovrebbe essere la risposta accettata in quanto è concisa e semplice e non carica l'intero file in memoria ~ + prestazioni (indovinare qui).
Nikkolasg,

Sono d'accordo con Nikkolasg. Ho appena provato a usarlo e funziona molto bene. L'ho modificato un po ', tuttavia, ad esempio, il percorso locale verrà dedotto automaticamente dall'URL indicato, quindi ad esempio "percorso = zero" e quindi verifica la presenza di zero; se è zero, allora uso File.basename () sull'URL per dedurre il percorso locale.
Shevy

1
Questa sarebbe la risposta migliore, ma aperto-uri NON caricare l'intero file in memoria stackoverflow.com/questions/17454956/...
Simon Perepelitsa

2
@SimonPerepelitsa hehe. L'ho rivisto ancora una volta, fornendo ora un metodo conciso di download su file che non legge l'intera risposta in memoria. La mia risposta precedente sarebbe stata sufficiente, perché in openrealtà non legge la risposta in memoria, la legge in un file temporaneo per eventuali risposte> 10240 byte. Quindi avevi ragione, ma no. La risposta rivista ripulisce questo malinteso e si spera sia un ottimo esempio del potere di Ruby :)
Overbryd

3
Se viene visualizzato un EACCES: permission deniederrore quando si modifica il nome file con il mvcomando perché è necessario chiudere prima il file. Suggerisci di cambiare quella parte inTempfile then io.close;
David Douglas il

28

L'esempio 3 nella documentazione net / http di Ruby mostra come scaricare un documento su HTTP e come produrre il file invece di caricarlo semplicemente in memoria, i sostituti inseriscono una scrittura binaria in un file, ad esempio come mostrato nella risposta di Dejw.

Casi più complessi sono mostrati più in basso nello stesso documento.


+1 per indicare la documentazione esistente e ulteriori esempi.
Semperos,


26

Puoi usare open-uri, che è una fodera

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

O usando net / http

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

10
Questo legge l'intero file in memoria prima di scriverlo sul disco, quindi ... questo può essere negativo.
kgilpin,

@kgilpin entrambe le soluzioni?
KrauseFx,

1
Sì, entrambe le soluzioni.
eltiare,

Detto questo, se stai bene, una versione più breve (supponendo che url e nome file siano in variabili url e file, rispettivamente), usando open-uricome nel primo: File.write(file, open(url).read)... Semplicemente morto, per il banale caso di download.
cade il

17

Espandendo la risposta di Dejw (modifica2):

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 
      }
    }
  }
}

dove filenamee urlsono stringhe.

Il sleepcomando è un hack che può ridurre drasticamente l'utilizzo della CPU quando la rete è il fattore limitante. Net :: HTTP non attende che il buffer (16kB in v1.9.2) si riempia prima di cedere, quindi la CPU si impegna a spostare piccoli pezzi in giro. La sospensione per un momento offre al buffer la possibilità di riempire tra le scritture e l'utilizzo della CPU è paragonabile a una soluzione di arricciatura, con una differenza di 4-5 volte nella mia applicazione. Una soluzione più solida potrebbe esaminare l'avanzamento f.pose regolare il timeout per target, diciamo, il 95% della dimensione del buffer - in effetti è così che ho ottenuto il numero 0,005 nel mio esempio.

Scusa, ma non conosco un modo più elegante di far aspettare Ruby per riempire il buffer.

Modificare:

Questa è una versione che si regola automaticamente per mantenere il buffer appena o al di sotto della capacità. È una soluzione non elegante, ma sembra essere altrettanto veloce e utilizza meno tempo della CPU, come sta chiamando per arricciarsi.

Funziona in tre fasi. Un breve periodo di apprendimento con un tempo di sonno deliberatamente lungo stabilisce le dimensioni di un buffer completo. Il periodo di rilascio riduce rapidamente il tempo di sospensione a ogni iterazione, moltiplicandolo per un fattore maggiore, fino a quando non trova un buffer insufficiente. Quindi, durante il periodo normale, si regola su e giù di un fattore minore.

Il mio Ruby è un po 'arrugginito, quindi sono sicuro che questo può essere migliorato. Prima di tutto, non c'è gestione degli errori. Inoltre, forse potrebbe essere separato in un oggetto, lontano dal download stesso, in modo da chiamare semplicemente autosleep.sleep(f.pos)nel tuo loop? Ancora meglio, Net :: HTTP potrebbe essere modificato per attendere un buffer completo prima di cedere :-)

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

Mi piace l' sleephack!
Radek,

13

Esistono più librerie compatibili con api di Net::HTTP, ad esempio 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

3

Ho avuto problemi, se il file conteneva German Umlauts (ä, ö, ü). Potrei risolvere il problema utilizzando:

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

0

se stai cercando un modo per scaricare un file temporaneo, fai cose ed eliminalo prova questo gioiello 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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.