Laden Sie eine große Datei in Python mit Anfragen herunter

399

Requests ist eine wirklich schöne Bibliothek. Ich möchte es zum Herunterladen großer Dateien (> 1 GB) verwenden. Das Problem ist, dass es nicht möglich ist, die gesamte Datei im Speicher zu halten. Ich muss sie in Blöcken lesen. Und dies ist ein Problem mit dem folgenden Code

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 

Aus irgendeinem Grund funktioniert es nicht so. Die Antwort wird weiterhin in den Speicher geladen, bevor sie in einer Datei gespeichert wird.

AKTUALISIEREN

Wenn Sie einen kleinen Client (Python 2.x / 3.x) benötigen, der große Dateien von FTP herunterladen kann, finden Sie ihn hier . Es unterstützt Multithreading und erneutes Verbinden (es überwacht Verbindungen) und optimiert Socket-Parameter für die Download-Aufgabe.

Roman Podlinov
quelle

Antworten:

651

Mit dem folgenden Streaming-Code wird die Python-Speichernutzung unabhängig von der Größe der heruntergeladenen Datei eingeschränkt:

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

Beachten Sie, dass die Anzahl der zurückgegebenen Bytes iter_contentnicht genau der Anzahl entspricht chunk_size. Es wird erwartet, dass es sich um eine Zufallszahl handelt, die oft viel größer ist und sich in jeder Iteration unterscheidet.

Siehe https://requests.readthedocs.io/en/latest/user/advanced/#body-content-workflow und https://requests.readthedocs.io/en/latest/api/#requests.Response.iter_content für weitere Referenz.

Roman Podlinov
quelle
9
@Shuman Wie ich sehe, haben Sie das Problem behoben, als Sie von http: // zu https: // ( github.com/kennethreitz/requests/issues/2043 ) gewechselt sind . Können Sie bitte Ihre Kommentare aktualisieren oder löschen, weil die Leute denken, dass es Probleme mit dem Code für Dateien mit einer Größe von
1024
8
das chunk_sizeist entscheidend. Standardmäßig ist es 1 (1 Byte). Das bedeutet, dass für 1 MB 1 Million Iterationen durchgeführt werden. docs.python-requests.org/en/latest/api/…
Eduard Gamonal
4
f.flush()scheint unnötig. Was versuchst du damit zu erreichen? (Ihre Speichernutzung beträgt nicht 1,5 GB, wenn Sie sie löschen). f.write(b'')(wenn iter_content()möglicherweise eine leere Zeichenfolge zurückgegeben wird) sollte harmlos sein und if chunkkönnte daher auch gelöscht werden.
JFS
11
@RomanPodlinov: Spült f.flush()keine Daten auf die physische Festplatte. Es überträgt die Daten an das Betriebssystem. Normalerweise reicht es aus, wenn kein Stromausfall vorliegt. f.flush()macht den Code hier ohne Grund langsamer. Das Leeren erfolgt, wenn der entsprechende Dateipuffer (innerhalb der App) voll ist. Wenn Sie häufiger schreiben müssen; Übergeben Sie den Parameter buf.size an open().
JFS
9
Vergessen Sie nicht, die Verbindung mitr.close()
0xcaff
272

Es ist viel einfacher, wenn Sie Response.rawund 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

Dadurch wird die Datei ohne übermäßigen Speicherplatz auf die Festplatte gestreamt, und der Code ist einfach.

John Zwinck
quelle
10
Beachten Sie, dass Sie möglicherweise anpassen müssen, wenn Sie gezippte Antworten pro Ausgabe 2155 streamen.
ChrisP
32
DAS sollte die richtige Antwort sein! Die akzeptierte Antwort bringt Sie auf 2-3 MB / s. Mit copyfileobj erreichen Sie ~ 40 MB / s. Curl-Downloads (gleiche Maschinen, gleiche URL usw.) mit ~ 50-55 MB / s.
Visoft
24
Um sicherzustellen, dass die Requests-Verbindung freigegeben wird, können Sie einen zweiten (verschachtelten) withBlock verwenden, um die Anfrage zu stellen:with requests.get(url, stream=True) as r:
Christian Long
7
@ChristianLong: Das stimmt, aber erst vor kurzem, da die zu unterstützende Funktion with requests.get()erst am 07.06.2017 zusammengeführt wurde! Ihr Vorschlag ist für Personen mit Anfragen 2.18.0 oder höher angemessen. Ref: github.com/requests/requests/issues/4136
John Zwinck
54

Nicht genau das, was OP gefragt hat, aber ... es ist lächerlich einfach, das zu tun mit 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)

Oder auf diese Weise, wenn Sie es in einer temporären Datei speichern möchten:

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)

Ich habe den Prozess beobachtet:

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

Und ich sah die Datei wachsen, aber die Speichernutzung blieb bei 17 MB. Vermisse ich etwas

x-yuri
quelle
2
Verwenden Sie für Python 2.xfrom urllib import urlretrieve
Vadim Kotov
Dies führt zu einer langsamen Download-Geschwindigkeit ...
citynorman
@citynorman Kannst du näher darauf eingehen? Im Vergleich zu welcher Lösung? Warum?
x-yuri
@ x-yuri vs die Lösung shutil.copyfileobjmit den meisten Stimmen, siehe meine und andere Kommentare dort
citynorman
42

Ihre Chunk-Größe könnte zu groß sein. Haben Sie versucht, das zu löschen - vielleicht 1024 Bytes gleichzeitig? (Sie können withauch die Syntax aufräumen)

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 

Wie können Sie übrigens feststellen, dass die Antwort in den Speicher geladen wurde?

Es hört sich so an, als würde Python die Daten nicht von anderen in eine Datei leeren SO Fragen Sie könnten versuchen , f.flush()und os.fsync()die Datei schreiben und den freien Speicher zu zwingen;

    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
quelle
1
Ich benutze System Monitor in Kubuntu. Es zeigt mir, dass der Python-Prozessspeicher zunimmt (bis zu 1,5 GB von 25 KB).
Roman Podlinov
Das Aufblähen des Gedächtnisses ist scheiße, f.flush(); os.fsync()könnte vielleicht das Schreiben eines freien Speichers erzwingen.
Danodonovan
2
Es istos.fsync(f.fileno())
Sebdelsol
29
Sie müssen stream = True im Aufruf von request.get () verwenden. Das ist es, was das Gedächtnis aufblähen lässt.
Hut8
1
kleiner Tippfehler: Sie verpassen einen Doppelpunkt (':') nachdef DownloadFile(url)
Aubrey