爬蟲提速必備!用 asyncio 重寫你的 Python 爬蟲,效率爆炸!
在編程開發中,我們總是繞不開一個問題:如何高效地處理 I/O 密集型任務? 傳統的同步編程在遇到網絡請求、文件讀寫、數據庫查詢這些“慢操作”時,就像一個盡職盡責但效率低下的服務生,他必須站在原地等到一杯咖啡煮好,才能去處理下一個客人的需求。
這,就是阻塞(Blocking),是 CPU 資源的巨大浪費。
今天,我們就來聊聊 Python 解決這個痛點的核心機制——異步編程,以及它背后的三大支柱:async、await 和 asyncio 模塊!

一、為什么需要異步編程?
在解釋技術細節之前,我們先用一個簡單的場景來理解“同步”和“異步”的區別:
- 同步(Sync): 就像你去咖啡店買咖啡。你點單后,就一直站在柜臺前等待咖啡師把你的咖啡做好,期間你什么也做不了。等你的咖啡好了,你才能去做下一件事。
- 異步(Async): 就像你在咖啡店點完咖啡后,拿到一個取餐叫號器(一個未來的通知)。你就可以先找個位子坐下,看看書,甚至去隔壁商店轉一圈。當叫號器響了(通知你的任務完成了),你再去取咖啡。在等待咖啡的過程中,你的時間被充分利用了!
在編程中,同步代碼在執行耗時 I/O 操作時會阻塞整個線程,導致資源浪費。而異步編程就是為了解決這個“等待”的問題,讓程序在等待 I/O 完成時,可以切換去做其他有意義的事情,從而提高并發和吞吐量。
二、核心概念:asyncio、async 和 await
在 Python 里實現異步,主要依賴 asyncio 模塊,以及兩個關鍵字 async 和 await。
1. asyncio:異步編程的“發動機”
asyncio 是 Python 用于編寫單線程并發代碼的庫。它提供了事件循環(Event Loop)、網絡 I/O、子進程和并發運行協程等全套工具。簡單來說,它是 Python 異步編程的基礎設施。
2. 協程:可暫停的任務
協程,就是我們上面提到的那個“可以暫停”的任務。它不是線程也不是進程,它是一種比線程更輕量級的存在。
- 定義: 協程是用戶級別的、可中斷的函數。它可以在執行過程中主動讓出控制權(暫停),并在需要時恢復執行。
- 如何定義: 使用 async def 關鍵字定義的函數就是一個協程函數。調用它不會立即執行,而是返回一個協程對象。
# 協程函數
async def fetch_data(delay):
"""模擬一個耗時的網絡請求任務"""
print(f"開始抓取數據,預計等待 {delay} 秒...")
# await 暫停執行,等待操作完成
await asyncio.sleep(delay)
print(f"數據抓取完成(耗時 {delay} 秒)")
return f"Result after {delay} seconds"3. async:我是一個異步函數/協程
async 關鍵字用于定義一個協程函數:async def function_name(...): 定義一個協程函數。調用這個函數會返回一個協程對象,而不是執行函數體。
4. await:請“暫停”,去等別人
await 關鍵字只能用在協程函數 (async def) 內部。它的作用是:
- 暫停執行: 遇到 await 時,當前的協程會暫停執行,并將控制權交還給事件循環。
- 等待任務: 事件循環會去執行其他已準備好的任務。
- 恢復執行: 直到 await 后面的異步操作(比如 asyncio.sleep() 或一個網絡請求)完成并返回結果后,當前協程才會恢復執行,并從暫停的地方繼續。
三、用 asyncio 實現并發
現在,我們把這些概念組合起來,看看如何用 asyncio 同時執行多個任務。
1. 簡單示例:等待 3 秒和 1 秒
我們希望程序能夠并發地等待 3 秒和 1 秒,總耗時應該接近于兩者中較長的那個(即 3 秒),而不是 4 秒。
import asyncio
import time
# 協程函數,注意前面的 async
asyncdef task_a():
print(f"Task A: 啟動于 {time.strftime('%X')}")
# 遇到 await 暫停,將控制權交給事件循環
await asyncio.sleep(3)
print(f"Task A: 結束于 {time.strftime('%X')}")
asyncdef task_b():
print(f"Task B: 啟動于 {time.strftime('%X')}")
# 遇到 await 暫停,將控制權交給事件循環
await asyncio.sleep(1)
print(f"Task B: 結束于 {time.strftime('%X')}")
# 主協程函數
asyncdef main():
print(f"主程序開始于 {time.strftime('%X')}")
# **關鍵步驟**:使用 asyncio.gather() 并發運行多個協程
await asyncio.gather(task_a(), task_b())
print(f"主程序結束于 {time.strftime('%X')}")
# 運行事件循環
if __name__ == "__main__":
# asyncio.run() 是 Python 3.7+ 推薦的啟動方式
# 它負責創建并關閉事件循環,并執行傳入的頂級協程。
asyncio.run(main())運行結果(注意時間):
主程序開始于 18:33:45
Task A: 啟動于 18:33:45
Task B: 啟動于 18:33:45
Task B: 結束于 18:33:46 <-- 注意,B 在 1 秒后先完成了
Task A: 結束于 18:33:48
主程序結束于 18:33:48可以看到,盡管 task_a 需要 3 秒,task_b 需要 1 秒,但它們是同時開始執行的,整個程序只用了大約 3 秒就完成了,完美實現了異步并發!
2. 獲取返回值
在實際應用中,我們還需要獲取異步操作的結果。asyncio.gather() 不僅能并發運行,還能返回每個協程的結果。
import asyncio
asyncdef fetch_user(user_id):
"""模擬根據 ID 獲取用戶信息"""
print(f"正在異步獲取用戶 ID: {user_id}")
await asyncio.sleep(2) # 模擬網絡延遲 2 秒
return {"id": user_id, "name": f"User_{user_id}", "status": "Ready"}
asyncdef main_with_result():
# 同時發起 3 個用戶的獲取請求
tasks = [
fetch_user(101),
fetch_user(102),
fetch_user(103)
]
# asyncio.gather() 等待所有協程完成,并返回一個包含所有結果的列表
results = await asyncio.gather(*tasks)
print("\n所有用戶數據已獲取:")
for res in results:
print(f"- {res}")
if __name__ == "__main__":
asyncio.run(main_with_result())代碼解析:
(1) fetch_user(user_id) 是一個協程函數,它會返回一個字典。
(2) 在 main_with_result 中,我們創建了 3 個 fetch_user 的協程對象,并將它們放入 tasks 列表中。
(3) await asyncio.gather(*tasks):
- *tasks 將列表解包成獨立的參數。
- asyncio.gather 并發地運行所有協程。
- await 暫停 main_with_result,直到所有 3 個獲取用戶的協程都完成。
- 最終,results 會按傳入任務的順序包含 3 個字典結果。
四、總結與進階
通過上面的例子,相信你已經掌握了 Python 異步編程的靈魂三件套:
元素 | 作用 | 放在哪里 | 相當于現實中的... |
| 定義一個可暫停的協程函數。 |
前 | 告訴我,這是一個可以中途休息的任務 |
| 暫停 當前協程,并等待一個異步操作完成。 | 只能在 | 叫號器響了,去等結果吧,我先去做別的! |
| 提供了事件循環等機制,管理協程的調度和運行。 | 模塊/運行時環境 | 咖啡店里的總調度師 |
異步編程的威力遠不止于此,它在網絡爬蟲(如 aiohttp)、Web 框架(如 FastAPI、Starlette)、高并發 API 服務器等領域都有廣泛應用。
如果你想讓你的 Python 程序“快到飛起”,那么掌握 asyncio 絕對是必修課!一起擼起來吧~






























