一、背景

对于日常Python爬虫由于效率问题,本次测试使用多线程和Scrapy框架来实现抓取斗图啦表情。
由于IO操作不使用CPU,对于IO密集(磁盘IO/网络IO/人机交互IO)型适合用多线程,对于计算密集型:建议用多进程。

  • 进程:
    优点:充分利用多核CPU(能够同时进行多个操作)
    缺点:系统资源消耗大,重新开辟内存空间
  • 线程:
    优点:共享内存,IO操作可以创造出并发操作
    缺点:抢占资源,请求上下文切换消耗时间

但是对于python这种解释性语言带有GIL(全局解释器锁)解释器锁,同一时刻只能有一个线程在运行,遇到IO操作才会释放切换。感觉没必要多线程,但是经测试,多线程还是在很大程度能够提升效率。

二、代码

git地址
Python多线程爬图&Scrapy框架爬图

2.1 多线程爬图

定义了10个线程去爬去每个页面的具体表情的url存放在类中的img_url_list内,然后通过10个线程从这个列表内取url进行本地图片下载。
核心代码

    # 定义全局页面url列表
    page_url_list = []
    # 定义具体各表情图片url列表
    img_url_list = []
    # 定义rlock进程锁
    rlock = threading.RLock()

    def __init__(self,page_number=10,img_dir='imgdir',thread_number=5):
        """
        :param page_number: 抓去多少个页面,默认10
        :param img_dir: 定义图片目录
        :param thread_number:默认5个线程
        """
        self.spider_url = 'https://www.doutula.com/photo/list/?page='
        self.page_number = int(page_number)
        self.img_dir = img_dir
        self.thread_num = thread_number

 def __add_urllist(self):
        """
        定义从page_url_list 爬取具体的image的url
        :return:
        """
        while True:
            DutuSpider.rlock.acquire()
            if len(DutuSpider.page_url_list) == 0:
                DutuSpider.rlock.release()
                break
            else:
                page_url = DutuSpider.page_url_list.pop()
                DutuSpider.rlock.release()
                response = requests.get(page_url, headers=self.__set_header())
                soup = BeautifulSoup(response.content,'lxml')
                sou_list = soup.find_all('img',attrs={'class':'img-responsive lazy image_dta'})
                # 将获取到的具体表情图标的url保存添加进img_url_list 列表
                for url_content in sou_list:
                    DutuSpider.rlock.acquire()
                    DutuSpider.img_url_list.append(url_content['data-original'])
                    DutuSpider.rlock.release()

    def __download_img(self):
        """
        从image_url_list中来下载image到本地
        :return:
        """
        while True:
            DutuSpider.rlock.acquire()
            if len(DutuSpider.img_url_list) == 0:
                DutuSpider.rlock.release()
                continue
            else:
                img_url = DutuSpider.img_url_list.pop()
                DutuSpider.rlock.release()
                try:
                    # 图片名称
                    img_name = img_url.split('/')[-1]
                    # 下载图片
                    urllib.urlretrieve(img_url,os.path.join(self.img_dir,img_name))
                    print('donload img %s' % img_name)
                except Exception as e:
                    pass

    def run(self):
        # 启动thread_num个进程来爬去具体的img url 链接
        for th in range(self.thread_num):
            add_pic_t = threading.Thread(target=self.__add_urllist)
            add_pic_t.start()

        # 启动thread_num个来下载图片
        for img_th in range(self.thread_num):
            download_t = threading.Thread(target=self.__download_img)
            download_t.start()

2.2 Scrapy框架爬图

利用Scrapy框架来爬取表情,items定义图片名称和每个图片的url,scrapy主文件来爬取每个图片的url来返回,piplines来进行本地文件存储。
核心代码

# items,定义img的url和name
class ScrapyDoutulaiItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    # 定义图片url和name
    img_url = scrapy.Field()
    img_name = scrapy.Field()

# 爬虫文件
class DoutulaiSpiderSpider(scrapy.Spider):
    name = 'doutulai_spider'
    allowed_domains = ['www.doutula.com']
    start_urls = ['https://www.doutula.com/photo/list/']
    page = 1

    def parse(self, response):
        content_items = ScrapyDoutulaiItem()
        # 解析img_url列表,拿到图片的url和,图片名称
        img_url_list = response.xpath('//img[@class="img-responsive lazy image_dta"]')
        # page_number = response.xpath('//*[@id="pic-detail"]/div/div[3]/div[3]/ul/li[12]/a/text()').extract_first()
        page_number = response.xpath('//a[@class="page-link"][last()]/text()').extract_first()

        for img_content in img_url_list:
            content_items['img_url'] = img_content.xpath('./@data-original').extract_first()
            content_items['img_name'] = img_content.xpath('./@data-original').extract_first().split('/')[-1]
            print(content_items)
            yield content_items
        # 不断爬取新页面
        if self.page <= page_number:
            self.page += 1
            next_url = self.start_urls[0] + '?page=' + str(self.page)
            yield scrapy.Request(next_url)

#pipeline下载图片
from urllib import urlretrieve
from scrapy_doutulai.settings import DOWNLOAD_DIR

class ScrapyDoutulaiPipeline(object):
    def __init__(self):
        """
        判断下载目录是否存在
        """
        if not os.path.exists(DOWNLOAD_DIR):
            os.makedirs(DOWNLOAD_DIR)

    def process_item(self, item, spider):
        """
        下载图片
        :param item:
        :param spider:
        :return:
        """
        try:
            filename = os.path.join(DOWNLOAD_DIR,item['img_name'])
            print(filename)
            urlretrieve(item['img_url'],filename)
        except Exception as e:
            pass

三、测试

测试使用2C2G centos7.4,python2.7版本,启动线程10个,爬去1000页的表情信息

3.1 多线程测试

  • 启动爬虫
    nohup doutulai/multithreading_spider/dutulai_spider.py &
  • 查看系统负载
    Python多线程爬图&Scrapy框架爬图
    Python多线程爬图&Scrapy框架爬图
  • 查看文件信息
    Python多线程爬图&Scrapy框架爬图

    3.2 Scrapy框架爬图

  • 启动爬虫
    nohup doutulai/scrapy_doutulai/scrapy_doutulai/main.py &
  • 查看系统负载
    Python多线程爬图&Scrapy框架爬图
    Python多线程爬图&Scrapy框架爬图
  • 查看文件信息
    Python多线程爬图&Scrapy框架爬图

  • 爬取的图片
    Python多线程爬图&Scrapy框架爬图

    3.3 持久化存储在OSS上

    最终配合阿里云OSS的API来将图片持久化存储在对象存储内。
    整体image下载地址:图片压缩包

Python多线程爬图&Scrapy框架爬图
Python多线程爬图&Scrapy框架爬图

四、总结

  • 经测试自己写的多线程爬图,CPU使用率很高,磁盘IO很大。Scrapy默认也是10个线程,但由于自己有磁盘IO操作,CPU使用平稳。
  • 虽然Python有GIL,但是在适当的场景下利用其多线程会很大程度的提升效率。之前如果单线程10分钟,利用多线程可以缩短3/2的 时间,具体需要结合线程数,磁盘与网络IO来判断。