ดาวน์โหลดไฟล์ขนาดใหญ่ในไพ ธ อนพร้อมคำขอ


398

คำขอเป็นห้องสมุดที่ดีจริงๆ ฉันต้องการใช้เพื่อดาวน์โหลดไฟล์ขนาดใหญ่ (> 1GB) ปัญหาคือมันเป็นไปไม่ได้ที่จะเก็บไฟล์ทั้งหมดไว้ในหน่วยความจำฉันต้องอ่านมันเป็นชิ้น ๆ และนี่เป็นปัญหาของรหัสต่อไปนี้

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 

ด้วยเหตุผลบางอย่างมันไม่ทำงานด้วยวิธีนี้ มันยังโหลดการตอบสนองลงในหน่วยความจำก่อนบันทึกลงในไฟล์

UPDATE

หากคุณต้องการลูกค้าขนาดเล็ก (หลาม 2.x /3.x) ซึ่งสามารถดาวน์โหลดไฟล์ขนาดใหญ่จาก FTP, คุณสามารถค้นหาได้ที่นี่ มันรองรับมัลติเธรด & เชื่อมต่อใหม่ (มันเชื่อมต่อจอภาพ) และยังปรับซ็อกเก็ตสำหรับงานดาวน์โหลด

คำตอบ:


650

ด้วยรหัสการสตรีมต่อไปนี้การใช้หน่วยความจำ Python จะถูก จำกัด โดยไม่คำนึงถึงขนาดของไฟล์ที่ดาวน์โหลด:

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

โปรดทราบว่าจำนวนไบต์ที่ส่งคืนโดยใช้iter_contentไม่ตรงchunk_size; คาดว่าจะเป็นตัวเลขสุ่มที่มักจะใหญ่กว่าและคาดว่าจะแตกต่างกันในการคำนวณซ้ำทุกครั้ง

ดูhttps://requests.readthedocs.io/en/latest/user/advanced/#body-content-workflowและhttps://requests.readthedocs.io/en/latest/api/#requests.Response.iter_contentสำหรับเพิ่มเติม การอ้างอิง


9
@Shuman อย่างที่ฉันเห็นคุณแก้ไขปัญหาเมื่อเปลี่ยนจาก http: // เป็น https: // ( github.com/kennethreitz/requests/issues/2043 ) คุณช่วยกรุณาอัปเดตหรือลบความคิดเห็นของคุณเพราะคนอาจคิดว่ามีปัญหากับรหัสสำหรับไฟล์ที่ใหญ่กว่า
1024Mb

8
chunk_sizeเป็นสิ่งสำคัญ โดยค่าเริ่มต้นคือ 1 (1 ไบต์) นั่นหมายความว่าสำหรับ 1MB มันจะทำการวนซ้ำ 1 ล้าน docs.python-requests.org/en/latest/api/…
Eduard Gamonal

4
f.flush()ดูเหมือนไม่จำเป็น คุณกำลังพยายามใช้อะไรให้สำเร็จ (การใช้หน่วยความจำของคุณจะไม่ 1.5gb ถ้าคุณวางไว้) f.write(b'')(หากiter_content()อาจส่งคืนสตริงว่าง) ควรไม่เป็นอันตรายและif chunkอาจถูกตัดทิ้งได้เช่นกัน
jfs

11
@RomanPodlinov: f.flush()ไม่ล้างข้อมูลลงในดิสก์ทางกายภาพ มันถ่ายโอนข้อมูลไปยังระบบปฏิบัติการ โดยปกติจะเพียงพอหากไม่มีไฟฟ้าขัดข้อง f.flush()ทำให้รหัสช้าลงที่นี่โดยไม่มีเหตุผล ฟลัชเกิดขึ้นเมื่อบัฟเฟอร์ไฟล์ที่เสียหาย (ภายในแอพ) เต็ม หากคุณต้องการเขียนบ่อยขึ้น; ผ่าน buf.size open()พารามิเตอร์
jfs

9
อย่าลืมปิดการเชื่อมต่อด้วยr.close()
0xcaff

271

มันง่ายกว่าถ้าคุณใช้Response.rawและ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

สตรีมไฟล์นี้ไปยังดิสก์โดยไม่ต้องใช้หน่วยความจำมากเกินไปและรหัสง่าย


