十四、高性能爬虫之单线程、多进程、多线程的使用、线程池、进程池的使用

一、单线程爬虫

1、爬取糗事百科段子

页面的URL是:http://www.qiushibaike.com/8hr/page/1

思路分析:

- 确定url地址

url地址的规律非常明显,一共只有13页url地址
在这里插入图片描述
- 确定数据的位置

数据都在类为class="recmd-content"的a标签中,
在这里插入图片描述

2、糗事百科代码实现
import requests
from lxml import etree
import datetime


class ChouShiBaiKe():

    def __init__(self):
        url = "http://www.qiushibaike.com/8hr/page/{}"
        # 构建构建每页url地址
        self.url_list = [ url.format(i) for i in range(1,14)]
        self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"}
    def get_html(self,html_url):
        # 向标题页发送请求,获取响应内容
        html_url_resp = requests.get(html_url, headers=self.headers)
        return html_url_resp.content

    def get_items(self,html_url_resp):
        # 获取每页的标题和标题url
        # 获取可以xpath的对象
        html_url_element = etree.HTML(html_url_resp)
        # 进行xpath提取标题和url
        html_url_a = html_url_element.xpath('//a[@class="recmd-content"]')
        # print(len(html_url_a))
        for a in html_url_a:
            item = {}
            # print(type(a))
            # 通过xpath得到的是列表,[0]是为了将列表中的内容取出来
            if a.xpath('./text()') != []:  #为空说明是广告
                item['title'] = a.xpath('./text()')[0]
                item['title_url'] = a.xpath('./@href')[0]
                self.save(item)



    def save(self,item):
        print(item)


    def run(self):

        for html_url in self.url_list:
            # 获取该页的标题详情
            html_url_resp = self.get_html(html_url)
            # 获取每页标题和标题url
            self.get_items(html_url_resp)



if __name__ == '__main__':
    start_time = datetime.datetime.now()
    spider = ChouShiBaiKe()
    spider.run()
    end_time = datetime.datetime.now()
    print('单线程消耗时间{}'.format(end_time-start_time))
    # 单线程消耗时间0:00:06.377732

二、 多线程爬虫

  在前面爬虫基础知识案例中我们发现请求回来的总数据不是太多,时间性对来说还是比较快的,那么如果该网站有大量数据等待爬虫爬取,我们是不是需要使用多线程并发来操作爬虫的网络请求呢?

1、回顾多线程的方法使用

在python3中,主线程主进程结束,子线程,子进程不会结束

  为了能够让主线程回收子线程,可以把子线程设置为守护线程,即该线程不重要,主线程结束,子线程结束

t1 = threading.Thread(targe=func,args=(,))
t1.setDaemon(True) # 设置为守护线程
t1.start() #此时线程才会启动
2、回顾队列模块的使用
from queue import Queue
q = Queue(maxsize=100) # maxsize为队列长度
item = {}
q.put_nowait(item) #不等待直接放,队列满的时候会报错
q.put(item) #放入数据,队列满的时候会阻塞等待
q.get_nowait() #不等待直接取,队列空的时候会报错
q.get() #取出数据,队列为空的时候会阻塞等待
q.qsize() #获取队列中现存数据的个数 
q.join() # 队列中维持了一个计数(初始为0),计数不为0时候让主线程阻塞等待,队列计数为0的时候才会继续往后执行
         # q.join()实际作用就是阻塞主线程,与task_done()配合使用
         # put()操作会让计数+1,task_done()会让计数-1
         # 计数为0,才停止阻塞,让主线程继续执行
q.task_done() # put的时候计数+1,get不会-1,get需要和task_done 一起使用才会-1
3、多线程实现思路剖析

把爬虫中的每个步骤封装成函数,分别用线程去执行不同的函数通过队列相互通信,函数间解耦
在这里插入图片描述

4、具体代码实现
import requests
from lxml import etree
import datetime
from queue import Queue
from threading import Thread

