Unduh file besar dengan python dengan permintaan

401

Permintaan adalah perpustakaan yang sangat bagus. Saya ingin menggunakannya untuk mengunduh file besar (> 1GB). Masalahnya adalah itu tidak mungkin untuk menyimpan seluruh file dalam memori saya harus membacanya dalam potongan. Dan ini merupakan masalah dengan kode berikut

import requests

def DownloadFile(url)
    local_filename = url.split('/')[-1]
    r = requests.get(url)
    f = open(local_filename, 'wb')
    for chunk in r.iter_content(chunk_size=512 * 1024): 
        if chunk: # filter out keep-alive new chunks
            f.write(chunk)
    f.close()
    return 

Dengan beberapa alasan itu tidak berfungsi seperti ini. Itu masih memuat respons ke dalam memori sebelum menyimpannya ke file.

MEMPERBARUI

Jika Anda memerlukan klien kecil (Python 2.x /3.x) yang dapat mengunduh file besar dari FTP, Anda dapat menemukannya di sini . Ini mendukung multithreading & menghubungkan kembali (ini memonitor koneksi) dan juga menyetel params socket untuk tugas pengunduhan.

Roman Podlinov
sumber

Jawaban:

653

Dengan kode streaming berikut, penggunaan memori Python dibatasi terlepas dari ukuran file yang diunduh:

def download_file(url):
    local_filename = url.split('/')[-1]
    # NOTE the stream=True parameter below
    with requests.get(url, stream=True) as r:
        r.raise_for_status()
        with open(local_filename, 'wb') as f:
            for chunk in r.iter_content(chunk_size=8192): 
                # If you have chunk encoded response uncomment if
                # and set chunk_size parameter to None.
                #if chunk: 
                f.write(chunk)
    return local_filename

Perhatikan bahwa jumlah byte yang dikembalikan menggunakan iter_contenttidak tepat chunk_size; itu diharapkan menjadi angka acak yang seringkali jauh lebih besar, dan diharapkan berbeda di setiap iterasi.

Lihat https://requests.readthedocs.io/en/latest/user/advanced/#body-content-workflow dan https://requests.readthedocs.io/en/latest/api/#requests.Response.iter_content untuk lebih lanjut referensi.

Roman Podlinov
sumber
9
@Shuman Seperti yang saya lihat Anda menyelesaikan masalah ketika beralih dari http: // ke https: // ( github.com/kennethreitz/requests/issues/2043 ). Bisakah Anda memperbarui atau menghapus komentar Anda karena orang mungkin berpikir ada masalah dengan kode untuk file yang lebih besar 1024Mb
Roman Podlinov
8
yang chunk_sizesangat penting. secara default 1 (1 byte). itu berarti bahwa untuk 1MB itu akan membuat 1 juta iterasi. docs.python-requests.org/en/latest/api/…
Eduard Gamonal
4
f.flush()sepertinya tidak perlu. Apa yang ingin Anda capai dengan menggunakannya? (Penggunaan memori Anda tidak akan 1,5 GB jika Anda menjatuhkannya). f.write(b'')(Jika iter_content()dapat mengembalikan string kosong) harus tidak berbahaya dan karena itu if chunkbisa dijatuhkan juga.
jfs
11
@RomanPodlinov: f.flush()tidak menyiram data ke disk fisik. Ini mentransfer data ke OS. Biasanya, itu sudah cukup kecuali ada kegagalan daya. f.flush()membuat kode lebih lambat di sini tanpa alasan. Siram terjadi ketika buffer file correponding (aplikasi dalam) penuh. Jika Anda perlu lebih sering menulis; meneruskan parameter buf.size ke open().
jfs
9
Jangan lupa untuk menutup koneksi denganr.close()
0xcaff
273

Jauh lebih mudah jika Anda menggunakan Response.rawdan shutil.copyfileobj():

import requests
import shutil

def download_file(url):
    local_filename = url.split('/')[-1]
    with requests.get(url, stream=True) as r:
        with open(local_filename, 'wb') as f:
            shutil.copyfileobj(r.raw, f)

    return local_filename

Ini mengalirkan file ke disk tanpa menggunakan memori yang berlebihan, dan kodenya sederhana.