10
โปรดทราบว่าคุณอาจจำเป็นต้องปรับเมื่อสตรีมมิ่งการตอบสนอง gzippedต่อปัญหา 2155.
ChrisP

32
นี่ควรเป็นคำตอบที่ถูกต้อง! ยอมรับคำตอบที่ได้รับคุณถึง 2-3MB / s การใช้ copyfileobj ทำให้คุณได้รับ ~ 40MB / s ดาวน์โหลด Curl (เครื่องเดียวกัน, url เดียวกัน, ฯลฯ ) ด้วย ~ 50-55 MB / s
visoft

24
เพื่อให้แน่ใจว่าการร้องขอการเชื่อมต่อได้รับการปล่อยตัวคุณสามารถใช้withบล็อกที่สอง (ซ้อนกัน) เพื่อทำการร้องขอ:with requests.get(url, stream=True) as r:
Christian Long

7
@ChristianLong: มันเป็นเรื่องจริง แต่เมื่อเร็ว ๆ นี้เนื่องจากคุณลักษณะการสนับสนุนwith requests.get()ถูกรวมเข้ากับ 2017-06-07 เท่านั้น! คำแนะนำของคุณเหมาะสมสำหรับผู้ที่มีคำขอ 2.18.0 หรือใหม่กว่า Ref: github.com/requests/requests/issues/4136
John Zwinck

4
@EricCousineau คุณสามารถแก้ไขพฤติกรรมนี้ได้โดยแทนที่readวิธีการ:response.raw.read = functools.partial(response.raw.read, decode_content=True)
Nuno André

54

ไม่ว่า OP จะขออะไร แต่ ... มันเป็นเรื่องง่ายที่จะทำเช่นนั้นด้วย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)

หรือด้วยวิธีนี้หากคุณต้องการบันทึกเป็นไฟล์ชั่วคราว:

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)

ฉันดูกระบวนการ:

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

และฉันเห็นไฟล์เพิ่มขึ้น แต่การใช้หน่วยความจำอยู่ที่ 17 MB ฉันพลาดอะไรไปรึเปล่า?


2
สำหรับ Python 2.x ให้ใช้from urllib import urlretrieve
Vadim Kotov

ส่งผลให้ความเร็วในการดาวน์โหลดช้า ...
citynorman

@citynorman คุณช่วยได้ไหม เมื่อเทียบกับวิธีการแก้ปัญหา? ทำไม?
x-yuri

@ x-yuri เทียบกับวิธีแก้ปัญหาshutil.copyfileobjด้วยคะแนนเสียงมากที่สุดดูความคิดเห็นของฉันและคนอื่น ๆ ที่นั่น
citynorman

41

ขนาดก้อนของคุณอาจใหญ่เกินไปคุณลองลดขนาดนั้นลงทีละ 1024 ไบต์หรือไม่? (เช่นคุณสามารถใช้withเพื่อจัดระเบียบไวยากรณ์)

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 

อนึ่งคุณคิดว่าการตอบสนองนั้นถูกโหลดเข้าสู่หน่วยความจำอย่างไร

ดูเหมือนว่าไพ ธ อนไม่ได้ล้างข้อมูลไปยังไฟล์จากคำถาม SOอื่น ๆ ที่คุณสามารถลองf.flush()และos.fsync()บังคับให้ไฟล์เขียนและหน่วยความจำว่าง

    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())

1
ฉันใช้การตรวจสอบระบบใน Kubuntu มันแสดงให้ฉันเห็นว่าหน่วยความจำกระบวนการหลามเพิ่มขึ้น (มากถึง 1.5gb จาก 25kb)
Roman Podlinov

ความทรงจำที่ขยายตัวอาจf.flush(); os.fsync()จะบังคับให้เขียนหน่วยความจำได้ฟรี
danodonovan

2
มันos.fsync(f.fileno())
sebdelsol

29
คุณต้องใช้สตรีม = True ในการร้องขอ requests.get () นั่นคือสิ่งที่ทำให้หน่วยความจำขยายตัว
ฮัท 8

1
ตัวพิมพ์เล็ก: คุณพลาดโคลอน (':') หลังdef DownloadFile(url)
Aubrey
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.