Comment télécharger un fichier binaire via HTTP?

131

Comment télécharger et enregistrer un fichier binaire via HTTP en utilisant Ruby?

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

Je suis sur la plate-forme Windows et je préférerais ne pas exécuter de programme externe.

Radek
la source
Ma solution est fortement basée sur snippets.dzone.com/posts/show/2469 qui est apparu après avoir tapé le téléchargement de fichier ruby dans la barre d'adresse FireFox ... alors avez-vous fait des recherches sur Internet avant de poser cette question?
Dawid
@Dejw: J'ai fait des recherches et j'ai trouvé une réponse ici. Fondamentalement, avec le même code que vous m'avez donné. La resp.bodypartie me déroute, je pensais que cela ne sauverait que la partie «corps» de la réponse mais je veux enregistrer le fichier entier / binaire. J'ai également trouvé que rio.rubyforge.org pouvait être utile. De plus, avec ma question, personne ne peut dire qu'une telle question n'a pas encore été répondue :-)
Radek
3
La partie du corps est exactement le fichier entier. La réponse est créée à partir des en-têtes (http) et du corps (le fichier), donc lorsque vous enregistrez le corps, vous avez enregistré le fichier ;-)
Dawid
1
une autre question ... disons que le fichier fait 100 Mo et que le processus de téléchargement est interrompu au milieu. Y aura-t-il quelque chose de sauvé? Puis-je reprendre le fichier?
Radek
Malheureusement non, car l' http.get('...')appel envoie une requête et reçoit une réponse (le fichier entier). Pour télécharger un fichier en morceaux et l'enregistrer simultanément, voir ma réponse modifiée ci-dessous ;-) La reprise n'est pas facile, peut-être que vous comptez les octets que vous avez enregistrés et que vous les ignorez lorsque vous retéléchargez le fichier ( file.write(resp.body)retourne le nombre d'octets écrits).
Dawid

Réponses:

143

Le moyen le plus simple est la solution spécifique à la plateforme:

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

Vous recherchez probablement:

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

Edit: changé. Merci.

Edit2: La solution qui enregistre une partie d'un fichier lors du téléchargement:

# 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
la source
15
Oui je sais. C'est pourquoi j'ai dit que c'était le cas a platform-specific solution.
Dawid
1
Des solutions plus spécifiques aux plates-formes: les plates-formes GNU / Linux fournissent wget. OS X fournit curl( curl http://oh.no/its/pbjellytime.flv --output secretlylove.flv). Windows a un équivalent Powershell (new-object System.Net.WebClient).DownloadFile('http://oh.no/its/pbjellytime.flv','C:\tmp\secretlylove.flv'). Des fichiers binaires pour wget et curl existent également pour tous les systèmes d'exploitation via téléchargement. Je recommande toujours fortement d'utiliser la bibliothèque standard à moins que votre code d'écriture soit uniquement pour votre propre amour.
fny
1
le début ... assurer ... la fin n'est pas nécessaire si le formulaire de bloc ouvert est utilisé. ouvrez 'sample.flv' faire | f | .... f.write segment
lab419
1
Le fichier non texte arrive corrompu.
Paul
1
J'utilise le téléchargement fragmenté en utilisant Net::HTTP. Et je reçois la partie du fichier mais j'obtiens une réponse Net::HTTPOK. Existe-t-il un moyen de nous assurer que nous avons complètement téléchargé le fichier?
Nickolay Kondratenko
118

Je sais que c'est une vieille question, mais Google m'a jeté ici et je pense avoir trouvé une réponse plus simple.

Dans Railscasts # 179 , Ryan Bates a utilisé la classe standard Ruby OpenURI pour faire une grande partie de ce qui a été demandé comme ceci:

( Avertissement : code non testé. Vous devrez peut-être le modifier / le peaufiner.)

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
la source
9
open("http://somedomain.net/flv/sample/sample.flv", 'rb')ouvrira l'URL en mode binaire.
zoli
1
quelqu'un sait si open-uri est intelligent pour remplir le tampon comme @Isa l'a expliqué?
gdelfino
1
@gildefino Vous obtiendrez plus de réponses si vous ouvrez une nouvelle question pour cela. Il est peu probable que beaucoup de gens lisent ceci (et c'est aussi la chose appropriée à faire dans Stack Overflow).
kikito
2
Impressionnant. J'ai eu des problèmes avec HTTP=> HTTPSredirection, et j'ai découvert comment le résoudre en utilisant open_uri_redirectionsGem
mathielo
1
FWIW certaines personnes pensent que open-uri est dangereux car il monkeypatche tout le code, y compris le code de bibliothèque, qui utilise openune nouvelle capacité que le code appelant pourrait ne pas anticiper. Vous ne devriez pas faire confiance aux entrées utilisateur transmises de opentoute façon, mais vous devez être doublement prudent maintenant.
méthode
42

Voici mon Ruby http à déposer en utilisant 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

Le principal avantage ici est concis et simple, car openfait une grande partie du travail lourd.Et il ne lit pas toute la réponse en mémoire.

La openméthode diffusera les réponses> 1 ko vers un fichier Tempfile. Nous pouvons exploiter ces connaissances pour implémenter cette méthode de téléchargement simplifié dans un fichier. Voir leOpenURI::Buffer implémentation ici.

Soyez prudent avec les entrées fournies par l'utilisateur! open(name, *rest, &block)est dangereux s'il nameprovient d'une entrée utilisateur!

Overbryd
la source
4
Cela devrait être la réponse acceptée car elle est concise et simple et ne charge pas le fichier entier en mémoire ~ + performances (estimation ici).
Nikkolasg le
Je suis d'accord avec Nikkolasg. J'ai juste essayé de l'utiliser et cela fonctionne très bien. Je l'ai un peu modifié cependant, par exemple, le chemin local sera déduit automatiquement de l'URL donnée, donc par exemple "path = nil" puis en vérifiant nil; s'il est nul, j'utilise File.basename () sur l'url pour déduire le chemin local.
shevy
1
Ce serait la meilleure réponse, mais ouvert uri NE charger tout le fichier en mémoire stackoverflow.com/questions/17454956/...
Simon Perepelitsa
2
@SimonPerepelitsa hehe. Je l'ai encore révisé, fournissant maintenant une méthode de téléchargement concise dans un fichier qui ne lit pas toute la réponse en mémoire. Ma réponse précédente aurait été suffisante, car openen fait ne lit pas la réponse en mémoire, il la lit dans un fichier temporaire pour toute réponse> 10240 octets. Vous aviez donc raison, mais pas. La réponse révisée nettoie ce malentendu et sert, espérons-le, de bon exemple sur la puissance de Ruby :)
Overbryd
3
Si vous obtenez une EACCES: permission deniederreur lors de la modification du nom de fichier avec la mvcommande, c'est parce que vous devez d'abord fermer le fichier. Suggérer de changer cette partie enTempfile then io.close;
David Douglas
28

Exemple 3 dans la documentation de Ruby's net / http montre comment télécharger un document via HTTP, et pour sortir le fichier au lieu de simplement le charger en mémoire, remplacez-le par une écriture binaire dans un fichier, par exemple comme indiqué dans la réponse de Dejw.

Des cas plus complexes sont présentés plus bas dans le même document.

Arkku
la source
+1 pour pointer vers la documentation existante et d'autres exemples.
semperos
1
Voici le lien spécifiquement: ruby-doc.org/stdlib-2.1.4/libdoc/net/http/rdoc/Net/…
kgilpin
26

Vous pouvez utiliser open-uri, qui est une ligne unique

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

Ou en utilisant net / http

require 'net/http'
File.write("file_name", Net::HTTP.get(URI.parse("http://url.com")))
KrauseFx
la source
10
Cela lit le fichier entier en mémoire avant de l'écrire sur le disque, donc ... cela peut être mauvais.
kgilpin
@kgilpin les deux solutions?
KrauseFx
1
Oui, les deux solutions.
eltiare
Cela dit, si cela vous convient, une version plus courte (en supposant que l'url et le nom de fichier sont dans des variables urlet file, respectivement), en utilisant open-uricomme dans la première: File.write(file, open(url).read)... Dead simple, pour le cas de téléchargement trivial.
lindes
17

Développement sur la réponse 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 
      }
    }
  }
}

