¿Cómo descargo un archivo binario a través de HTTP?

131

¿Cómo descargo y guardo un archivo binario a través de HTTP usando Ruby?

La URL es http://somedomain.net/flv/sample/sample.flv.

Estoy en la plataforma de Windows y preferiría no ejecutar ningún programa externo.

Radek
fuente
Mi solución se basa fuertemente en snippets.dzone.com/posts/show/2469 que apareció después de que escribí la descarga del archivo ruby en la barra de direcciones de FireFox ... ¿investigó en Internet antes de hacer esta pregunta?
Dawid
@Dejw: Investigué y encontré una pregunta respondida aquí. Básicamente con el mismo código que me diste. La resp.bodyparte me confunde. Pensé que solo salvaría parte del cuerpo de la respuesta, pero quiero guardar todo el archivo / binario. También descubrí que rio.rubyforge.org podría ser útil. Además, con mi pregunta, nadie puede decir que esa pregunta aún no fue respondida :-)
Radek
3
La parte del cuerpo es exactamente el archivo completo. La respuesta se crea a partir de encabezados (http) y cuerpo (el archivo), por lo que cuando guarda el cuerpo, guarda el archivo ;-)
Dawid
1
una pregunta más ... digamos que el archivo es de 100 MB y el proceso de descarga se interrumpe en el medio. ¿Habrá algo guardado? ¿Puedo hacer reanudar el archivo?
Radek
Lamentablemente no, porque la http.get('...')llamada envía una solicitud y recibe respuesta (todo el archivo). Para descargar un archivo en fragmentos y guardarlo simultáneamente, vea mi respuesta editada a continuación ;-) Reanudar no es fácil, quizás cuente los bytes que guardó y luego los omita cuando file.write(resp.body)vuelva a descargar el archivo ( devuelve el número de bytes escritos).
Dawid

Respuestas:

143

La forma más simple es la solución específica de la plataforma:

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

Probablemente estés buscando:

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: modificado. Gracias.

Edit2: la solución que guarda parte de un archivo durante la descarga:

# 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
fuente
15
Sí, lo sé. Por eso dije que es así a platform-specific solution.
Dawid
1
Más soluciones específicas de plataforma: las plataformas GNU / Linux proporcionan wget. OS X proporciona curl( curl http://oh.no/its/pbjellytime.flv --output secretlylove.flv). Windows tiene un equivalente de Powershell (new-object System.Net.WebClient).DownloadFile('http://oh.no/its/pbjellytime.flv','C:\tmp\secretlylove.flv'). También existen binarios para wget y curl para todos los sistemas operativos mediante descarga. Todavía recomiendo usar la biblioteca estándar a menos que escriba su código únicamente para su propio amor.
fny
1
el comienzo ... asegúrese ... el final no es necesario si se utiliza el formulario de bloque abierto. abra 'sample.flv' do | f | .... segmento de escritura
lab419
1
El archivo sin texto llega dañado.
Paul
1
Yo uso la descarga fragmentada usando Net::HTTP. Y recibo la parte del archivo pero obtengo respuesta Net::HTTPOK. ¿Hay alguna manera de garantizar que hayamos descargado el archivo por completo?
Nickolay Kondratenko
118

Sé que esta es una vieja pregunta, pero Google me arrojó aquí y creo que encontré una respuesta más simple.

En Railscasts # 179 , Ryan Bates utilizó la clase estándar Ruby OpenURI para hacer mucho de lo que se le pidió de esta manera:

( Advertencia : código no probado. Es posible que deba cambiarlo / 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
kikito
fuente
9
open("http://somedomain.net/flv/sample/sample.flv", 'rb')abrirá la URL en modo binario.
zoli
1
Alguien sabe si open-uri es inteligente para llenar el búfer como explicó @Isa
gdelfino
1
@gildefino Obtendrá más respuestas si abre una nueva pregunta para eso. Es poco probable que muchas personas lo lean (y también es lo apropiado en Stack Overflow).
kikito
2
Increíble. Tuve problemas con la redirección HTTP=> HTTPS, y descubrí cómo resolverla usando open_uri_redirectionsGem
mathielo
1
FWIW algunas personas piensan que open-uri es peligroso porque parchea todo el código, incluido el código de la biblioteca, que se usa opencon una nueva capacidad que el código de llamada podría no anticipar. No debe confiar en la entrada del usuario que se le pasa de opentodos modos, pero debe ser doblemente cuidadoso ahora.
método del
42

Aquí está mi Ruby http para presentar 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

La principal ventaja aquí es conciso y simple, porque openhace gran parte del trabajo pesado. Y no lee toda la respuesta en la memoria.

El openmétodo transmitirá respuestas> 1kb a a Tempfile. Podemos explotar este conocimiento para implementar este método de descarga magra a archivo. Vea la OpenURI::Bufferimplementación aquí.

¡Tenga cuidado con las aportaciones proporcionadas por el usuario! open(name, *rest, &block)no es seguro si nameproviene de la entrada del usuario!

Overbryd
fuente
44
Esta debería ser la respuesta aceptada, ya que es conciso y simple y no carga todo el archivo en la memoria ~ + rendimiento (supongo que aquí).
Nikkolasg
Estoy de acuerdo con Nikkolasg. Intenté usarlo y funciona muy bien. Sin embargo, lo modifiqué un poco, por ejemplo, la ruta local se deducirá automáticamente de la URL dada, por ejemplo, "ruta = nula" y luego buscando nula; si es nulo, entonces uso File.basename () en la url para deducir la ruta local.
shevy
1
Esta sería la mejor respuesta, pero open-uri SI carga el archivo completo en memoria stackoverflow.com/questions/17454956/…
Simon Perepelitsa
2
@SimonPerepelitsa jeje. Lo revisé una vez más y ahora proporciono un método conciso de descarga a archivo que no lee toda la respuesta en la memoria. Mi respuesta anterior habría sido suficiente, porque en openrealidad no lee la respuesta en la memoria, la lee en un archivo temporal para cualquier respuesta> 10240 bytes. Así que eras amable, pero no. La respuesta revisada limpia este malentendido y, con suerte, sirve como un gran ejemplo sobre el poder de Ruby :)
Overbryd
3
Si obtiene un EACCES: permission deniederror al cambiar el nombre del archivo con el mvcomando es porque primero debe cerrar el archivo. Sugerir cambiar esa parte aTempfile then io.close;
David Douglas
28

El ejemplo 3 en la documentación de red / http de Ruby muestra cómo descargar un documento a través de HTTP y generar el archivo en lugar de simplemente cargarlo en la memoria.

Los casos más complejos se muestran más abajo en el mismo documento.

Arkku
fuente
+1 para señalar la documentación existente y otros ejemplos.
semperos
1
Aquí está el enlace específicamente: ruby-doc.org/stdlib-2.1.4/libdoc/net/http/rdoc/Net/…
kgilpin
26

Puedes usar open-uri, que es un trazador de líneas

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")))
KrauseFx
fuente
10
Esto lee todo el archivo en la memoria antes de escribirlo en el disco, así que ... eso puede ser malo.
kgilpin
@kgilpin ambas soluciones?
KrauseFx
1
Sí, ambas soluciones.
eltiare
Dicho esto, si está de acuerdo con eso, una versión más corta (suponiendo que la url y el nombre de archivo estén en variables urly file, respectivamente), usando open-uricomo en el primero: File.write(file, open(url).read)... Muy simple, para el caso de descarga trivial.
lindes
17

Ampliando la respuesta 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 
      }
    }
  }
}

