Tải tập tin lớn bằng python với yêu cầu


399

Yêu cầu là một thư viện thực sự tốt đẹp. Tôi muốn sử dụng nó để tải xuống các tệp lớn (> 1GB). Vấn đề là không thể giữ toàn bộ tập tin trong bộ nhớ. Tôi cần đọc nó theo từng đoạn. Và đây là một vấn đề với đoạn mã sau

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 

Bởi một số lý do nó không hoạt động theo cách này. Nó vẫn tải phản hồi vào bộ nhớ trước khi lưu nó vào một tập tin.

CẬP NHẬT

Nếu bạn cần một máy khách nhỏ (Python 2.x /3.x) có thể tải xuống các tệp lớn từ FTP, bạn có thể tìm thấy nó ở đây . Nó hỗ trợ đa luồng & kết nối lại (nó giám sát các kết nối), nó cũng điều chỉnh các thông số ổ cắm cho tác vụ tải xuống.

Câu trả lời:


651

Với mã phát trực tiếp sau đây, việc sử dụng bộ nhớ Python bị hạn chế bất kể kích thước của tệp được tải xuống:

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

Lưu ý rằng số byte được trả về sử dụng iter_contentkhông chính xác chunk_size; nó được dự đoán là một số ngẫu nhiên thường lớn hơn nhiều và được dự kiến ​​sẽ khác nhau trong mỗi lần lặp.

Xem https://requests.readthedocs.io/en/latest/user/advified/#body-content-workflowhttps://requests.readthedocs.io/en/latest/api/#requests.Response.iter_content để biết thêm tài liệu tham khảo.


9
@Shuman Như tôi thấy bạn đã giải quyết vấn đề khi chuyển từ http: // sang https: // ( github.com/kennethreitz/requests/issues/2043 ). Bạn có thể vui lòng cập nhật hoặc xóa nhận xét của mình không vì mọi người có thể nghĩ rằng có vấn đề với mã cho các tệp lớn hơn 1024Mb
Roman Podlinov

8
các chunk_sizelà rất quan trọng. theo mặc định đó là 1 (1 byte). điều đó có nghĩa là với 1 MB, nó sẽ tạo ra 1 triệu lần lặp. docs.python-requests.org/en/latest/api/...
Eduard Gamonal

4
f.flush()dường như không cần thiết Bạn đang cố gắng để đạt được điều gì khi sử dụng nó? (mức sử dụng bộ nhớ của bạn sẽ không là 1,5gb nếu bạn bỏ nó). f.write(b'')(nếu iter_content()có thể trả về một chuỗi rỗng) sẽ vô hại và do đó if chunkcũng có thể bị loại bỏ.
JFS

11
@RomanPodlinov: f.flush()không xóa dữ liệu vào đĩa vật lý. Nó chuyển dữ liệu sang HĐH. Thông thường, nó là đủ trừ khi có sự cố mất điện. f.flush()làm cho mã chậm hơn ở đây không có lý do. Việc xả xảy ra khi bộ đệm tập tin correponding (bên trong ứng dụng) đã đầy. Nếu bạn cần viết thường xuyên hơn; truyền tham số buf.size cho open().
jfs

9
Đừng quên đóng kết nối vớir.close()
0xcaff

272

Sẽ dễ dàng hơn nhiều nếu bạn sử dụng Response.rawshutil.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

Điều này truyền tệp vào đĩa mà không sử dụng bộ nhớ quá mức và mã rất đơn giản.


10
Lưu ý rằng bạn có thể cần phải điều chỉnh khi luồng phản ứng gzip mỗi vấn đề 2155.
ChrisP

32
Đây phải là câu trả lời chính xác! Các chấp nhận câu trả lời giúp bạn lên đến 2-3MB / s. Sử dụng copyfileobj giúp bạn đạt ~ 40MB / s. Tải xuống curl (cùng máy, cùng url, v.v.) với ~ 50-55 MB / s.
visoft

24
Để đảm bảo kết nối Yêu cầu được giải phóng, bạn có thể sử dụng khối thứ hai (lồng nhau) withđể thực hiện yêu cầu:with requests.get(url, stream=True) as r:
Christian Long

7
@ChristianLong: Điều đó đúng, nhưng chỉ rất gần đây, vì tính năng hỗ trợ with requests.get()chỉ được hợp nhất vào ngày 2017-06-07! Đề xuất của bạn là hợp lý cho những người có Yêu cầu 2.18.0 trở lên. Tham chiếu: github.com/requests/requests/issues/4136
John Zwinck


54

Không chính xác những gì OP đã yêu cầu, nhưng ... thật dễ dàng để làm điều đó với 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)

Hoặc theo cách này, nếu bạn muốn lưu nó vào một tệp tạm thời:

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)

Tôi đã xem quá trình:

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

Và tôi thấy tập tin ngày càng tăng, nhưng mức sử dụng bộ nhớ vẫn ở mức 17 MB. Tui bỏ lỡ điều gì vậy?


2
Đối với Python 2.x, hãy sử dụngfrom urllib import urlretrieve
Vadim Kotov

Điều này dẫn đến tốc độ tải xuống chậm ...
citynorman

@citynorman Bạn có thể giải thích? So với giải pháp nào? Tại sao?
x-yuri

@ x-yuri so với giải pháp shutil.copyfileobjcó nhiều phiếu bầu nhất, hãy xem ý kiến ​​của tôi và những người khác ở đó
citynorman

42

Kích thước khối của bạn có thể quá lớn, bạn đã thử bỏ nó - có thể 1024 byte mỗi lần chưa? (ngoài ra, bạn có thể sử dụng withđể dọn dẹp cú pháp)

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 

Ngẫu nhiên, làm thế nào bạn suy luận rằng phản hồi đã được tải vào bộ nhớ?

Nghe có vẻ như python không xóa dữ liệu thành tệp, từ các câu hỏi SO khác mà bạn có thể thử f.flush()os.fsync()buộc tệp ghi và bộ nhớ trống;

    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
Tôi sử dụng System Monitor trong Kubfox. Nó cho tôi thấy rằng bộ nhớ quá trình python tăng (lên tới 1,5gb từ 25kb).
Roman Podlinov

Bộ nhớ đó bị hút, có lẽ f.flush(); os.fsync()có thể buộc một bộ nhớ không có bộ nhớ.
danodonovan

2
đó làos.fsync(f.fileno())
sebdelsol

29
Bạn cần sử dụng stream = True trong lệnh gọi request.get (). Đó là những gì gây ra sự phình to bộ nhớ.
Hut8

1
lỗi đánh máy nhỏ: bạn bỏ lỡ một dấu hai chấm (':') saudef DownloadFile(url)
Aubrey
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.