Jak pobrać plik binarny przez HTTP?

134

Jak pobrać i zapisać plik binarny przez HTTP przy użyciu Rubiego?

Adres URL to http://somedomain.net/flv/sample/sample.flv.

Pracuję na platformie Windows i wolałbym nie uruchamiać żadnego zewnętrznego programu.

Radek
źródło
Moje rozwiązanie jest silnie oparte na snippets.dzone.com/posts/show/2469, które pojawiło się po wpisaniu pobierania pliku ruby w pasku adresu FireFox ... więc czy przeprowadziłeś jakieś badania w Internecie, zanim zadałeś to pytanie?
Dawid
@Dejw: Poszukałem informacji i znalazłem tutaj odpowiedź na pytanie. Zasadniczo za pomocą tego samego kodu, który mi dałeś. resp.bodyCzęść jest mylące mnie Myślałem, że to zapisać tylko „jednostka” część odpowiedzi, ale chcę, aby zapisać cały plik / binarny. Zauważyłem też, że pomocny może być rio.rubyforge.org . Zresztą moim pytaniem nikt nie może powiedzieć, że na takie pytanie jeszcze nie odpowiedział :-)
Radek
3
Część ciała to dokładnie cały plik. Odpowiedź jest tworzona z nagłówków (http) i treści (pliku), więc zapisując treść zapisałeś plik ;-)
Dawid
1
jeszcze jedno pytanie ... powiedzmy, że plik ma 100 MB i proces pobierania zostaje przerwany w środku. Czy będzie coś uratowanego? Czy mogę wznowić plik?
Radek
Niestety nie, ponieważ http.get('...')call wysyła żądanie i otrzymuje odpowiedź (cały plik). Aby pobrać plik w kawałkach i zapisać go jednocześnie, zobacz moją edytowaną odpowiedź poniżej ;-) Wznowienie nie jest łatwe, być może liczysz zapisane bajty, a następnie pomijasz je, gdy ponownie pobierasz plik ( file.write(resp.body)zwraca liczbę zapisanych bajtów).
Dawid

Odpowiedzi:

145

Najprostszym sposobem jest rozwiązanie specyficzne dla platformy:

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

Prawdopodobnie szukasz:

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

Edycja: zmieniona. Dziękuję Ci.

Edit2: Rozwiązanie, które zapisuje część pliku podczas pobierania:

# 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
źródło
15
Tak, wiem. Dlatego powiedziałem, że tak a platform-specific solution.
Dawid
1
Więcej rozwiązań specyficznych dla platformy: platformy GNU / Linux zapewniają wget. OS X zapewnia curl( curl http://oh.no/its/pbjellytime.flv --output secretlylove.flv). Windows ma odpowiednik Powershell (new-object System.Net.WebClient).DownloadFile('http://oh.no/its/pbjellytime.flv','C:\tmp\secretlylove.flv'). Pliki binarne dla wget i curl istnieją również dla wszystkich systemów operacyjnych do pobrania. Nadal bardzo polecam używanie biblioteki standardowej, chyba że piszesz kod wyłącznie dla własnej miłości.
fny
1
początek ... upewnij się ... koniec nie jest konieczny, jeśli używana jest forma bloku otwartego. otwórz „sample.flv” do | f | .... f.write segment
lab419
1
Plik nietekstowy jest uszkodzony.
Paul
1
Używam pobierania fragmentów za pomocą Net::HTTP. Otrzymuję część pliku, ale otrzymuję odpowiedź Net::HTTPOK. Czy jest jakiś sposób, aby upewnić się, że plik został pobrany w całości?
Nickolay Kondratenko
120

Wiem, że to stare pytanie, ale Google rzucił mnie tutaj i myślę, że znalazłem prostszą odpowiedź.

W Railscasts # 179 Ryan Bates użył standardowej klasy Ruby OpenURI, aby zrobić wiele z tego, o co proszono:

( Ostrzeżenie : nieprzetestowany kod. Być może trzeba będzie go zmienić / poprawić).

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
źródło
9
open("http://somedomain.net/flv/sample/sample.flv", 'rb')otworzy adres URL w trybie binarnym.
zoli
1
ktoś wie, czy open-uri inteligentnie zapełnia bufor, jak wyjaśnił @Isa?
gdelfino
1
@gildefino Otrzymasz więcej odpowiedzi, jeśli otworzysz w związku z tym nowe pytanie. Jest mało prawdopodobne, aby wiele osób to przeczytało (i jest to również właściwa rzecz w przypadku przepełnienia stosu).
kikito
2
Niesamowite. Miałem problemy z przekierowaniem HTTP=> HTTPSi dowiedziałem się, jak rozwiązać ten problem za pomocą open_uri_redirectionsGem
mathielo
2
FWIW Niektórzy uważają, że open-uri jest niebezpieczny, ponieważ monkeypatkuje cały kod, w tym kod biblioteki, który używa openz nową zdolnością, której kod wywołujący może nie przewidzieć. I tak nie powinieneś ufać przekazaniu danych przez użytkownika open, ale musisz teraz być podwójnie ostrożny.
metoda
44

Oto mój plik http w Ruby do pliku przy użyciu 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.read) }
  when Tempfile then io.close; FileUtils.mv(io.path, path)
  end
