python多线程爬虫与单线程爬虫效率效率对比

前言
我们之前写的爬虫都是单个线程的?这怎么够?一旦一个地方卡到不动了,那不就永远等待下去了?为此我们可以使用多线程或者多进程来处理。 首先声明一点! 多线程和多进程是不一样的!一个是 thread 库,一个是 multiprocessing 库。而多线程 thread 在 Python 里面被称作鸡肋的存在!而没错!本节介绍的是就是这个库 thread。 不建议你用这个,不过还是介绍下了,如果想看可以看看下面,不想浪费时间直接看 multiprocessing 多进程

鸡肋点
名言:
“Python 下多线程是鸡肋,推荐使用多进程!”

那当然有同学会问了,为啥?

背景
1、GIL 是什么? GIL 的全称是 Global Interpreter Lock (全局解释器锁),来源是 python 设计之初的考虑,为了数据安全所做的决定。 2、每个 CPU 在同一时间只能执行一个线程(在单核 CPU 下的多线程其实都只是并发,不是并行,并发和并行从宏观上来讲都是同时处理多路请求的概念。但并发和并行又有区别,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。) 在 Python 多线程下,每个线程的执行方式:

获取 GIL
执行代码直到 sleep 或者是 python 虚拟机将其挂起。
释放 GIL
可见,某个线程想要执行,必须先拿到 GIL,我们可以把 GIL 看作是 “通行证”,并且在一个 python 进程中,GIL 只有一个。拿不到通行证的线程,就不允许进入 CPU 执行。 在 Python2.x 里,GIL 的释放逻辑是当前线程遇见 IO 操作或者 ticks 计数达到 100(ticks 可以看作是 Python 自身的一个计数器,专门做用于 GIL,每次释放后归零,这个计数可以通过 sys.setcheckinterval 来调整),进行释放。 而每次释放 GIL 锁,线程进行锁竞争、切换线程,会消耗资源。并且由于 GIL 锁存在,python 里一个进程永远只能同时执行一个线程 (拿到 GIL 的线程才能执行),这就是为什么在多核 CPU 上,python 的多线程效率并不高。

那么是不是 python 的多线程就完全没用了呢?
在这里我们进行分类讨论: 1、CPU 密集型代码 (各种循环处理、计数等等),在这种情况下,由于计算工作多,ticks 计数很快就会达到阈值,然后触发 GIL 的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以 python 下的多线程对 CPU 密集型代码并不友好。 2、IO 密集型代码 (文件处理、网络爬虫等),多线程能够有效提升效率 (单线程下有 IO 操作会进行 IO 等待,造成不必要的时间浪费,而开启多线程能在线程 A 等待时,自动切换到线程 B,可以不浪费 CPU 的资源,从而能提升程序执行效率)。所以 python 的多线程对 IO 密集型代码比较友好。 而在 python3.x 中,GIL 不使用 ticks 计数,改为使用计时器(执行时间达到阈值后,当前线程释放 GIL),这样对 CPU 密集型程序更加友好,但依然没有解决 GIL 导致的同一时间只能执行一个线程的问题,所以效率依然不尽如人意。

多核性能
多核多线程比单核多线程更差,原因是单核下多线程,每次释放 GIL,唤醒的那个线程都能获取到 GIL 锁,所以能够无缝执行,但多核下,CPU0 释放 GIL 后,其他 CPU 上的线程都会进行竞争,但 GIL 可能会马上又被 CPU0 拿到,导致其他几个 CPU 上被唤醒后的线程会醒着等待到切换时间后又进入待调度状态,这样会造成线程颠簸 (thrashing),导致效率更低

多进程为什么不会这样?
每个进程有各自独立的 GIL,互不干扰,这样就可以真正意义上的并行执行,所以在 python 中,多进程的执行效率优于多线程 (仅仅针对多核 CPU 而言)。 所以在这里说结论:多核下,想做并行提升效率,比较通用的方法是使用多进程,能够有效提高执行效率。

目标网站https://image.so.com
在这里插入图片描述
将类别为图中红框中的图片,下载保存到本地。

1**.单线程代码**

import requests
from my_test import settings
import sys
import time
import pymysql


