如何提升爬虫的性能
如果你使用过爬虫框架scrapy,那么你多多少少会惊异于她的并发和高效。
在scrapy中,你可以通过在settings中设置线程数来轻松定制一个多线程爬虫。这得益于scrappy的底层twisted异步框架。
异步在爬虫开发中经常突显奇效,因为他可以是单个链接爬虫不堵塞。
不阻塞可以理解为:在A线程等待response的时候,B线程可以发起requests,或者C线程可以进行数据处理。
要单个爬虫线程不阻塞,python可以使用到的库有:
- threading
- gevent
- asyncio
一个常规的阻塞爬虫
下面的代码实现了一个获取 猫眼电影top100 的爬虫,网站反爬较弱,带上UA即可。
我们给爬虫写一个装饰器,记录其爬取时间。
import requests
import time
from lxml import etree
from threading import Thread
from functools import cmp_to_key
# 给输出结果排序
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
# 获取单个页面数据
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
}
# 调度
def schedule(url, f):
page = getPage(url)
for data in parsePage(page):
f.append(data)
# 数据展示
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条数据。当前面的线程未成功收到response时,后面所有的线程都阻塞了。
这也是这个爬虫程序低效的原因。因为线程之间有明确的先后顺序,后面的线程无法越过前面的线程发送请求。
threading打破线程的优先级?
接下来我们使用多线程打破这种优先顺序。修改main函数
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)
记得导入threading库
from threading import Thread
点击运行,发现时间缩短为0.4s,性能的提升还是很客观的。
threading的作用在于开启了多个线程,每个线程同时竞争GIL,当拿到GIL发出requests后。该线程又立即释放GIL。进入等待Response的状态。
释放掉的GIL又马上被其他线程获取...如此以来,每个线程都是平等的,无先后之分。看起来就好像同时进行着(实际并不是,因为GIL的原因)。
所以效率大大提升了。
gevent异步协程搞一波?
gevent是一个优先的异步网络库,可以轻松支持高并发的网络访问。我们现在试着把阻塞的爬虫加上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)
同样这里也要导入gevent库
import gevent
from gevent import monkey
monkey.patch_all()
点击运行,平均时间在0.45上左右,和多线程差不多。
新版异步库ascyncio搞一波?
ascyncion是python前不久刚推出的基于协程的异步库,号称最有野心的库。要使ascyncio支持我们的程序,必须对getPage做点修改:
因为requests是不支持异步的,所以我们这里使用aiohttp库替换requests,并用它来实现getPage函数。
# 异步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函数也需要修改
@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)
记得导入相关库
import asyncio
import aiohttp
点击运行,平均时间在0.35左右,性能稍优于多线程和gevent一点。
结语
对于爬虫技术,其实有些比较新的东西是值得去了解一下的。比如:
- 提升并发方面:asyncio, aiohttp
- 动态渲染:pyppeteer(puppeteer的python版,支持异步)
- 验证码破解:机器学习,模型训练
还有一些数据解析方面的工具性能大概如下:
- re > lxml > bs4
- 但是即便是同一种解析方法,不同工具实现的,性能也不一样。比如同样是xpath,lxml的性能略好于parsel(scrapy团队开发的数据解析工具,支持css,re,xpath)的。