python3 爬虫多线程,进程,协程案例(不看后悔系列)

367 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。


并行 和 并发


并行:多个CPU核心,不同的程序就分配给不同的CPU来运行。可以让多个程序同时执行。

并发:单个CPU核心,在一个时间切片里一次只能运行一个程序,如果需要运行多个程序,则串行执行。

cpu1  ----  ----

cpu1    ----  ----(为什么Redis存储速度这么快,单核 并发处理,内部数据使用特此加密,独特的纹码,一堆多,一对一等匹配更快)

Python多线程

多线程伴随着GIL 

GIL 全局解释器锁:线程的执行权限,在Python的进程里只有一个GIL。

一个线程需要执行任务,必须获取GIL。

好处:直接杜绝了多个线程访问内存空间的安全问题。
坏处:Python的多线程不是真正多线程,不能充分利用多核CPU的资源。

但是,在I/O阻塞的时候,解释器会释放GIL。

多线程代码示例

# -*- coding: utf-8 -*-
# Author       :   szy
# Create Date  :   2019/11/20
 
 
from threading import Thread
from queue import Queue
import time
from lxml import etree
import requests
 
 
class DouBanSpider(Thread):
    def __init__(self, url, q):
        # 重写写父类的__init__方法
        super(DouBanSpider, self).__init__()
        self.url = url
        self.q = q
        self.headers = {
            'Cookie': 'll="118282"; bid=ctyiEarSLfw; ps=y; __yadk_uid=0Sr85yZ9d4bEeLKhv4w3695OFOPoedzC; dbcl2="155150959:OEu4dds1G1o"; as="https://sec.douban.com/b?r=https%3A%2F%2Fbook.douban.com%2F"; ck=fTrQ; _pk_id.100001.4cf6=c86baf05e448fb8d.1506160776.3.1507290432.1507283501.; _pk_ses.100001.4cf6=*; __utma=30149280.1633528206.1506160772.1507283346.1507290433.3; __utmb=30149280.0.10.1507290433; __utmc=30149280; __utmz=30149280.1506160772.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utma=223695111.1475767059.1506160772.1507283346.1507290433.3; __utmb=223695111.0.10.1507290433; __utmc=223695111; __utmz=223695111.1506160772.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); push_noty_num=0; push_doumail_num=0',
            'Host': 'movie.douban.com',
            'Referer': 'https://movie.douban.com/top250?start=225&filter=',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36',
        }
 
    def run(self):
        self.parse_page()
 
    def send_request(self, url):
        '''
        用来发送请求的方法
        :return: 返回网页源码
        '''
        # 请求出错时,重复请求3次,
        i = 0
        while i <= 3:
            try:
                print(u"[INFO]请求url:" + url)
 
                html = requests.get(url=url, headers=self.headers).content
            except Exception as e:
                print(u'[INFO] %s%s' % (e, url))
 
                i += 1
            else:
                return html
 
    def parse_page(self):
        '''
        解析网站源码,并采用xpath提取 电影名称和平分放到队列中
        :return:
        '''
        response = self.send_request(self.url)
        html = etree.HTML(response)
        #  获取到一页的电影数据
        node_list = html.xpath("//div[@class='info']")
        for move in node_list:
            # 电影名称
            title = move.xpath('.//a/span/text()')[0]
            # 评分
            score = move.xpath(
                './/div[@class="bd"]//span[@class="rating_num"]/text()')[0]
 
            # 将每一部电影的名称跟评分加入到队列
            self.q.put(score + "\t" + title)
 
 
def main():
    # 创建一个队列用来保存进程获取到的数据
    q = Queue()
    base_url = 'https://movie.douban.com/top250?start='
    # 构造所有url
    url_list = [base_url + str(num) for num in range(0, 225 + 1, 25)]
 
    # 保存线程
    Thread_list = []
    # 创建并启动线程
    for url in url_list:
        p = DouBanSpider(url, q)
        p.start()
        Thread_list.append(p)
 
    # 让主线程等待子线程执行完成
    for i in Thread_list:
        i.join()
 
    while not q.empty():
        print(q.get())
 
 
 
