Python: 使用future,併發下載圖片

開局一張圖(下載的圖片):在這裏插入圖片描述


1. 網絡普通下載圖片

爲了高效處理網絡I/O,需要使用併發,因爲網絡有很高的延遲,所以爲了不浪費CPU週期去等待,最好在收到網絡響應之前做些其他的事。

兩個示例程序,從網上下載圖片。第一個示例程序是依序下載的:下載完一個圖,並將其保存在硬盤中之後,才請求下一個圖像。另一個腳本是併發下載的:幾乎同時請求所有圖像,每下載完一個文件就保存一個文件,腳本使用concurrent.futures模塊。

在I/O密集型應用中,如果代碼寫得正確,那麼不管使用哪種併發策略(使用線程或asyncio包),吞吐量都比依序執行的代碼高很多。

這邊我改了《流暢的Python》中的下載地址和對象:

# a5_4_downloadimage.py
import os
import sys
import time
import requests

DOWNNLOAD_DIR = r'D:\downloadimage'
BASE_URL = 'http://pic2.sc.chinaz.com/Files/pic/pic9/202002/'
image_list = ['zzpic231' + str(i) + '_s.jpg' for i in range(10, 90)]

def save_image(img, filename):
    path = os.path.join(DOWNNLOAD_DIR, filename)
    with open(path, 'wb') as fp:
        fp.write(img)

def get_image(suffix):
    url = os.path.join(BASE_URL, suffix)
    response = requests.get(url)
    return response.content

def show(text):
    print(text,end='\n')
    sys.stdout.flush()

def download_all(image_name_list):  # download_all是與併發實現比較的關鍵函數。
    for image_name in image_name_list:
        image = get_image(image_name)
        save_image(image, image_name)
        show(image)
    return len(image_name_list)

def main(download_task):
    t0 = time.time()
    count = download_task(image_list)
    elapsed = time.time() - t0
    msg = f'\n download {count} images in {elapsed}s'
    print(msg)

if __name__ == '__main__':
    main(download_all)

#  download 80 images in 4.6661295890808105s
#  download 80 images in 5.478628873825073s
#  download 80 images in 4.028514862060547s

2. 使用concurrent.futures模塊實現併發下載

concurrent.futures模塊的主要特色是 ThreadPoolExecutorProcessPoolExecutor 類,這兩個類實現的接口能分別在不同的線程或進程中執行可調用的對象。這兩個類在內部維護着一個工作線程或進程池,以及要執行的任務隊列。不過,這個接口抽象的層級很高,像下載圖片這種簡單的案例,無需關心任何實現細節。

使用ThreadPoolExecutor.map方法,以最簡單的方式實現併發下載:

# a5_4_downloadimage2.py
from concurrent import futures
from a5_4_downloadimage import save_image, get_image, show, main

MAX_WORDERS = 20  # 設定ThreadPoolExecutor類最多使用幾個線程:併發20個

def download_single(image_name):
    image = get_image(image_name)
    save_image(image, image_name)
    show(image)
    return image_name

def download_multiple(image_name_list):
    tasks = min(MAX_WORDERS, len(image_name_list))
    with futures.ThreadPoolExecutor(tasks) as executor:
        res = executor.map(download_single, sorted(image_name_list))
    return len(list(res))

if __name__ == '__main__':
    main(download_multiple)

# download 80 images in 1.4081335067749023s
# download 80 images in 1.561039924621582s
# download 80 images in 1.393141746520996s

download_multiple 函數中設定工作的線程數量:使用允許的最大(MAX_WORKERS)與要處理的數量之間較小的那個值,以免創建多餘的線程;使用工作的線程數實例化ThreadPoolExecutor類;executor.__exit__ 方法會調用 executor.shutdown(wait=True) 方法,它會在所有線程都執行完畢前阻塞線程;map方法的作用與內置的map函數類似,不過 download_single 函數會在多個線程中併發調用;map方法返回一個生成器,因此可以迭代,獲取各個函數返回的值。最後返回獲取的結果數量,如果有線程拋出異常,異常會在return語句處拋出,這與隱式調用 next() 函數從迭代器中獲取相應的返回值一樣。

download_single 函數其實是前面例子中的 download_all 函數的 for 循環體。編寫併發代碼時經常這樣重構:把依序執行的for循環體改成函數,以便併發調用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章