asyncio于Python3.4引入标準庫,增加了對異步I/O的支援,asyncio基于事件循環,可以輕松實作異步I/O操作。接下來,我們用基于asyncio的庫實作一個高性能爬蟲。
準備工作
Earth View from Google Earth是一款Chrome插件,會在打開新标簽頁時自動加載一張來自Google Earth的背景圖檔。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAjM2EzLcd3LcJzLcJzdllmVldWYtl2PnVGcq5SZ11GdvdHbtNXdvwFO1gDO4AjMtUGall3LcVmdhNXLwRHdo9CXt92YucWbpRWdvx2Yx5yazF2Lc9CX6MHc0RHaiojIsJye.jpeg)
使用Chrome開發者工具觀察插件的網絡請求,我們發現插件會請求一個位址如https://www.gstatic.com/prettyearth/assets/data/v2/1234.json的JSON檔案,檔案中包含了經過Base64的圖檔内容,觀察發現,圖檔的ID範圍大緻在1000-8000之間,我們的爬蟲就要來爬取這些精美的背景圖檔。
實作主要邏輯
由于爬取目标是JSON檔案,爬蟲的主要邏輯就變成了爬取JSON-->提取圖檔-->儲存圖檔。
requests是一個常用的http請求庫,但是由于requests的請求都是同步的,我們使用aiohttp這個異步http請求庫來代替。
1async def fetch_image_by_id(item_id):
2 url = f'https://www.gstatic.com/prettyearth/assets/data/v2/{item_id}.json'
3 # 由于URL是https的,是以選擇不驗證SSL
4 async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
5 async with session.get(url) as response:
6 # 擷取後需要将JSON字元串轉為對象
7 try:
8 json_obj = json.loads(await response.text())
9 except json.decoder.JSONDecodeError as e:
10 print(f'Download failed - {item_id}.jpg')
11 return
12 # 擷取JSON中的圖檔内容字段,經過Base64解碼成二進制内容
13 image_str = json_obj['dataUri'].replace('data:image/jpeg;base64,', '')
14 image_data = base64.b64decode(image_str)
15 save_folder = dir_path = os.path.dirname(
16 os.path.realpath(__file__)) + '/google_earth/'
17 with open(f'{save_folder}{item_id}.jpg', 'wb') as f:
18 f.write(image_data)
19 print(f'Download complete - {item_id}.jpg')
複制
aiohttp基于asyncio,是以在調用時需要使用async/await文法糖,可以看到,由于aiohttp中提供了一個ClientSession上下文,代碼中使用了async with的文法糖。
加入并行邏輯
上面的代碼是抓取單張圖檔的邏輯,批量抓取圖檔,需要再嵌套一層方法:
1async def fetch_all_images():
2 # 使用Semaphore限制最大并發數
3 sem = asyncio.Semaphore(10)
4 ids = [id for id in range(1000, 8000)]
5 for current_id in ids:
6 async with sem:
7 await fetch_image_by_id(current_id)
複制
接下來,将這個方法加入到asyncio的事件循環中。
1event_loop = asyncio.get_event_loop()
2future = asyncio.ensure_future(fetch_all_images())
3results = event_loop.run_until_complete(future)
複制
使用uvloop加速
uvloop基于libuv,libuv是一個使用C語言實作的高性能異步I/O庫,uvloop用來代替asyncio預設事件循環,可以進一步加快異步I/O操作的速度。
uvloop的使用非常簡單,隻要在擷取事件循環前,調用如下方法,将asyncio的事件循環政策設定為uvloop的事件循環政策。
1asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
複制
使用上面的代碼,我們可以快速将大約1500張的圖檔爬取下來。
性能對比
為了驗證aiohttp和uvloop的性能,筆者使用requests+concurrent庫實作了一個多程序版的爬蟲,分别爬取20個id,消耗的時間如圖。
可以看到,耗時相差了大概7倍,aiohttp+uvloop的組合在爬蟲這種I/O密集型的場景下,可以說具有壓倒性優勢。相信在不遠的将來,基于asyncio的庫會将無數爬蟲工程師從加班中拯救出來。