end

Główną zaletą jest to, że jest zwięzłe i proste, ponieważ openwykonuje większość ciężkich prac. I nie odczytuje w pamięci całej odpowiedzi.

openMetoda strumień odpowiedzi> 1KB do A Tempfile. Możemy wykorzystać tę wiedzę do wdrożenia tej szczupłej metody pobierania do pliku. Zobacz OpenURI::Bufferimplementację tutaj.

Zachowaj ostrożność podczas wprowadzania danych przez użytkownika! open(name, *rest, &block)jest niebezpieczne, jeśli namepochodzi z danych wejściowych użytkownika!

Overbryd
źródło
4
To powinna być akceptowana odpowiedź, ponieważ jest zwięzła i prosta i nie ładuje całego pliku do pamięci ~ + wydajność (oszacowanie tutaj).
Nikkolasg
Zgadzam się z Nikkolasgiem. Właśnie próbowałem go użyć i działa bardzo dobrze. Jednak trochę go zmodyfikowałem, na przykład lokalna ścieżka zostanie wydedukowana automatycznie z podanego adresu URL, więc np. „Path = nil”, a następnie sprawdzanie, czy nie ma nil; jeśli jest zero, używam File.basename () na adresie URL, aby wydedukować lokalną ścieżkę.
shevy
1
To byłoby najlepsze rozwiązanie, ale otwartym uri CZY wczytać cały plik w pamięci stackoverflow.com/questions/17454956/...
Simon Perepelitsa
2
@SimonPerepelitsa hehe. Poprawiłem go jeszcze raz, udostępniając teraz zwięzłą metodę pobierania do pliku, która nie odczytuje całej odpowiedzi w pamięci. Moja poprzednia odpowiedź byłaby wystarczająca, ponieważ w openrzeczywistości nie czyta odpowiedzi w pamięci, wczytuje ją do pliku tymczasowego dla odpowiedzi> 10240 bajtów. Więc miałeś rację, ale nie. Poprawiona odpowiedź wyjaśnia to nieporozumienie i, miejmy nadzieję,
posłuży
3
Jeśli pojawi się EACCES: permission deniedbłąd podczas zmiany nazwy pliku za pomocą mvpolecenia, to dlatego, że musisz najpierw zamknąć plik. Zaproponuj zmianę tej części naTempfile then io.close;
David Douglas
28

Przykład 3 w dokumentacji net / http Rubiego pokazuje, jak pobrać dokument przez HTTP i aby wyprowadzić plik zamiast po prostu ładować go do pamięci, należy zastąpić put zapisem binarnym do pliku, np. Jak pokazano w odpowiedzi Dejw.