class DownLoadPictures(object):
    def __init__(self, sn):
        self.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
                                      '(KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36',
                        'Referer': 'https://image.so.com/z?ch=beauty'}
        self.url = 'https://image.so.com/zjl?ch=beauty&sn={}'.format(sn)

        self.conn = pymysql.Connect(**settings.MYSQL_CONFIG)
        self.cursor = self.conn.cursor()

    def __del__(self):
        self.cursor.close()
        self.conn.close()

    def get_resp_data(self):
        print('当前是链接为{}的图片下载!'.format(self.url))
        # 返回的数据在json里
        resp = requests.get(self.url, headers=self.headers)
        return resp.json()

    def get_download_url(self):
        resp_data = self.get_resp_data()
        # 判断是否还有图片
        if resp_data['end'] is False:
            for elem in resp_data['list']:
                downloadurl = elem['qhimg_downurl']
                fromUrl = elem['purl']
                title = elem['title']
                self.download_picture(downloadurl, title, fromUrl)
        else:
            print('链接为{}已无图片'.format(self.url))

    def download_picture(self, downloadurl, title, fromUrl):
        sql = "select * from beautyImages where downloadUrl = '{}' and title='{}'".format(downloadurl, title)
        row_count = self.cursor.execute(sql)
        if not row_count:
            try:
                resp = requests.get(downloadurl, headers=self.headers)
                if resp.status_code == requests.codes.ok:
                    with open(settings.STORE_PATH + '/' + title + '.jpg', 'wb') as f:
                        f.write(resp.content)
                print('下载完成')
                # 插入数据库
                insert_sql = "INSERT INTO beautyImages(title, downloadUrl, fromUrl, createTime) values (%s, %s, %s, %s)"
                try:
                    self.cursor.execute(insert_sql, (title, downloadurl, fromUrl, time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())))
                    self.conn.commit()
                    print('插入标题为{}, 链接为{}成功!'.format(title, downloadurl))
                except Exception:
                    print('插入标题为{}, 链接为{}失败, 失败原因是{}'.format(title, downloadurl, sys.exc_info()[1]))
            except Exception:
                print('标题为{}, 链接为{}下载失败,失败原因是{}'.format(title, downloadurl, sys.exc_info()[1]))
        else:
            print('标题为{}, 链接为{}已存在'.format(title, downloadurl))


if __name__ == '__main__':
    start_time = time.time()
    for i in range(0, 301, 30):
        test = DownLoadPictures(sn=i)
        test.get_download_url()
    use_time = time.time() - start_time
    print('单线程用时:{}秒'.format(use_time))

单线程程序运行结果为
在这里插入图片描述

在单线程的情况下,爬取和下载这十一个链接共花费了236s左右的时间。共获取了329张图片。
在这里插入图片描述

在这里插入图片描述

2.多线程代码

import requests
from my_test import settings
import sys
import time
import pymysql
import threading


# 继承父类threading.Thread
class DownLoadPictures(threading.Thread):
    def __init__(self, name, sn):
        super().__init__()
        self.name = name
        self.headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
                                      '(KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36',
                        'Referer': 'https://image.so.com/z?ch=beauty'}
        self.url = 'https://image.so.com/zjl?ch=beauty&sn={}'.format(sn)

        self.conn = pymysql.Connect(**settings.MYSQL_CONFIG)
        self.cursor = self.conn.cursor()

    def __del__(self):
        self.cursor.close()
        self.conn.close()

    def get_resp_data(self):
        # print('当前是链接为{}的图片下载!'.format(self.url))
        print('当前是线程为{}的图片下载!'.format(self.name))
        # 返回的数据在json里
        resp = requests.get(self.url, headers=self.headers)
        return resp.json()

    def run(self):
        # 重写run函数,线程在创建后会直接运行run函数
        resp_data = self.get_resp_data()
        # 判断是否还有图片
        if resp_data['end'] is False:
            for elem in resp_data['list']:
                downloadurl = elem['qhimg_downurl']
                fromUrl = elem['purl']
                title = elem['title']
                self.download_picture(downloadurl, title, fromUrl)
        else:
            print('链接为{}已无图片'.format(self.url))

    def download_picture(self, downloadurl, title, fromUrl):
        sql = "select * from beautyImages where downloadUrl = '{}' and title='{}'".format(downloadurl, title)
        row_count = self.cursor.execute(sql)
        if not row_count:
            try:
                resp = requests.get(downloadurl, headers=self.headers)
                if resp.status_code == requests.codes.ok:
                    with open(settings.STORE_PATH + '/' + title + '.jpg', 'wb') as f:
                        f.write(resp.content)
                print('下载完成')
                # 插入数据库
                insert_sql = "INSERT INTO beautyImages(title, downloadUrl, fromUrl, createTime) values (%s, %s, %s, %s)"
                try:
                    self.cursor.execute(insert_sql, (title, downloadurl, fromUrl, time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())))
                    self.conn.commit()
                    print('插入标题为{}, 链接为{}成功!'.format(title, downloadurl))
                except Exception:
                    print('插入标题为{}, 链接为{}失败, 失败原因是{}'.format(title, downloadurl, sys.exc_info()[1]))
            except Exception:
                print('标题为{}, 链接为{}下载失败,失败原因是{}'.format(title, downloadurl, sys.exc_info()[1]))
        else:
            print('标题为{}, 链接为{}已存在'.format(title, downloadurl))


if __name__ == '__main__':
    start_time = time.time()
    thread_list = []
    for i in range(0, 301, 30):
        test = DownLoadPictures(name=str(i), sn=i)
        thread_list.append(test)
    for t in thread_list:
        t.start()
    for t in thread_list:
        t.join()
    use_time = time.time() - start_time
    print('多线程用时:{}秒'.format(use_time))

多线程程序运行结果为:
在这里插入图片描述
在多线程的情况下,爬取和下载这十一个链接共花费了31左右的时间。共获取了330张图片。

在这里插入图片描述
在这里插入图片描述

8.总结
两份代码都是执行同一个任务,爬取360美女图片300多张图片并保存。但单线程程序花了236多的时间,而多线程程序只用了31s就完成了,大大提升了效率。

亲测有效,本文章全系对技术的兴趣爱好,欢迎大家学习交流。

  • 8
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值