class ChouShiBaiKe():

    def __init__(self):
        self.base_url = "http://www.qiushibaike.com/8hr/page/{}"
        # 构建构建每页url地址
        self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"}
        # 创建队列
        self.q_url = Queue(10)
        self.q_html = Queue(10)
        self.q_item = Queue(10)


    # 生产url
    def get_url(self):
        for i in range(1, 14):
            url = self.base_url.format(i)
            self.q_url.put(url)

    # 获取对应url的response内容
    def get_html(self):
        while True:
            # 向标题页发送请求,获取响应内容
            url = self.q_url.get()
            html_url_resp = requests.get(url, headers=self.headers)
            self.q_html.put(html_url_resp.content)
            self.q_url.task_done() # 计数 -1
    # 获取每页的标题和url
    def get_items(self):
        while True:
            # 获取每页的响应内容
            html_url_resp = self.q_html.get()
            # 获取可以xpath的对象
            html_url_element = etree.HTML(html_url_resp)
            # 进行xpath提取标题和url
            html_url_a = html_url_element.xpath('//a[@class="recmd-content"]')
            # print(len(html_url_a))
            titles = []
            for a in html_url_a:
                # print(type(a))
                # 通过xpath得到的是列表,[0]是为了将列表中的内容取出来
                if a.xpath('./text()') != []:  #为空说明是广告
                    item = {}
                    item['title'] = a.xpath('./text()')[0]
                    item['title_url'] = a.xpath('./@href')[0]
                    titles.append(item)

            self.q_item.put(titles)
            self.q_html.task_done()

    def save(self):
        while True:
            items = self.q_item.get()
            for item in items:
                print(item)
            self.q_item.task_done()


    def run(self):
        thread_list =[]
        t_url = Thread(target=self.get_url)
        thread_list.append(t_url)

        for i in range(3):
            t_html = Thread(target=self.get_html)
            thread_list.append(t_html)

        for i in range(3):
            t_items = Thread(target=self.get_items)
            thread_list.append(t_items)

        t_save = Thread(target=self.save)
        thread_list.append(t_save)

        for t in thread_list:
            # t.setDaemon(True) # 设置为守护线程
            t.start() #此时线程才会启动

        for q in [self.q_url, self.q_html, self.q_item]:
            q.join()  # 主线程阻塞,直到每个q队列计数为0
            print('程序结束了')


if __name__ == '__main__':
    start_time = datetime.datetime.now()
    spider = ChouShiBaiKe()
    spider.run()
    end_time = datetime.datetime.now()
    print('多线程消耗时间{}'.format(end_time-start_time))
	
	# 单线程消耗时间0:00:06.377732
    # 多线程消耗时间0:00:02.270735
注意点:
  • put会让队列的计数+1,但是单纯的使用get不会让其-1,需要和task_done同时使用才能够-1。
  • task_done不能放在另一个队列的put之前,否则可能会出现数据没有处理完成,程序结束的情况。

三、多进程爬虫

  在一个进程中无论开多少个线程都只能运行在一个CPU的核心之上,这是python的特点,不能说是缺点!

  如果我们想利用计算机的多核心优势,就可以用多进程的方式实现,思路和多线程相似,只是对应的api不相同。

1、回顾多进程程的方法使用
from multiprocessing import Process  #导入模块
t1 = Process(targe=func,args=(,)) #使用一个进程来执行一个函数
t1.daemon = True  #设置为守护进程
t1.start() #此时线程才会启动
2、多进程中队列的使用

 &emsp多进程中使用普通的队列模块会发生阻塞,对应的需要使multiprocessing提供的JoinableQueue模块,其使用过程和在线程中使用的queue方法相同

3 具体实现

具体的实现如下:

import requests
from lxml import etree
import datetime
from multiprocessing import Process
from multiprocessing import JoinableQueue as Queue

