如何提升爬蟲的性能
如果你使用過爬蟲框架scrapy,那么你多多少少會驚異于她的并發(fā)和高效。
在scrapy中,你可以通過在settings中設(shè)置線程數(shù)來輕松定制一個多線程爬蟲。這得益于scrappy的底層twisted異步框架。
異步在爬蟲開發(fā)中經(jīng)常突顯奇效,因為他可以是單個鏈接爬蟲不堵塞。
不阻塞可以理解為:在A線程等待response的時候,B線程可以發(fā)起requests,或者C線程可以進行數(shù)據(jù)處理。
要單個爬蟲線程不阻塞,python可以使用到的庫有:
- threading
- gevent
- asyncio
一個常規(guī)的阻塞爬蟲
下面的代碼實現(xiàn)了一個獲取 貓眼電影top100 的爬蟲,網(wǎng)站反爬較弱,帶上UA即可。
我們給爬蟲寫一個裝飾器,記錄其爬取時間。
import requests
import time
from lxml import etree
from threading import Thread
from functools import cmp_to_key
# 給輸出結(jié)果排序
def sortRule(x, y):
for i in x.keys():
c1 = int(i)
for i in y.keys():
c2 = int(i)
if c1 > c2:
return 1
elif c1 < c2:
return -1
else:
return 0
# 計算時間的裝飾器
def caltime(func):
def wrapper(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
print("costtime: ", time.time() - start)
return wrapper
# 獲取頁面
def getPage(url):
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36',
# 'Cookie': '__mta=141898381.1589978369143.1590927122695.1590927124319.9; uuid_n_v=v1; uuid=EDAA8A109A9611EABDA40952C053E9B506991609A05441F5AFBA3872BEA6088C; _csrf=f36a7050eb60429b197a902b4f1d66317db95bde0879648c8bff0e8237e937de; Hm_lvt_703e94591e87be68cc8da0da7cbd0be2=1589978364; mojo-uuid=8b4dad0e1f472f08ffd3f3f67b75f2ab; _lxsdk_cuid=17232188c2f0-022085e6f29b1b-30657c06-13c680-17232188c30c8; _lxsdk=EDAA8A109A9611EABDA40952C053E9B506991609A05441F5AFBA3872BEA6088C; mojo-session-id={"id":"afcd899e03fe72ca70e34368fe483d15","time":1590927095603}; __mta=141898381.1589978369143.1590063115667.1590927111235.7; mojo-trace-id=10; Hm_lpvt_703e94591e87be68cc8da0da7cbd0be2=1590927124; _lxsdk_s=1726aa4fd86-ba9-904-221%7C%7C15',
}
try:
resp = requests.get(url=url, headers=headers)
if resp.status_code == 200:
return resp.text
return None
except Exception as e:
print(e)
return None
# 獲取單個頁面數(shù)據(jù)
def parsePage(page):
if not page:
yield
data = etree.HTML(page).xpath('.//dl/dd')
for d in data:
rank = d.xpath("./i/text()")[0]
title = d.xpath(".//p[@class='name']/a/text()")[0]
yield {
rank: title
}
# 調(diào)度
def schedule(url, f):
page = getPage(url)
for data in parsePage(page):
f.append(data)
# 數(shù)據(jù)展示
def show(f):
f.sort(key=cmp_to_key(sortRule))
for x in f:
print(x)
@caltime
def main():
urls = ['https://maoyan.com/board/4?offset={offset}'.format(offset=i) for i in range(0, 100, 10)]
f = []
for url in urls:
schedule(url, f)
show(f)
if __name__ == '__main__':
main()
成功爬取完top100平均花費2.8s左右。
這個爬蟲程序總共有10個小的爬蟲線程,每個爬蟲線程爬取10條數(shù)據(jù)。當(dāng)前面的線程未成功收到response時,后面所有的線程都阻塞了。
這也是這個爬蟲程序低效的原因。因為線程之間有明確的先后順序,后面的線程無法越過前面的線程發(fā)送請求。
threading打破線程的優(yōu)先級?
接下來我們使用多線程打破這種優(yōu)先順序。修改main函數(shù)
def main():
urls = ['https://maoyan.com/board/4?offset={offset}'.format(offset=i) for i in range(0, 100, 10)]
threads = []
f = []
for url in urls:
# schedule(url, f)
t = Thread(target=schedule, args=(url, f))
threads.append(t)
t.start()
for t in threads:
t.join()
show(f)
記得導(dǎo)入threading庫
from threading import Thread
點擊運行,發(fā)現(xiàn)時間縮短為0.4s,性能的提升還是很客觀的。
threading的作用在于開啟了多個線程,每個線程同時競爭GIL,當(dāng)拿到GIL發(fā)出requests后。該線程又立即釋放GIL。進入等待Response的狀態(tài)。
釋放掉的GIL又馬上被其他線程獲取...如此以來,每個線程都是平等的,無先后之分。看起來就好像同時進行著(實際并不是,因為GIL的原因)。
所以效率大大提升了。
gevent異步協(xié)程搞一波?
gevent是一個優(yōu)先的異步網(wǎng)絡(luò)庫,可以輕松支持高并發(fā)的網(wǎng)絡(luò)訪問。我們現(xiàn)在試著把阻塞的爬蟲加上gevent試試
@caltime
def main():
threads = []
urls = ['https://maoyan.com/board/4?offset={offset}'.format(offset=i) for i in range(0, 100, 10)]
f = []
for url in urls:
threads.append(gevent.spawn(schedule, url, f))
gevent.joinall(threads)
show(f)
同樣這里也要導(dǎo)入gevent庫
import gevent
from gevent import monkey
monkey.patch_all()
點擊運行,平均時間在0.45上左右,和多線程差不多。
新版異步庫ascyncio搞一波?
ascyncion是python前不久剛推出的基于協(xié)程的異步庫,號稱最有野心的庫。要使ascyncio支持我們的程序,必須對getPage做點修改:
因為requests是不支持異步的,所以我們這里使用aiohttp庫替換requests,并用它來實現(xiàn)getPage函數(shù)。
# 異步requests
async def getPage(url):
headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'}
async with aiohttp.ClientSession() as session:
async with session.get(url, headers = headers) as resp:
return await resp.text()
main函數(shù)也需要修改
@caltime
def main():
urls = ['https://maoyan.com/board/4?offset={offset}'.format(offset=i) for i in range(0, 100, 10)]
loop = asyncio.get_event_loop()
f = []
threads = []
for url in urls:
threads.append(schedule(url,f))
loop.run_until_complete(asyncio.wait(threads))
show(f)
記得導(dǎo)入相關(guān)庫
import asyncio
import aiohttp
點擊運行,平均時間在0.35左右,性能稍優(yōu)于多線程和gevent一點。
結(jié)語
對于爬蟲技術(shù),其實有些比較新的東西是值得去了解一下的。比如:
- 提升并發(fā)方面:asyncio, aiohttp
- 動態(tài)渲染:pyppeteer(puppeteer的python版,支持異步)
- 驗證碼破解:機器學(xué)習(xí),模型訓(xùn)練
還有一些數(shù)據(jù)解析方面的工具性能大概如下:
- re > lxml > bs4
- 但是即便是同一種解析方法,不同工具實現(xiàn)的,性能也不一樣。比如同樣是xpath,lxml的性能略好于parsel(scrapy團隊開發(fā)的數(shù)據(jù)解析工具,支持css,re,xpath)的。