if __name__ == "__main__":
    start = time.time()
    main()
    print('[info]耗时:%s' % (time.time() - start))
 

python多进程:

密集CPU任务,需要充分使用多核CPU资源(服务器,大量的并行计算)的时候,用多进程。 (multiprocessing库)
缺陷:多个进程之间通信成本高,切换开销大。
多线程:密集I/O任务(网络I/O,磁盘I/O,数据库I/O)使用多线程合适。
(threading.Thread、multiprocessing.dummy等库)
缺陷:同一个时间切片只能运行一个线程,不能做到高并行,但是可以做到高并发。

多进程代码示例

# -*- coding: utf-8 -*-
# Author       :   szy
# Create Date  :   2019/11/20
from multiprocessing import Process, Queue  # 可以理解多进程为并行
import time
from lxml import etree
import requests
 
 
class DouBanSpider(Process):  # 继承类
    def __init__(self, url, q):
        # 重写写父类的__init__方法
        super(DouBanSpider, self).__init__()
        self.url = url
        self.q = q
        self.headers = {
            'Host': 'movie.douban.com',
            'Referer': 'https://movie.douban.com/top250?start=225&filter=',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36',
        }
 
    def run(self):
        self.parse_page()
 
    def send_request(self, url):
        '''
        用来发送请求的方法
        :return: 返回网页源码
        '''
        # 请求出错时,重复请求3次,
        i = 0
        while i <= 3:
            try:
                print(u"[INFO]请求url:" + url)
 
                return requests.get(url=url, headers=self.headers).content
            except Exception as e:
                print(u'[INFO] %s%s' % (e, url))
 
                i += 1
 
    def parse_page(self):
        '''
        解析网站源码,并采用xpath提取 电影名称和平分放到队列中
        :return:
        '''
        response = self.send_request(self.url)
        html = etree.HTML(response)
        #  获取到一页的电影数据
        node_list = html.xpath("//div[@class='info']")
        for move in node_list:
            # 电影名称
            title = move.xpath('.//a/span/text()')[0]
            # 评分
            score = move.xpath(
                './/div[@class="bd"]//span[@class="rating_num"]/text()')[0]
 
            # 将每一部电影的名称跟评分加入到队列
            self.q.put(score + "\t" + title)
 
 
def main():
    # 创建一个队列用来保存进程获取到的数据
    q = Queue()
    base_url = 'https://movie.douban.com/top250?start='
    # 构造所有url
    url_list = [base_url + str(num) for num in range(0, 225 + 1, 25)]
 
    # 保存进程
    Process_list = []
    # 创建并启动进程
    for url in url_list:
        p = DouBanSpider(url, q)
        p.start()
        Process_list.append(p)
 
    # 让主进程等待子进程执行完成
    for i in Process_list:
        i.join()
 
    while not q.empty():
        print(q.get())
 
 
if __name__ == "__main__":
    start = time.time()
    main()
    print('[info]耗时:%s' % (time.time() - start))

python协程:

又称微线程,在单线程上执行多个任务,用函数切换,开销极小。不通过操作系统调度,没有进程、线程的切换开销。genvent,monkey.patchall

多线程请求返回是无序的,那个线程有数据返回就处理那个线程,而协程返回的数据是有序的。

缺陷:单线程执行,处理密集CPU和本地磁盘IO的时候,性能较低。处理网络I/O性能还是比较高.

协程示例

# -*- coding: utf-8 -*-
# Author       :   szy
# Create Date  :   2019/11/20
 
#!/usr/bin/env python2
# -*- coding=utf-8 -*-
 
from queue import Queue
import time
from lxml import etree
import requests
import gevent
 