class ChouShiBaiKe():

    def __init__(self):
        self.base_url = "http://www.qiushibaike.com/8hr/page/{}"
        # 构建构建每页url地址
        self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"}
        # 创建队列
        self.q_url = Queue(10)
        self.q_html = Queue(10)
        self.q_item = Queue(10)


    # 生产url
    def get_url(self):
        for i in range(1, 14):
            url = self.base_url.format(i)
            self.q_url.put(url)

    # 获取对应url的response内容
    def get_html(self):
        while True:
            # 向标题页发送请求,获取响应内容
            url = self.q_url.get()
            html_url_resp = requests.get(url, headers=self.headers)
            self.q_html.put(html_url_resp.content)
            self.q_url.task_done()
    # 获取每页的标题和url
    def get_items(self):
        while True:
            # 获取每页的响应内容
            html_url_resp = self.q_html.get()
            # 获取可以xpath的对象
            html_url_element = etree.HTML(html_url_resp)
            # 进行xpath提取标题和url
            html_url_a = html_url_element.xpath('//a[@class="recmd-content"]')
            # print(len(html_url_a))
            titles = []
            for a in html_url_a:
                # print(type(a))
                # 通过xpath得到的是列表,[0]是为了将列表中的内容取出来
                if a.xpath('./text()') != []:  #为空说明是广告
                    item = {}
                    item['title'] = a.xpath('./text()')[0]
                    item['title_url'] = a.xpath('./@href')[0]
                    titles.append(item)

            self.q_item.put(titles)
            self.q_html.task_done()

    def save(self):
        while True:
            items = self.q_item.get()
            for item in items:
                print(item)
            self.q_item.task_done()


    def run(self):
        process_list =[]
        p_url = Process(target=self.get_url)
        process_list.append(p_url)

        for i in range(2):
            p_html = Process(target=self.get_html)
            process_list.append(p_html)

        for i in range(2):
            p_items = Process(target=self.get_items)
            process_list.append(p_items)

        p_save = Process(target=self.save)
        process_list.append(p_save)

        for t in process_list:
            # t.setDaemon(True) # 设置为守护线程
            t.start() #此时线程才会启动

        for q in [self.q_url, self.q_html, self.q_item]:
            q.join()  # 主线程阻塞,直到每个q队列计数为0
            print('程序结束了')


if __name__ == '__main__':
    start_time = datetime.datetime.now()
    spider = ChouShiBaiKe()
    spider.run()
    end_time = datetime.datetime.now()
    print('多进程消耗时间{}'.format(end_time-start_time))
    
	# 单线程消耗时间0:00:06.377732
    # 多线程消耗时间0:00:02.270735
	# 多进程消耗时间0: 00:03.296609

  上述多进程实现的代码中,multiprocessing提供的JoinableQueue可以创建可连接的共享进程队列。和普通的Queue对象一样,队列允许项目的使用者通知生产者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。 对应的该队列能够和普通队列一样能够调用task_done和join方法。

小结
  • multiprocessing导包:from multiprocessing import Process
  • 创建进程: Process(target=self.get_url_list)
  • 添加入队列: put
  • 从队列获取:get
  • 守护线程:t.daemon=True
  • 主线程阻塞: q.join()
  • 跨进程通讯可以使用from multiprocessing import JoinableQueue as Queue

三、线程池实现爬虫

1、程池使用方法介绍
  • 实例化线程池对象
 from multiprocessing.dummy import Pool
 pool = Pool(processes=3) # 默认大小是cpu的个数
 """源码内容:
 if processes is None:
     processes = os.cpu_count() or 1 
     # 此处or的用法:
         默认选择or前边的值,
         如果or前边的值为False,就选择后边的值
 """
  • 把从发送请求,提取数据,到保存合并成一个函数,交给线程池异步执行

