Wie lade ich eine Binärdatei über HTTP herunter?

131

Wie lade ich eine Binärdatei über HTTP mit Ruby herunter und speichere sie?

Die URL lautet http://somedomain.net/flv/sample/sample.flv.

Ich bin auf der Windows-Plattform und würde es vorziehen, kein externes Programm auszuführen.

Radek
quelle
Meine Lösung basiert stark auf snippets.dzone.com/posts/show/2469, das angezeigt wurde, nachdem ich den Download von Ruby-Dateien in die FireFox-Adressleiste eingegeben habe. Haben Sie also im Internet recherchiert, bevor Sie diese Frage gestellt haben?
Dawid
@Dejw: Ich habe recherchiert und hier eine beantwortete Frage gefunden. Grundsätzlich mit dem gleichen Code, den Sie mir gegeben haben. Der resp.bodyTeil verwirrt mich. Ich dachte, er würde nur den 'Körper'-Teil der Antwort speichern, aber ich möchte die gesamte / binäre Datei speichern. Ich fand auch, dass rio.rubyforge.org hilfreich sein könnte. Außerdem kann mit meiner Frage niemand sagen, dass eine solche Frage noch nicht beantwortet wurde :-)
Radek
3
Der Körperteil ist genau die ganze Datei. Die Antwort wird aus den Headern (http) und dem Body (der Datei) erstellt. Wenn Sie also den Body speichern, haben Sie die Datei gespeichert ;-)
Dawid
1
Noch eine Frage ... Nehmen wir an, die Datei ist 100 MB groß und der Download-Vorgang wird in der Mitte unterbrochen. Wird etwas gerettet? Kann ich die Datei wieder aufnehmen?
Radek
Leider nicht, da der http.get('...')Anruf eine Anfrage sendet und eine Antwort empfängt (die gesamte Datei). Um eine Datei in Blöcken herunterzuladen und gleichzeitig zu speichern, siehe meine bearbeitete Antwort unten ;-) Das Fortsetzen ist nicht einfach. Vielleicht zählen Sie die gespeicherten Bytes und überspringen sie, wenn Sie die Datei erneut herunterladen ( file.write(resp.body)gibt die Anzahl der geschriebenen Bytes zurück).
Dawid

Antworten:

143

Der einfachste Weg ist die plattformspezifische Lösung:

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

Wahrscheinlich suchen Sie nach:

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

Bearbeiten: Geändert. Danke.

Edit2: Die Lösung, die einen Teil einer Datei beim Herunterladen speichert:

# 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
quelle
15
Ja, ich weiß. Deshalb habe ich gesagt, dass es so ist a platform-specific solution.
Dawid
1
Weitere plattformspezifische Lösungen: GNU / Linux-Plattformen bieten wget. OS X bietet curl( curl http://oh.no/its/pbjellytime.flv --output secretlylove.flv). Windows hat ein Powershell-Äquivalent (new-object System.Net.WebClient).DownloadFile('http://oh.no/its/pbjellytime.flv','C:\tmp\secretlylove.flv'). Binärdateien für wget und curl gibt es für alle Betriebssysteme auch per Download. Ich empfehle weiterhin dringend, die Standardbibliothek zu verwenden, es sei denn, Sie schreiben Code ausschließlich für Ihre eigene Liebe.
fny
1
Der Anfang ... stellt sicher, dass das Ende nicht erforderlich ist, wenn das offene Blockformular verwendet wird. öffne 'sample.flv' do | f | .... f.write segment
lab419
1
Die Nicht-Textdatei kommt beschädigt an.
Paul
1
Ich benutze Chunked Download mit Net::HTTP. Und ich erhalte den Teil der Datei, bekomme aber eine Antwort Net::HTTPOK. Gibt es eine Möglichkeit, um sicherzustellen, dass wir die Datei vollständig heruntergeladen haben?
Nickolay Kondratenko
118

Ich weiß, dass dies eine alte Frage ist, aber Google hat mich hierher geworfen und ich denke, ich habe eine einfachere Antwort gefunden.

In Railscasts Nr. 179 verwendete Ryan Bates die Ruby-Standardklasse OpenURI , um viele der folgenden Fragen zu beantworten :

( Warnung : ungetesteter Code. Möglicherweise müssen Sie ihn ändern / optimieren.)

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
quelle
9
open("http://somedomain.net/flv/sample/sample.flv", 'rb')öffnet die URL im Binärmodus.
Zoli
1
Weiß jemand, ob Open-Uri intelligent darin ist, den Puffer zu füllen, wie @Isa erklärt hat?
Gdelfino
1
@ Gildefino Sie erhalten mehr Antworten, wenn Sie eine neue Frage dazu öffnen. Es ist unwahrscheinlich, dass viele Leute dies lesen (und es ist auch die richtige Sache, um in Stack Overflow zu tun).
Kikito
2
Genial. Ich hatte Probleme mit HTTP=> HTTPSUmleitung und fand heraus, wie man es mit open_uri_redirectionsGem
mathielo
1
FWIW Einige Leute denken, dass Open-Uri gefährlich ist, weil es den gesamten Code, einschließlich des Bibliothekscodes, monkeypatcht, der openmit einer neuen Fähigkeit verwendet wird, die der aufrufende Code möglicherweise nicht vorwegnimmt. Sie sollten openohnehin nicht auf Benutzereingaben vertrauen , aber Sie müssen jetzt doppelt vorsichtig sein.
Methode
42

Hier ist mein Ruby http zur Datei mit 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

Der Hauptvorteil hier ist es prägnant und einfach, weil ein openGroßteil des schweren Hebens erledigt.Und es liest nicht die gesamte Antwort im Speicher.

Die openMethode überträgt Antworten> 1 KB an a Tempfile. Wir können dieses Wissen nutzen, um diese Lean-Download-to-File-Methode zu implementieren. Siehe die OpenURI::BufferImplementierung hier.

Bitte seien Sie vorsichtig mit vom Benutzer bereitgestellten Eingaben! open(name, *rest, &block)ist unsicher, wenn namevon Benutzereingaben kommt!

Overbryd
quelle
4
Dies sollte die akzeptierte Antwort sein, da sie präzise und einfach ist und nicht die gesamte Datei in den Speicher lädt ~ + Leistung (Schätzung hier).
Nikkolasg
Ich stimme Nikkolasg zu. Ich habe gerade versucht, es zu benutzen und es funktioniert sehr gut. Ich habe es ein wenig geändert, zum Beispiel wird der lokale Pfad automatisch von der angegebenen URL abgeleitet, also z. B. "path = nil" und dann nach nil suchen; Wenn es Null ist, verwende ich File.basename () in der URL, um den lokalen Pfad abzuleiten.
Shevy
1
Dies wäre die beste Antwort, aber open-uri lädt die gesamte Datei in den Speicher stackoverflow.com/questions/17454956/…
Simon Perepelitsa
2
@ SimonPerepelitsa hehe. Ich habe es noch einmal überarbeitet und jetzt eine übersichtliche Download-to-File-Methode bereitgestellt, die nicht die gesamte Antwort im Speicher liest . Meine vorherige Antwort wäre ausreichend gewesen, da opendie Antwort tatsächlich nicht im Speicher gelesen wird, sondern für Antworten> 10240 Byte in eine temporäre Datei eingelesen wird. Sie hatten also ein bisschen Recht, aber nicht. Die überarbeitete Antwort räumt dieses Missverständnis auf und dient hoffentlich als
gutes
3
Wenn Sie EACCES: permission deniedbeim Ändern des Dateinamens mit dem mvBefehl eine Fehlermeldung erhalten , müssen Sie die Datei zuerst schließen. Schlagen Sie vor, diesen Teil zu ändernTempfile then io.close;
David Douglas
28

Beispiel 3 in der net / http-Dokumentation von Ruby zeigt, wie Sie ein Dokument über HTTP herunterladen und die Datei ausgeben, anstatt sie nur in den Speicher zu laden. Ersetzen Sie Puts durch ein binäres Schreiben in eine Datei, z. B. wie in Dejws Antwort gezeigt.

Komplexere Fälle werden weiter unten im selben Dokument gezeigt.

Arkku
quelle
+1 für den Hinweis auf vorhandene Dokumentation und weitere Beispiele.
Semperos
1
Hier ist der Link speziell: ruby-doc.org/stdlib-2.1.4/libdoc/net/http/rdoc/Net/…
kgilpin
26

Sie können Open-Uri verwenden, einen Einzeiler

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

Oder mit net / http

require 'net/http'
File.write("file_name", Net::HTTP.get(URI.parse("http://url.com")))
KrauseFx
quelle
10
Dies liest die gesamte Datei in den Speicher, bevor sie auf die Festplatte geschrieben wird. Das kann also schlecht sein.
kgilpin
@ kgilpin beide Lösungen?
KrauseFx
1
Ja, beide Lösungen.
eltiare
Das heißt, wenn du bist OK mit , dass eine kürzere Version (unter der Annahme , URL und Dateinamen sind in Variablen urlund filejeweils) unter Verwendung von open-uriwie in der ersten: File.write(file, open(url).read)... Toten einfach, für den trivialen Download Fall.
Lindes
17

Erweiterung der Antwort von 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 
      }
    }
  }
}