# 打上猴子补丁
from gevent import monkey
monkey.patch_all()
 
 
class DouBanSpider(object):
    def __init__(self):
        # 创建一个队列用来保存进程获取到的数据
        self.q = Queue()
        self.headers = {
            'Cookie': 'll="118282"; bid=ctyiEarSLfw; ps=y; __yadk_uid=0Sr85yZ9d4bEeLKhv4w3695OFOPoedzC; dbcl2="155150959:OEu4dds1G1o"; as="https://sec.douban.com/b?r=https%3A%2F%2Fbook.douban.com%2F"; ck=fTrQ; _pk_id.100001.4cf6=c86baf05e448fb8d.1506160776.3.1507290432.1507283501.; _pk_ses.100001.4cf6=*; __utma=30149280.1633528206.1506160772.1507283346.1507290433.3; __utmb=30149280.0.10.1507290433; __utmc=30149280; __utmz=30149280.1506160772.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utma=223695111.1475767059.1506160772.1507283346.1507290433.3; __utmb=223695111.0.10.1507290433; __utmc=223695111; __utmz=223695111.1506160772.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); push_noty_num=0; push_doumail_num=0',
            'Host': 'movie.douban.com',
            'Referer': 'https://movie.douban.com/top250?start=225&filter=',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.104 Safari/537.36',
        }
 
    def run(self, url):
        self.parse_page(url)
 
    def send_request(self, url):
        '''
        用来发送请求的方法
        :return: 返回网页源码
        '''
        # 请求出错时,重复请求3次,
        i = 0
        while i <= 3:
            try:
                print(u"[INFO]请求url:" + url)
                html = requests.get(url=url, headers=self.headers).content
            except Exception as e:
                print(u'[INFO] %s%s' % (e, url))
                i += 1
            else:
                return html
 
    def parse_page(self, url):
        '''
        解析网站源码,并采用xpath提取 电影名称和平分放到队列中
        :return:
        '''
        response = self.send_request(url)
        html = etree.HTML(response)
        # 获取到一页的电影数据
        node_list = html.xpath("//div[@class='info']")
        for move in node_list:
            # 电影名称
            title = move.xpath('.//a/span/text()')[0]
            # 评分
            score = move.xpath(
                './/div[@class="bd"]//span[@class="rating_num"]/text()')[0]
 
            # 将每一部电影的名称跟评分加入到队列
            self.q.put(score + "\t" + title)
 
    def main(self):
 
        base_url = 'https://movie.douban.com/top250?start='
        # 构造所有url
        url_list = [base_url + str(num) for num in range(0, 225 + 1, 25)]
        # 创建协程并执行
        job_list = [gevent.spawn(self.run, url) for url in url_list]
        # 让线程等待所有任务完成,再继续执行。
        gevent.joinall(job_list)
 
        while not self.q.empty():
            print(self.q.get())
 
 
if __name__ == "__main__":
    start = time.time()
    douban = DouBanSpider()
    douban.main()
    print('[info]耗时:%s' % (time.time() - start))

 

Gevent额外说明

Gevent是一种基于协程的Python网络库,它用到Greenlet提供的,封装了libevent事件循环的高层同步API。它让开发者在不改变编程习惯的同时,用同步的方式写异步I/O的代码。

使用Gevent的性能确实要比用传统的线程高,甚至高很多。另外补充2点:

  1. Monkey-patching,我们都叫猴子补丁,因为如果使用了这个补丁,Gevent直接修改标准库里面大部分的阻塞式系统调用,包括socket、ssl、threading和 select等模块,而变为协作式运行。但是我们无法保证你在复杂的生产环境中有哪些地方使用这些标准库会由于打了补丁而出现奇怪的问题
  2. 第三方库支持。得确保项目中用到其他用到的网络库也必须使用纯Python或者明确说明支持Gevent
  3. gevent.spawn()方法会创建一个新的greenlet协程对象,并运行它

    gevent.joinall()方法的参数是一个协程对象列表,它会等待所有的协程都执行完毕后再退出

 更多协程参考链接:

永恒的记忆(和我上面写的风格一样):www.cnblogs.com/lucky-heng/…

廖雪峰(基础入门):www.liaoxuefeng.com/wiki/897692…

量化交易学习地址(暂弃):yun.itheima.com/course/546.…

gevent和asyncio使用方法详解blog.csdn.net/weixin_4162…

【多进程、多线程、协程+异步】对比测试:blog.csdn.net/Newyee/arti…

Trick:python使用协程是很多大佬的习惯,不用考虑各种限制,充分利用python单线程