使用方法pool.apply_async(func)

 def exetute_requests_item_save(self):
     url = self.queue.get()
     html_str = self.parse_url(url)
     content_list = self.get_content_list(html_str)
     self.save_content_list(content_list)
     self.total_response_num +=1

 pool.apply_async(self.exetute_requests_item_save)
  • 添加回调函数

通过apply_async的方法能够让函数异步执行,但是只能够执行一次,为了让其能够被反复执行,通过添加回调函数的方式能够让_callback 递归的调用自己,同时需要指定递归退出的条件。

 def _callback(self,temp):
     if self.is_running:
          pool.apply_async(self.exetute_requests_item_save,callback=self._callback)

pool.apply_async(self.exetute_requests_item_save,callback=self._callback)

  • 确定程序结束的条件 程序在获取的响应和url数量相同的时候可以结束
 while True: #防止主线程结束
     time.sleep(0.0001)  #避免cpu空转,浪费资源
     if self.total_response_num>=self.total_requests_num:
         self.is_running= False
         break
 self.pool.close() #关闭线程池,防止新的线程开启
# self.pool.join() #等待所有的子线程结束
2、使用线程池实现爬虫的具体实现
import requests
from lxml import etree
import datetime
from queue import Queue
from multiprocessing.dummy import Pool
import time


class ChouShiBaiKe():

    def __init__(self):
        self.url = "http://www.qiushibaike.com/8hr/page/{}"
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36'
        }
        # 构建构建每页url地址
        # self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"}
        # 创建队列
        self.q_url = Queue()
        # 创建进程池对象
        self.pool = Pool(5)
        # 请求url总数据
        self.total_request_num = 0
        # 获取响应总数
        self.total_response_num = 0
        # 停止回调标志
        self.is_running = True


    # 获取url
    def get_url(self):
        for i in range(1, 14):
            url = self.url.format(i)
            # 将生成的url放入到队列中
            self.q_url.put(url)
            self.total_request_num += 1

    # 向标题页发送请求,获取响应内容
    def get_html(self,url):
        resp = requests.get(url, headers=self.headers)

        return resp.content

    # 获取每页的标题和标题url
    def get_items(self,html_resp):
        # 获取可以xpath的对象
        html_url_element = etree.HTML(html_resp)
        # 进行xpath提取标题和url
        html_url_a = html_url_element.xpath('//a[@class="recmd-content"]')
        # print(len(html_url_a))
        titles = []
        for a in html_url_a:
            item = {}
            # print(type(a))
            # 通过xpath得到的是列表,[0]是为了将列表中的内容取出来
            if a.xpath('./text()') != []:  #为空说明是广告
                item['title'] = a.xpath('./text()')[0]
                item['title_url'] = a.xpath('./@href')[0]
                titles.append(item)
        return titles

    # 保存
    def save(self,titles):
        for t in titles:
            print(t)

    # 完整的执行流程
    def execute_request_items_save(self):
        url = self.q_url.get()
        html_resp = self.get_html(url)
        titles = self.get_items(html_resp)
        self.save(titles)
        self.total_response_num += 1
        return '完整执行'

    # 回调函数
    def _callback(self,xxx):# callback函数必须接收一个参数!
        # xxx参数是self.execute_request_item_save的返回值!!!
        # 哪怕用不上 也必须接收!
        print(xxx)
        if self.is_running:
            self.pool.apply_async(self.execute_request_items_save, callback=self._callback)


    def run(self):
        self.get_url()

        for i in range(5):
            self.pool.apply_async(self.execute_request_items_save, callback=self._callback)
        # 退出机制
        while True:
            #避免cpu空转,浪费资源
            time.sleep(0.0001)
            if self.total_response_num == self.total_request_num and self.total_request_num == 13:
                self.is_running = False
                break
        self.pool.close()

        print('程序执行结束')