wo filenameundurl sind Saiten.

Der sleepBefehl ist ein Hack, der die CPU-Auslastung drastisch reduzieren kann, wenn das Netzwerk der begrenzende Faktor ist. Net :: HTTP wartet nicht darauf, dass der Puffer (16 KB in Version 1.9.2) gefüllt wird, bevor er nachgibt, sodass die CPU selbst kleine Teile bewegt. Wenn Sie einen Moment lang schlafen, kann sich der Puffer zwischen den Schreibvorgängen füllen, und die CPU-Auslastung ist vergleichbar mit einer Curl-Lösung, die sich in meiner Anwendung um das 4-5-fache unterscheidet. Eine robustere Lösung könnte den Fortschritt von untersuchenf.pos und das Zeitlimit anpassen, um beispielsweise 95% der Puffergröße zu erreichen - tatsächlich habe ich in meinem Beispiel die 0,005-Zahl erhalten.

Entschuldigung, aber ich kenne keine elegantere Möglichkeit, Ruby warten zu lassen, bis sich der Puffer gefüllt hat.

Bearbeiten:

Dies ist eine Version, die sich automatisch anpasst, um den Puffer auf oder unter der Kapazität zu halten. Es ist eine unelegante Lösung, aber es scheint genauso schnell zu sein und so wenig CPU-Zeit zu verbrauchen, wie es zum Locken aufruft.

Es funktioniert in drei Stufen. Eine kurze Lernphase mit einer absichtlich langen Schlafzeit legt die Größe eines vollen Puffers fest. Die Drop-Periode reduziert die Ruhezeit bei jeder Iteration schnell, indem sie mit einem größeren Faktor multipliziert wird, bis ein unterfüllter Puffer gefunden wird. Während der normalen Zeit wird es dann um einen kleineren Faktor nach oben und unten angepasst.

Mein Ruby ist etwas rostig, daher bin ich mir sicher, dass dies verbessert werden kann. Erstens gibt es keine Fehlerbehandlung. Vielleicht könnte es auch in ein Objekt getrennt werden, weg vom Herunterladen selbst, so dass Sie einfach autosleep.sleep(f.pos)in Ihrer Schleife aufrufen würden ? Noch besser ist, dass Net :: HTTP geändert werden könnte, um auf einen vollen Puffer zu warten, bevor es ergibt :-)

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
Ist ein
quelle
Ich mag den sleepHack!
Radek
13

Es gibt api-freundlichere Bibliotheken als Net::HTTPzum Beispiel 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
fguillen
quelle
3

Ich hatte Probleme, wenn die Datei deutsche Umlaute enthielt (ä, ö, ü). Ich könnte das Problem lösen mit:

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

Wenn Sie nach einer Möglichkeit suchen, temporäre Dateien herunterzuladen, Dinge zu tun und sie zu löschen, versuchen Sie dieses Juwel 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
Äquivalent8
quelle