一、前言
很多时候,我们写一个爬虫,满足要求之后,会发现很多改进,其中之一就是爬行速度。本文通过代码说明了如何利用多进程、多线程和协同进程来提高爬行速度。注意:我们不深入介绍理论和原理,一切都在代码里。
第二,同步
首先,我们编写一个简化的爬虫,细分每个功能,有意识地执行功能编程。以下代码的目的是访问百度页面300次并返回状态代码,其中parse_1函数可以设置循环数,每个循环都会将当前循环数(从0开始)和url传递给parse_2函数。
导入请求
defparse_1():
url='https://www.baidu.com '
foriirange(300):
parse_2(url)
defparse_2(url):
response=requests.get(url)
打印(响应.状态代码)
if__name__=='__main__':
parse_1()的性能消耗主要在IO请求中,在单进程、单线程模式下请求URL时必然会造成等待。
示例代码是典型的串行逻辑。parse_1将url和循环号传递给parse_2。parse_2请求并返回状态代码后,parse_1继续迭代一次,重复前面的步骤。
第三,多线程
由于CPU执行程序时每个时间尺度上只有一个线程,多线程实际上提高了进程的利用率,从而提高了CPU的利用率。
实现多线程的库有很多,concurrent.futures中的ThreadPoolExecutor演示了这一点,之所以引入ThreadPoolExecutor库,是因为它比其他库代码更简单。
为了方便说明问题,下面代码中如果是新增加的部分,代码行前会加上 符号便于观察说明问题,实际运行需要去掉
导入请求from concurrent . futureimportthreadpoolexecutor
defparse_1():
url='https://www.baidu.com '
#建立线程池
池=线程池执行器(6)
foriirange(300):
pool.submit(parse_2,url)
关闭池(等待=真)
defparse_2(url):
response=requests.get(url)
打印(响应.状态代码)
if__name__=='__main__':
Parse_1()是异步的,而不是同步的。异步意味着彼此独立,在等待一个事件的同时继续做自己的事情,而不是在工作前等待事件结束。线程是实现异步的一种方式,也就是说多线程是异步处理,也就是说我们不知道处理结果,有时候我们需要知道处理结果,所以可以用回调。
导入请求
from concurrent . futureimportthreadpoolexecutor
#添加回调函数
defcallback(未来):
打印(future.result())
defparse_1():
url='https://www.baidu.com '
池=线程池执行器(6)
foriirange(300):
results=pool.submit(parse_2,url)
#回调的关键步骤
results.add_done_callback(回调)
关闭池(等待=真)
defparse_2(url):
response=requests.get(url)
打印(响应.状态代码)
if__name__=='__main__':
Parse_1()Python实现了多线程。有一个GIL(全局解释器锁)被无数人诟病,但是多线程还是很适合抓取网页的,大多是IO密集型的。
四.多进程
多进程有两种实现方式:ProcessPoolExecutor和多进程。
00-1010类似于实现多线程的ThreadPoolExecutor。
导入请求
> from concurrent.futures import ProcessPoolExecutor def parse_1(): url = 'https://www.baidu.com' # 建立线程池 > pool = ProcessPoolExecutor(6) for i in range(300): > pool.submit(parse_2, url) > pool.shutdown(wait=True) def parse_2(url): response = requests.get(url) print(response.status_code) if __name__ == '__main__': parse_1()可以看到改动了两次类名,代码依旧很简洁,同理也可以添加回调函数
import requests from concurrent.futures import ProcessPoolExecutor > def callback(future): > print(future.result()) def parse_1(): url = 'https://www.baidu.com' pool = ProcessPoolExecutor(6) for i in range(300): > results = pool.submit(parse_2, url) > results.add_done_callback(callback) pool.shutdown(wait=True) def parse_2(url): response = requests.get(url) print(response.status_code) if __name__ == '__main__': parse_1()2. multiprocessing
直接看代码,一切都在注释中。
import requests > from multiprocessing import Pool def parse_1(): url = 'https://www.baidu.com' # 建池 > pool = Pool(processes=5) # 存放结果 > res_lst = [] for i in range(300): # 把任务加入池中 > res = pool.apply_async(func=parse_2, args=(url,)) # 获取完成的结果(需要取出) > res_lst.append(res) # 存放最终结果(也可以直接存储或者print) > good_res_lst = [] > for res in res_lst: # 利用get获取处理后的结果 > good_res = res.get() # 判断结果的好坏 > if good_res: > good_res_lst.append(good_res) # 关闭和等待完成 > pool.close() > pool.join() def parse_2(url): response = requests.get(url) print(response.status_code) if __name__ == '__main__': parse_1()可以看到multiprocessing库的代码稍繁琐,但支持更多的拓展。多进程和多线程确实能够达到加速的目的,但如果遇到IO阻塞会出现线程或者进程的浪费,因此有一个更好的方法……
五、异步非阻塞
协程+回调配合动态协作就可以达到异步非阻塞的目的,本质只用了一个线程,所以很大程度利用了资源
实现异步非阻塞经典是利用asyncio库+yield,为了方便利用逐渐出现了更上层的封装 aiohttp,要想更好的理解异步非阻塞最好还是深入了解asyncio库。而gevent是一个非常方便实现协程的库
import requests > from gevent import monkey # 猴子补丁是协作运行的灵魂 > monkey.patch_all() > import gevent def parse_1(): url = 'https://www.baidu.com' # 建立任务列表 > tasks_list = [] for i in range(300): > task = gevent.spawn(parse_2, url) > tasks_list.append(task) > gevent.joinall(tasks_list) def parse_2(url): response = requests.get(url) print(response.status_code) if __name__ == '__main__': parse_1()gevent能很大提速,也引入了新的问题:如果我们不想速度太快给服务器造成太大负担怎么办?如果是多进程多线程的建池方法,可以控制池内数量。如果用gevent想要控制速度也有一个不错的方法:建立队列。gevent中也提供了Quene类,下面代码改动较大
import requests from gevent import monkey monkey.patch_all() import gevent > from gevent.queue import Queue def parse_1(): url = 'https://www.baidu.com' tasks_list = [] # 实例化队列 > quene = Queue() for i in range(300): # 全部url压入队列 > quene.put_nowait(url) # 两路队列 > for _ in range(2): > task = gevent.spawn(parse_2) > tasks_list.append(task) gevent.joinall(tasks_list) # 不需要传入参数,都在队列中 > def parse_2(): # 循环判断队列是否为空 > while not quene.empty(): # 弹出队列 > url = quene.get_nowait() response = requests.get(url) # 判断队列状态 > print(quene.qsize(), response.status_code) if __name__ == '__main__': parse_1()结束语
以上就是几种常用的加速方法。如果对代码测试感兴趣可以利用time模块判断运行时间。爬虫的加速是重要技能,但适当控制速度也是爬虫工作者的良好习惯,不要给服务器太大压力,拜拜~