常规的爬虫

  • 缺点:
    耗时长、效率低、易崩溃

并发爬虫

原理

将整个爬虫程序分为cpu操作和IO操作两部分。cpu首先开始执行task,在遇到IO操作时,cpu会切换到另一个task开始执行,IO操作结束后,再通知cpu进行处理。由于IO操作读取内存、磁盘网络等不需要cpu的参与、两者可以同时进行,cpu可以释放出来执行其他的task实现加速。采用多线程并发操作执行程序可以大大降低运行时间,提高效率

python线程池如何给函数传递多个参数 python线程池并发爬虫_html

  • 优点:
    速度快、效率高、安全性高

应用

在介绍多线程之前,先介绍一种需要使用到生产者-消费者的爬虫模式(Producer-Consumer-Spider PCS模式)。这种模式将爬虫集成为生产者、消费者模块。生产者负责处理输入数据,生成中间变量传递给消费者。消费者负责解析内容,生成输出数据。

在爬虫程序中,生产者往往是对url进行处理,发送请求获取响应。消费者往往是对响应页面进行解析,获取输出数据。

python线程池如何给函数传递多个参数 python线程池并发爬虫_线程池_02

常用的多线程的方法

  • 常规调用

开启多线程需要引入threading包,通过函数threading.Thread(target=fun, args=())即可创建线程。target参数为需要执行函数的函数名(不是调用不带括号),args参数为所调函数需要的参数元组。

python线程池如何给函数传递多个参数 python线程池并发爬虫_html_03

可以通过常规的用法和线程进行耗时对比明显的差异

pcs模式

在常规爬虫的基础上采用生产者-消费者模式进行改进,引入队列(Queue)对数据进行更加复杂的操作,实现更加强大的功能。创建线程传入url_queue队列执行生产者方法得到html_queue队列,消费者方法依次从html_queue队列中获取数据执行解析方法,得到输出数据。直到两个队列为空时,结束线程。

python线程池如何给函数传递多个参数 python线程池并发爬虫_多线程_04

#!/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()

python线程池如何给函数传递多个参数 python线程池并发爬虫_html_05

线程池

尽管在使用多线程进行爬虫时可以提高程序运行效率,但是线程的创建和销毁都会消耗资源,过多的创建线程会导致线程浪费,增加运行成本。引入线程池对线程进行管理,当我们需要调用线程时从线程池中获取,用完之后再归还入池中,实现线程的循环使用,大大降低运行成本。创建一个线程池需要使用到concurrent.futures包中的ThreadPoolExecutor()方法

python线程池如何给函数传递多个参数 python线程池并发爬虫_多线程_06

可以用with ThreadPoolExecutor()创建线程池,之后方法执行时会自动从池中获取线程并发执行。可以在ThreadPoolExecutor()中传入参数设置线程池信息。例如max_workers参数可以设置池中最大线程数。线程池的使用共有三种方法:

一次性提交

使用pool.map()方法一次性提交任务队列里的任务并得到所有结果。注意map()方法中有两个参数,一个是被执行的方法名,另一个是其所需参数集,必须是可迭代对象(*iterables)。

分步提交

使用pool.submit()方法可以依次从任务队列取出Task执行,并将其结果依次封装到future对象中,调用result()方法可以取得返回的结果。

分步提交加强版

使用as_completed()可以优先返回已经执行完的结果。在整个代码运行过程中,先执行完毕的线程先将其返回值封装到future对象中。对比第二种方法,减少了运行时间,提高了执行效率。