併發體驗:Python抓圖的8種方式
本文系作者「無名小妖」的第二篇原創投稿文章,作者通過用爬蟲示例來說明併發相關的多執行緒、多程式、協程之間的執行效率對比。如果你喜歡寫部落格,想投稿可微信我,有稿費酬勞。
假設我們現在要在網上下載圖片,一個簡單的方法是用 requests+BeautifulSoup。注:本文所有例子都使用python3.5)
單執行緒
示例 1:get_photos.py
import os
import time
import uuid
import requests
from bs4 import BeautifulSoup
def out_wrapper(func): # 記錄程式執行時間的簡單裝飾器
def inner_wrapper():
start_time = time.time()
func()
stop_time = time.time()
print('Used time {}'.format(stop_time-start_time))
return inner_wrapper
def save_flag(img, filename): # 儲存圖片
path = os.path.join('down_photos', filename)
with open(path, 'wb') as fp:
fp.write(img)
def download_one(url): # 下載一個圖片
image = requests.get(url)
save_flag(image.content, str(uuid.uuid4()))
def user_conf(): # 返回30個圖片的url
url = 'https://unsplash.com/'
ret = requests.get(url)
soup = BeautifulSoup(ret.text, "lxml")
zzr = soup.find_all('img')
ret = []
num = 0
for item in zzr:
if item.get("src").endswith('80') and num < 30:
num += 1
ret.append(item.get("src"))
return ret
@out_wrapper
def download_many():
zzr = user_conf()
for item in zzr:
download_one(item)
if __name__ == '__main__':
download_many()
示例1進行的是順序下載,下載30張圖片的平均時間在60s左右(結果因實驗環境不同而不同)。
這個程式碼能用但並不高效,怎麼才能提高效率呢?
參考開篇的示意圖,有三種方式:多程式、多執行緒和協程。下面我們一一說明:
我們都知道 Python 中存在 GIL(主要是Cpython),但 GIL 並不影響 IO 密集型任務,因此對於 IO 密集型任務而言,多執行緒更加適合(執行緒可以開100個,1000個而程式同時執行的數量受 CPU 核數的限制,開多了也沒用)
不過,這並不妨礙我們通過實驗來了解多程式。
多程式
示例2
from multiprocessing import Process
from get_photos import out_wrapper, download_one, user_conf
@out_wrapper
def download_many():
zzr = user_conf()
task_list = []
for item in zzr:
t = Process(target=download_one, args=(item,))
t.start()
task_list.append(t)
[t.join() for t in task_list] # 等待程式全部執行完畢(為了記錄時間)
if __name__ == '__main__':
download_many()
本示例重用了示例1的部分程式碼,我們只需關注使用多程式的這部分。
筆者測試了3次(使用的機器是雙核超執行緒,即同時只能有4個下載任務在進行),輸出分別是:19.5s、17.4s和18.6s。速度提升並不是很多,也證明了多程式不適合io密集型任務。
還有一種使用多程式的方法,那就是內建模組futures中的ProcessPoolExecutor。
示例3
from concurrent import futures
from get_photos import out_wrapper, download_one, user_conf
@out_wrapper
def download_many():
zzr = user_conf()
with futures.ProcessPoolExecutor(len(zzr)) as executor:
res = executor.map(download_one, zzr)
return len(list(res))
if __name__ == '__main__':
download_many()
使用 ProcessPoolExecutor 程式碼簡潔了不少,executor.map 和標準庫中的 map用法類似。耗時和示例2相差無幾。多程式就到這裡,下面來體驗一下多執行緒。
多執行緒
示例4
import threading
from get_photos import out_wrapper, download_one, user_conf
@out_wrapper
def download_many():
zzr = user_conf()
task_list = []
for item in zzr:
t = threading.Thread(target=download_one, args=(item,))
t.start()
task_list.append(t)
[t.join() for t in task_list]
if __name__ == '__main__':
download_many()
threading 和 multiprocessing 的語法基本一樣,但是速度在9s左右,相較多程式提升了1倍。
下面的示例5和示例6中分別使用內建模組 futures.ThreadPoolExecutor 中的 map 和submit、as_completed
示例5
from concurrent import futures
from get_photos import out_wrapper, download_one, user_conf
@out_wrapper
def download_many():
zzr = user_conf()
with futures.ThreadPoolExecutor(len(zzr)) as executor:
res = executor.map(download_one, zzr)
return len(list(res))
if __name__ == '__main__':
download_many()
示例6:
from concurrent import futures
from get_photos import out_wrapper, download_one, user_conf
@out_wrapper
def download_many():
zzr = user_conf()
with futures.ThreadPoolExecutor(len(zzr)) as executor:
to_do = [executor.submit(download_one, item) for item in zzr]
ret = [future.result() for future in futures.as_completed(to_do)]
return ret
if __name__ == '__main__':
download_many()
Executor.map 由於和內建的map用法相似所以更易於使用,它有個特性:返回結果的順序與呼叫開始的順序一致。不過,通常更可取的方式是,不管提交的順序,只要有結果就獲取。
為此,要把 Executor.submit 和 futures.as_completed結合起來使用。
最後到了協程,這裡分別介紹 gevent 和 asyncio。
gevent
示例7
from gevent import monkey
monkey.patch_all()
import gevent
from get_photos import out_wrapper, download_one, user_conf
@out_wrapper
def download_many():
zzr = user_conf()
jobs = [gevent.spawn(download_one, item) for item in zzr]
gevent.joinall(jobs)
if __name__ == '__main__':
download_many()
asyncio
示例8
import uuid
import asyncio
import aiohttp
from get_photos import out_wrapper, user_conf, save_flag
async def download_one(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
save_flag(await resp.read(), str(uuid.uuid4()))
@out_wrapper
def download_many():
urls = user_conf()
loop = asyncio.get_event_loop()
to_do = [download_one(url) for url in urls]
wait_coro = asyncio.wait(to_do)
res, _ = loop.run_until_complete(wait_coro)
loop.close()
return len(res)
if __name__ == '__main__':
download_many()
協程的耗時和多執行緒相差不多,區別在於協程是單執行緒。具體原理限於篇幅這裡就不贅述了。
但是我們不得不說一下asyncio,asyncio是Python3.4加入標準庫的,在3.5為其新增async和await關鍵字。或許對於上述多執行緒多程式的例子你稍加研習就能掌握,但是想要理解asyncio你不得不付出更多的時間和精力。
另外,使用執行緒寫程式比較困難,因為排程程式任何時候都能中斷執行緒。必須保留鎖以保護程式,防止多步操作在執行的過程中中斷,防止資料處於無效狀態。
而協程預設會做好全方位保護,我們必須顯式產出才能讓程式的餘下部分執行。對協程來說,無需保留鎖,在多個執行緒之間同步操作,協程自身就會同步,因為在任意時刻只有一個協程執行。想交出控制權時,可以使用 yield 或 yield from(await) 把控制權交還排程程式。
總結
本篇文章主要是將python中併發相關的模組進行基本用法的介紹,全做拋磚引玉。而這背後相關的程式、執行緒、協程、阻塞io、非阻塞io、同步io、非同步io、事件驅動等概念和asyncio的用法並未介紹。大家感興趣的話可以自行google或者百度,也可以在下方留言,大家一起探討。
(如果本文對你有幫助,可以對作者打賞)
相關文章
- Python合併字典的七種方式!Python
- SQLite 併發的四種處理方式SQLite
- 3種方式實現python多執行緒併發處理Python執行緒
- python 三種方式實現截圖Python
- React 併發功能體驗-前端的併發模式已經到來。React前端模式
- 【開發經驗】幾種常見的加密方式加密
- Python進行開發的兩種方式Python
- 分散式鎖解決併發的三種實現方式分散式
- 併發程式設計 建立執行緒的三種方式程式設計執行緒
- Python Schema一種優雅的資料驗證方式Python
- php合併陣列的幾種方式PHP陣列
- 幾種常見網路抓包方式介紹
- 程式間的8種通訊方式
- Android合併檔案的3種方式Android
- 短影片直播系統,實現高併發秒殺的多種方式
- 【詳解】併發程式的等待方式
- 換種方式使用 Laravel 的 request 驗證Laravel
- Python 操作 MySQL 的5種方式PythonMySql
- python的幾種輸出方式Python
- python的幾種輸入方式Python
- python 非同步的幾種方式Python非同步
- Vue3 的8種元件通訊方式Vue元件
- Python圖片驗證碼降噪 — 8鄰域降噪Python
- 8 種基本軟體開發模型:選擇哪一種?模型
- Rainbond 5.6 版本釋出,增加多種安裝方式,最佳化拓撲圖操作體驗AI
- 前端開發工程師不可不知的8種佈局方式前端工程師
- Python種匯入模組的三種方式總結Python
- canvas渲染熱力圖的一種方式Canvas
- 聊聊excel生成圖片的幾種方式Excel
- python接收郵件的幾種方式Python
- python字典的四種遍歷方式Python
- 併發-8-ThreadPoolExecutorthread
- 併發問題處理方式
- 短視訊app開發,三種圖片並排展示的方式APP
- python中合併表格的兩種方法Python
- Python的 併發、並行Python並行
- 和小姐姐面試python,是種什麼體驗?面試Python
- Python 中一種輕鬆實現併發程式設計的方法Python程式設計