John Zwinck
sumber
10
Perhatikan bahwa Anda mungkin perlu menyesuaikan kapan streaming respons gzip per edisi 2155.
ChrisP
32
INI harus menjadi jawaban yang benar! The diterima jawaban membuat Anda hingga 2-3MB / s. Menggunakan copyfileobj membuat Anda mencapai ~ 40MB / s. Unduh curl (mesin yang sama, url yang sama, dll) dengan ~ 50-55 MB / s.
visoft
24
Untuk memastikan koneksi Permintaan dilepaskan, Anda dapat menggunakan withblok kedua (bersarang) untuk membuat permintaan:with requests.get(url, stream=True) as r:
Christian Long
7
@ChristianLong: Itu benar, tetapi baru-baru ini saja, karena fitur untuk mendukung with requests.get()baru digabung pada 2017-06-07! Saran Anda masuk akal untuk orang yang memiliki Permintaan 2.18.0 atau lebih tinggi. Ref: github.com/requests/requests/issues/4136
John Zwinck
4
@EricCousineau Anda dapat memperbaiki perilaku ini dengan mengganti readmetode:response.raw.read = functools.partial(response.raw.read, decode_content=True)
Nuno André
54

Tidak persis dengan apa yang diminta OP, tapi ... sangat mudah untuk melakukannya dengan urllib:

from urllib.request import urlretrieve
url = 'http://mirror.pnl.gov/releases/16.04.2/ubuntu-16.04.2-desktop-amd64.iso'
dst = 'ubuntu-16.04.2-desktop-amd64.iso'
urlretrieve(url, dst)

Atau dengan cara ini, jika Anda ingin menyimpannya ke file sementara:

from urllib.request import urlopen
from shutil import copyfileobj
from tempfile import NamedTemporaryFile
url = 'http://mirror.pnl.gov/releases/16.04.2/ubuntu-16.04.2-desktop-amd64.iso'
with urlopen(url) as fsrc, NamedTemporaryFile(delete=False) as fdst:
    copyfileobj(fsrc, fdst)

Saya menyaksikan prosesnya:

watch 'ps -p 18647 -o pid,ppid,pmem,rsz,vsz,comm,args; ls -al *.iso'

Dan saya melihat file tumbuh, tetapi penggunaan memori tetap di 17 MB. Apakah saya melewatkan sesuatu?

x-yuri
sumber
2
Untuk Python 2.x, gunakanfrom urllib import urlretrieve
Vadim Kotov
Ini menghasilkan kecepatan unduh yang lambat ...
citynorman
@citynorman Bisakah Anda menguraikan? Dibandingkan dengan solusi apa? Mengapa?
x-yuri
@ x-yuri vs solusi shutil.copyfileobjdengan suara terbanyak, lihat komentar saya dan orang lain di sana
citynorman
42

Ukuran chunk Anda mungkin terlalu besar, sudahkah Anda mencoba menjatuhkannya - mungkin 1024 byte sekaligus? (juga, Anda bisa menggunakan withuntuk merapikan sintaks)

def DownloadFile(url):
    local_filename = url.split('/')[-1]
    r = requests.get(url)
    with open(local_filename, 'wb') as f:
        for chunk in r.iter_content(chunk_size=1024): 
            if chunk: # filter out keep-alive new chunks
                f.write(chunk)
    return 

Kebetulan, bagaimana Anda menyimpulkan bahwa respons telah dimuat ke memori?

Kedengarannya seolah-olah python tidak membuang data ke file, dari pertanyaan SO lainnya Anda bisa mencoba f.flush()dan os.fsync()memaksa file menulis dan membebaskan memori;

    with open(local_filename, 'wb') as f:
        for chunk in r.iter_content(chunk_size=1024): 
            if chunk: # filter out keep-alive new chunks
                f.write(chunk)
                f.flush()
                os.fsync(f.fileno())
danodonovan
sumber
1
Saya menggunakan Monitor Sistem di Kubuntu. Ini menunjukkan kepada saya bahwa proses memori python meningkat (hingga 1.5GB dari 25kb).
Roman Podlinov
Memori itu mengasapi menyebalkan, mungkin f.flush(); os.fsync()bisa memaksa menulis memori bebas.
danodonovan
2
ituos.fsync(f.fileno())
sebdelsol
29
Anda perlu menggunakan stream = True dalam panggilan requests.get (). Itulah yang menyebabkan memori kembung.
Hut8
1
kesalahan ketik kecil: Anda melewatkan titik dua (':') setelahdef DownloadFile(url)
Aubrey