高性能异步爬虫
引言:前面介绍的都是对单个网页的爬取,假如你想同时对多个网页进行爬取呢?这是你肯定会想到构建一个url列表然后循环遍历访问,首先我们知道无论是get请求还是post请求,都是同步阻塞操作。因为程序都是从上往下依次执行的,你给一个网站发起请求就必然等待接受到结果才会对下一个网站发起请求。这样是不是大大的损耗了时间影响了程序的执行效率。
下面来看一个同步的例子,同步简单来说就是:执行A事件的时候发起B件事,必须等待B件事结束之后才能继续做A事件。
import requests
import time
start_time = time.time() headers = { 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.81 Safari/537.36', } urls = ['https://www.baidu.com', 'http://httpbin.org/get', 'https://www.python.org/'] def get_page(url): page = requests.get(url,headers=headers) if page.status_code == 200: print(f'{url}成功访问…...')
time.sleep(2)#睡眠2秒
for url in urls: get_page(url) end_time = time.time() print(f'总耗时:{end_time-start_time}')
这里构建了一个模拟访问的函数,并在程序开始和结束获取当前时间戳,并计算其总耗时。结果如下
https://www.baidu.com成功访问
http://httpbin.org/get成功访问
https://www.python.org/成功访问
总耗时:8.20714783668518
如果再加入解析数据并将数据入库的操作,可想而知程序的效率是十分低下的,大大提高了时间成本。这里就可以采用开启多线程的形式执行。多线程也就是假如在执行A事件时遇到了阻塞操作就去执行B事件,达到并发的目的。提高效率。
from threading import Thread import time import requests start_time = time.time() headers = { 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.81 Safari/537.36', } urls = ['https://www.baidu.com', 'http://httpbin.org/get', 'https://www.python.org/'] def get_page(url): page = requests.get(url,headers=headers) if page.status_code == 200: print(f'{url}成功访问') time.sleep(2) end_time = time.time() p_list = [] for url in urls: p = Thread(target=get_page,args=(url,)) #开启多线程 p.start() p_list.append(p) for p in p_list:p.join() print(f'总耗时:{end_time-start_time}')
如果是windows就需要加上if __name__ == '__main__':,join方法是用来回收资源的,确保在所有子线程执行完之后主线程自动退出。
结果如下:
https://www.baidu.com成功访问 https://www.python.org/成功访问 http://httpbin.org/get成功访问 总耗时:4.0531158447265625e-06
可以看出时间花费仅在4秒左右,比单线程的效率快了一倍。这里用到了threading模块,其中Thread()函数创建线程需要给target传参也就是将被执行的函数名,args传的是变量必须是元组的形势,start方法启动线程。
当然比起多线程还有更快的方式,就是利用线程池。
什么是池:
要在程序开始的时候,还没提交任务先创建几个线程或者进程这就是通俗易懂的池。
线程池为什么会比多线程快?
如果先开好进程,那么有任务之后就可以直接使用这个池中的数据。并且开好的线程会一直存在在池中,可以被多个任务反复利用。这样极大的减少了开启\关闭\调度线程的时间开销。
下面来看一个例子,还是由第一个例子改写的:
from concurrent.futures import ThreadPoolExecutor #线程池模块 import time import requests start_time = time.time() headers = { 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.81 Safari/537.36', } urls = ['https://www.baidu.com', 'http://httpbin.org/get', 'https://www.python.org/'] def get_page(url): page = requests.get(url,headers=headers) if page.status_code == 200: print(f'{url}成功访问') time.sleep(2) end_time = time.time() p = ThreadPoolExecutor(3)#开启线程池 有几个任务就开几个线程 for url in urls: tp.submit(get_page,url) #提交任务 print(f'总耗时:{end_time-start_time}')
结果:
http://httpbin.org/get成功访问 https://www.baidu.com成功访问 https://www.python.org/成功访问 总耗时:2.86102294921875e-06
比起第一个程序快了6秒左右。首先实例化一个线程池对象,然后循环url列表向利用submit函数向池中提交任务。
当然有线程池就会有进程池,因为爬虫技术大部分都是IO操作使用多线程编程更加合适,而多进程适用于计算操作比较多的情况。