donde filenamey urlson cuerdas.

El sleepcomando es un truco que puede reducir drásticamente el uso de la CPU cuando la red es el factor limitante. Net :: HTTP no espera a que se llene el búfer (16kB en v1.9.2) antes de ceder, por lo que la CPU se ocupa de mover pequeños trozos. Dormir por un momento le da al búfer la oportunidad de llenarse entre escrituras, y el uso de la CPU es comparable a una solución curl, con una diferencia de 4-5x en mi aplicación. Una solución más sólida podría examinar el progreso f.posy ajustar el tiempo de espera para alcanzar, por ejemplo, el 95% del tamaño del búfer; de hecho, así es como obtuve el número 0.005 en mi ejemplo.

Lo siento, pero no conozco una forma más elegante de hacer que Ruby espere a que se llene el búfer.

Editar:

Esta es una versión que se ajusta automáticamente para mantener el búfer a la capacidad o por debajo. Es una solución poco elegante, pero parece ser tan rápida y usar tan poco tiempo de CPU, como se dice que se encrespa.

Funciona en tres etapas. Un breve período de aprendizaje con un tiempo de sueño deliberadamente largo establece el tamaño de un búfer completo. El período de caída reduce el tiempo de suspensión rápidamente con cada iteración, multiplicándolo por un factor mayor, hasta que encuentra un búfer insuficientemente lleno. Luego, durante el período normal, se ajusta hacia arriba y hacia abajo en un factor menor.

Mi Ruby está un poco oxidado, así que estoy seguro de que esto se puede mejorar. En primer lugar, no hay manejo de errores. Además, ¿tal vez podría separarse en un objeto, lejos de la descarga en sí, para que simplemente llame autosleep.sleep(f.pos)a su bucle? Aún mejor, Net :: HTTP podría cambiarse para esperar un búfer completo antes de producir :-)

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
Es un
fuente
Me gusta el sleephack!
Radek
13

Hay más bibliotecas aptasNet::HTTP para api que , por ejemplo, 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
fuente
3

Tuve problemas si el archivo contenía Umlauts alemanes (ä, ö, ü). Podría resolver el problema usando:

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

si está buscando una manera de descargar un archivo temporal, hacer cosas y eliminarlo, pruebe 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
fuente