filenameeturl sont des chaînes.

La sleepcommande est un hack qui peut réduire considérablement l'utilisation du processeur lorsque le réseau est le facteur limitant. Net :: HTTP n'attend pas que le tampon (16 Ko dans la v1.9.2) se remplisse avant de céder, donc le processeur s'emploie à déplacer de petits morceaux. Dormir pendant un moment donne au tampon une chance de se remplir entre les écritures, et l'utilisation du processeur est comparable à une solution curl, différence 4-5x dans mon application. Une solution plus robuste pourrait examiner les progrèsf.pos et ajuster le délai d'expiration pour cibler, par exemple, 95% de la taille de la mémoire tampon - en fait, c'est ainsi que j'ai obtenu le nombre 0,005 dans mon exemple.

Désolé, mais je ne connais pas une manière plus élégante de faire attendre Ruby que le tampon se remplisse.

Éditer:

Il s'agit d'une version qui s'ajuste automatiquement pour garder le tampon juste à sa capacité ou en dessous. C'est une solution inélégante, mais elle semble être tout aussi rapide et utiliser aussi peu de temps CPU, car elle appelle à curl.

Cela fonctionne en trois étapes. Une brève période d'apprentissage avec un temps de sommeil délibérément long détermine la taille d'un tampon plein. La période de suppression réduit rapidement le temps de sommeil à chaque itération, en le multipliant par un facteur plus grand, jusqu'à ce qu'il trouve un tampon sous-rempli. Ensuite, pendant la période normale, il s'ajuste vers le haut et vers le bas par un facteur plus petit.

Mon Ruby est un peu rouillé, donc je suis sûr que cela peut être amélioré. Tout d'abord, il n'y a pas de gestion des erreurs. Aussi, peut-être qu'il pourrait être séparé en un objet, loin du téléchargement lui-même, de sorte que vous l'appeliez simplement autosleep.sleep(f.pos)dans votre boucle? Mieux encore, Net :: HTTP pourrait être modifié pour attendre un tampon plein avant de céder :-)

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
Est un
la source
J'aime le sleephack!
Radek
13

Il existe plus de bibliothèques compatibles avec les API que Net::HTTP, par exemple, 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
la source
3

J'ai eu des problèmes, si le fichier contenait des Umlauts allemands (ä, ö, ü). Je pourrais résoudre le problème en utilisant:

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

si vous cherchez un moyen de télécharger un fichier temporaire, faites des choses et supprimez-le, essayez cette gemme 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
la source