if __name__ == '__main__':
    start_time = datetime.datetime.now()
    spider = ChouShiBaiKe()
    spider.run()
    end_time = datetime.datetime.now()
    print('多线程池消耗时间{}'.format(end_time-start_time))


	# 单线程消耗时间0:00:06.377732
	# 多进程消耗时间0: 00:03.296609
    # 多线程池消耗时间0:00:01.761484
小结
  • 线程池导包: from multiprocessing.dummy import Pool
  • 线程池的创建:pool = Pool(process=3)
  • 线程池异步方法:pool.apply_async(func)

五、协程池实现爬虫

1、协程池模块使用介绍

协程池模块

import gevent.monkey
 gevent.monkey.patch_all()
 from gevent.pool import Pool
2、使用协程池实现爬虫的具体实现过程
import gevent.monkey
# monkey补丁要打在发送请求之前
gevent.monkey.patch_all()
import requests
from lxml import etree
import datetime
from queue import Queue
from gevent.pool import Pool
import time


class ChouShiBaiKe():

    def __init__(self):
        self.url = "http://www.qiushibaike.com/8hr/page/{}"
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36'
        }
        # 构建构建每页url地址
        # self.headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36"}
        # 创建队列
        self.q_url = Queue()
        # 创建进程池对象
        self.pool = Pool(5)
        # 请求url总数据
        self.total_request_num = 0
        # 获取响应总数
        self.total_response_num = 0
        # 停止回调标志
        self.is_running = True


    # 获取url
    def get_url(self):
        for i in range(1, 14):
            url = self.url.format(i)
            # 将生成的url放入到队列中
            self.q_url.put(url)
            self.total_request_num += 1

    # 向标题页发送请求,获取响应内容
    def get_html(self,url):
        resp = requests.get(url, headers=self.headers)

        return resp.content

    # 获取每页的标题和标题url
    def get_items(self,html_resp):
        # 获取可以xpath的对象
        html_url_element = etree.HTML(html_resp)
        # 进行xpath提取标题和url
        html_url_a = html_url_element.xpath('//a[@class="recmd-content"]')
        # print(len(html_url_a))
        titles = []
        for a in html_url_a:
            item = {}
            # print(type(a))
            # 通过xpath得到的是列表,[0]是为了将列表中的内容取出来
            if a.xpath('./text()') != []:  #为空说明是广告
                item['title'] = a.xpath('./text()')[0]
                item['title_url'] = a.xpath('./@href')[0]
                titles.append(item)
        return titles

    # 保存
    def save(self,titles):
        for t in titles:
            print(t)

    # 完整的执行流程
    def execute_request_items_save(self):
        url = self.q_url.get()
        html_resp = self.get_html(url)
        titles = self.get_items(html_resp)
        self.save(titles)
        self.total_response_num += 1
        return '完整执行'

    # 回调函数
    def _callback(self,xxx):# callback函数必须接收一个参数!
        # xxx参数是self.execute_request_item_save的返回值!!!
        # 哪怕用不上 也必须接收!
        print(xxx)
        if self.is_running:
            self.pool.apply_async(self.execute_request_items_save, callback=self._callback)


    def run(self):
        self.get_url()

        for i in range(5):
            self.pool.apply_async(self.execute_request_items_save, callback=self._callback)
        # 退出机制
        while True:
            #避免cpu空转,浪费资源
            time.sleep(0.0001)
            if self.total_response_num == self.total_request_num and self.total_request_num == 13:
                self.is_running = False
                break
        # self.pool.close()   # 协程没有过close函数
        print('程序执行结束')


if __name__ == '__main__':
    start_time = datetime.datetime.now()
    spider = ChouShiBaiKe()
    spider.run()
    end_time = datetime.datetime.now()
    print('多协程池消耗时间{}'.format(end_time-start_time))

	# 单线程消耗时间0:00:06.377732
	# 多进程消耗时间0: 00:03.296609
    # 多线程池消耗时间0:00:01.761484
    # 多协程池消耗时间0:00:01.689546

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值