Bardziej złożone przypadki przedstawiono w dalszej części tego samego dokumentu.

Arkku
źródło
+1 za wskazanie istniejącej dokumentacji i dalszych przykładów.
semperos
26

Poniższe rozwiązania najpierw odczytują całą zawartość z pamięci przed zapisaniem jej na dysku (aby uzyskać bardziej wydajne rozwiązania we / wy, spójrz na inne odpowiedzi).

Możesz użyć open-uri, czyli jednej wkładki

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

Lub używając net / http

require 'net/http'
File.write("file_name", Net::HTTP.get(URI.parse("http://url.com")))
KrauseFx
źródło
10
To wczytuje cały plik do pamięci przed zapisaniem go na dysku, więc ... to może być złe.
kgilpin
@kgilpin oba rozwiązania?
KrauseFx
1
Tak, oba rozwiązania.
eltiare
To powiedziawszy, jeśli nie masz nic przeciwko , krótsza wersja (zakładając, że adres URL i nazwa pliku są odpowiednio w zmiennych urli file), używając open-urijak w pierwszym: File.write(file, open(url).read)... Dead simple, dla trywialnego przypadku pobierania.
Lindes
17

Poszerzenie odpowiedzi Dejw (edycja 2):

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

gdzie filenamei urlsą struny.

sleepKomenda jest hack, które mogą znacznie zmniejszyć zużycie procesora, gdy sieć jest czynnikiem ograniczającym. Net :: HTTP nie czeka na zapełnienie bufora (16kB w wersji 1.9.2) przed uzyskaniem wyniku, więc procesor zajmuje się przenoszeniem małych fragmentów. Spanie przez chwilę daje szansę na zapełnienie bufora między zapisami, a użycie procesora jest porównywalne z rozwiązaniem curl, 4-5x różnica w mojej aplikacji. Bardziej niezawodne rozwiązanie mogłoby zbadać postęp f.posi dostosować limit czasu do docelowej, powiedzmy, 95% rozmiaru bufora - w rzeczywistości w ten sposób otrzymałem liczbę 0,005 w moim przykładzie.

Przepraszam, ale nie znam bardziej eleganckiego sposobu na to, by Ruby czekał na zapełnienie bufora.

Edytować:

Jest to wersja, która automatycznie dostosowuje się, aby utrzymać bufor tylko na poziomie lub poniżej pojemności. To nieeleganckie rozwiązanie, ale wydaje się być równie szybkie i zużywa tak mało czasu procesora, jak woła do zwijania.

Działa w trzech etapach. Krótki okres uczenia się z celowo długim czasem snu określa wielkość pełnego bufora. Okres upuszczania szybko skraca czas uśpienia z każdą iteracją, mnożąc go przez większy współczynnik, aż znajdzie niedopełniony bufor. Następnie, w normalnym okresie, dostosowuje się w górę iw dół o mniejszy współczynnik.

Mój Ruby jest trochę zardzewiały, więc jestem pewien, że można to poprawić. Przede wszystkim nie ma obsługi błędów. Może też można go podzielić na obiekt, z dala od samego pobierania, aby po prostu wywołać autosleep.sleep(f.pos)swoją pętlę? Co więcej, można zmienić Net :: HTTP, aby czekał na pełny bufor przed uzyskaniem :-)

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
Jest
źródło
Lubię sleephack!
Radek
13

Bibliotek przyjaznych dla API jest więcej niż Net::HTTPnp. 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
źródło
3

Miałem problemy, jeśli plik zawierał niemieckie umlauty (ä, ö, ü). Mogłem rozwiązać problem za pomocą:

ec = Encoding::Converter.new('iso-8859-1', 'utf-8')
...
f << ec.convert(seg)
...
Rolf
źródło
0

jeśli szukasz sposobu, jak pobrać plik tymczasowy, zrób rzeczy i usuń go, wypróbuj ten klejnot 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
odpowiednik 8
źródło