常规的爬虫
- 缺点:
耗时长、效率低、易崩溃
并发爬虫
原理
将整个爬虫程序分为cpu操作和IO操作两部分。cpu首先开始执行task,在遇到IO操作时,cpu会切换到另一个task开始执行,IO操作结束后,再通知cpu进行处理。由于IO操作读取内存、磁盘网络等不需要cpu的参与、两者可以同时进行,cpu可以释放出来执行其他的task实现加速。采用多线程并发操作执行程序可以大大降低运行时间,提高效率
- 优点:
速度快、效率高、安全性高
应用
在介绍多线程之前,先介绍一种需要使用到生产者-消费者的爬虫模式(Producer-Consumer-Spider PCS模式)。这种模式将爬虫集成为生产者、消费者模块。生产者负责处理输入数据,生成中间变量传递给消费者。消费者负责解析内容,生成输出数据。
在爬虫程序中,生产者往往是对url进行处理,发送请求获取响应。消费者往往是对响应页面进行解析,获取输出数据。
常用的多线程的方法
- 常规调用
开启多线程需要引入threading包,通过函数threading.Thread(target=fun, args=())即可创建线程。target参数为需要执行函数的函数名(不是调用不带括号),args参数为所调函数需要的参数元组。
可以通过常规的用法和线程进行耗时对比明显的差异
pcs模式
在常规爬虫的基础上采用生产者-消费者模式进行改进,引入队列(Queue)对数据进行更加复杂的操作,实现更加强大的功能。创建线程传入url_queue队列执行生产者方法得到html_queue队列,消费者方法依次从html_queue队列中获取数据执行解析方法,得到输出数据。直到两个队列为空时,结束线程。
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# 生产者
import queue
import random
import threading
import time
def do_craw(url_queue: queue.Queue, html_queue: queue.Queue):
while True:
# 从队列中移除并返回一个项目。
u = url_queue.get()
html_queue.put(u)
print(threading.current_thread().name,'url_queue.qsize=', url_queue.qsize())
time.sleep(random.randint(1, 2))
#消费者
def do_parse(html_queue: queue.Queue):
while True:
h = html_queue.get()
print(h)
print(threading.current_thread().name,'html_queue.qsize=', html_queue.qsize())
time.sleep(random.randint(1, 2))
if __name__ == "__main__":
# 创建一个队列对象
# 先进先出队列
# maxsize 是个整数,用于设置可以放入队列中的项目数的上限。当达到这个大小的时候,插入操作将阻塞至队列中的项目被消费掉。如果 maxsize 小于等于零,队列尺寸为无限大。
url_queue = queue.Queue(maxsize=0)
html_queue = queue.Queue()
list_name = ['小王', '小花', '小妞']
for name in list_name:
#将 name 放入队列
url_queue.put(name)
for _ in range(3):
t = threading.Thread(target=do_craw, args=(url_queue, html_queue), name=r'raw{}'.format(_))
t.start()
for id in range(2):
t = threading.Thread(target=do_parse, args=(html_queue,), name=r'do_parse{}'.format(id))
t.start()
线程池
尽管在使用多线程进行爬虫时可以提高程序运行效率,但是线程的创建和销毁都会消耗资源,过多的创建线程会导致线程浪费,增加运行成本。引入线程池对线程进行管理,当我们需要调用线程时从线程池中获取,用完之后再归还入池中,实现线程的循环使用,大大降低运行成本。创建一个线程池需要使用到concurrent.futures包中的ThreadPoolExecutor()方法
可以用with ThreadPoolExecutor()创建线程池,之后方法执行时会自动从池中获取线程并发执行。可以在ThreadPoolExecutor()中传入参数设置线程池信息。例如max_workers参数可以设置池中最大线程数。线程池的使用共有三种方法:
一次性提交
使用pool.map()方法一次性提交任务队列里的任务并得到所有结果。注意map()方法中有两个参数,一个是被执行的方法名,另一个是其所需参数集,必须是可迭代对象(*iterables)。
分步提交
使用pool.submit()方法可以依次从任务队列取出Task执行,并将其结果依次封装到future对象中,调用result()方法可以取得返回的结果。
分步提交加强版
使用as_completed()可以优先返回已经执行完的结果。在整个代码运行过程中,先执行完毕的线程先将其返回值封装到future对象中。对比第二种方法,减少了运行时间,提高了执行效率。