在本节实践中,我们将借助Python多线程编程并采用生产者消费者模式来编写爬取Bing每日壁纸的爬虫。在正式编程前,我们还是一样地先来分析一下我们的需求及大体实现的过程。
总体设计预览
首先,我们先来看一下第三方提供的Bing壁纸网站http://bing.plmeizi.com/。在这一个网站中保存了以往的Bing每日壁纸,往下滑动也可以看到其目前一共有88页(即2016年9月至今)。
接着我们像之前一样来分析每页的URL构成法,这里不难分析得知其构成法为:http://bing.plmeizi.com/?page={index},其中index代表页码数(具体分析方法可参看之前的实践博客)。
然后我们再分析每页所需解析的数据源代码。如下图所示,通过简单地分析HTML源码我们便可以确定所需获取的图片数据被包含在了一个list类的div标签下的a标签中,并且这个a标签下有两个子节点——img图片标签和p文字说明标签。
如此一来,我们的目标和过程也就明确了,先获取每页的URL,然后对每页进行HTML解析并获取图片的img和说明内容,从而可利用其源地址src进行下载保存以达到爬取图片的目的。
爬取的发送请求和解析过程在之前的实践中已介绍,在此不在叙述,本节主要介绍如何利用生产者消费者模式来编写多线程爬虫。
生产者消费者模式分析
首先,我们来认识一下这里我们所设定的生产者消费者角色都是什么。生产者即生产资源的角色,这里我们可以让其专门负责解析每页URL并获取图片的src和说明内容;而消费者即消费资源的角色,这里我们可以指定其专门从以获取的src那里取出图片源地址进行下载。
这么一来,此模式的总体思路即:生产者线程解析单页网址,获取相应节点资源信息,并将信息存入一个中间单元,而消费者线程则负责从这个中间单元获取信息并消费。这里的共享中间单元,主要指的是图片相关的数据信息;而实际上,我们还可以设定另一个全局页面资源,其包含所有待解析的单页URL,主要目的是用于控制生产者是否解析完成和检查消费者是否已消费完成(当中间单元没有图片数据且页面资源页也已全部解析完毕则说明爬取任务已经完成)。
经上述分析,我想生产者消费者线程类也大体有了一个雏形,考虑到多线程编程,下面我们采用Queue线程队列来分别做进一步的介绍。
生产者线程类的实现:
在生产者线程类中,有两个属性self.pages_queue 和 self.imgs_queue,分别对应着上文中的页面资源和图片中间单元,这两个属性都可以在类初始化时通过参数传递进行设置;另外还具有parse_url(self,url)的方法,用于对单页进行解析,同时将解析后获取的图片资源存入图片中间单元;再者是run(self)方法的重写,其专门从页面资源中获取一个页面,并调用传递给parse_url()方法,同时其还需判断控制进程的完毕状态。具体实现如下:
## 生产者线程类 import requests from lxml import etree import threading from queue import Queue import re class Producer(threading.Thread): # 初始化时需要调用父类的初始化方法,同时设置相关资源属性 def __init__(self, pages_queue, imgs_queue, *args, **kwargs): super(Producer, self).__init__(*args, **kwargs) self.pages_queue = pages_queue self.imgs_queue = imgs_queue # 重写run()方法 def run(self): while True: if self.pages_queue.empty(): #检测是否已经解析完毕 break url = self.pages_queue.get() #获取url资源并进行解析 self.parse_url(url) # 解析url,获取图片数据信息并存入中间单元 def parse_url(self, url): response = requests.get(url, headers=HEADERS) text = response.content.decode('utf-8') html = etree.HTML(text) imgs = html.xpath('//div[@class="list "]//img') dates = html.xpath('//div[@class="list "]//p') for img, date in zip(imgs, dates): img_url = re.sub(r'-listpic', '', img.get('src')) #获取src img_name = re.findall(r'http://bimgs.plmeizi.com/images/bing/[0-9]*/(.*)\.jpg', img.get('src'))[0] #提取图片名 img_date = re.findall('\([0-9]{8}\)', date.text)[0] #提取图片日期 img_filename = img_name + ' ' + img_date + '.jpg' #生成图片文件名 self.imgs_queue.put((img_url, img_filename)) #以元组形式存储图片数据信息
消费者线程类的实现
和生产者线程类类似,其也有两个属性self.pages_queue 和 self.imgs_queue,含义和初始化操作均与生产者的一样;另外由于其只需要单纯地从中间单元取数据并消费,因此这一行为可以直接通过run()重写实现。消费者线程类的编写较为简单,具体如下:
## 消费者线程类 from urllib import request import threading from queue import Queue #定义全局计数变量 gNumber = 0 class Consumer(threading.Thread): def __init__(self, pages_queue, imgs_queue, *args, **kwargs): super(Consumer, self).__init__(*args, **kwargs) self.pages_queue = pages_queue self.imgs_queue = imgs_queue def run(self): global gNumber while True: if self.imgs_queue.empty() and self.pages_queue.empty():#当两个资源均为空时说明此任务已完成,退出 break img_url, img_filename = self.imgs_queue.get()#从中间单元分别获取src_url和文件名 request.urlretrieve(img_url, 'Bing_Wallpaper/' + img_filename)#进行下载 gNumber += 1 print(gNumber, img_filename, 'downloading...successfully!')
主功能模块的实现
主功能模块负责进行一些前期数据准备工作,如两个资源队列的初始化和单页url的生成与存储,再者实例化创建相关的线程并执行。其具体实现如下:
## 主功能模块 import threading from queue import Queue def downingSpider(): # 创建页面资源队列和图片中间单元队列 pages_queue = Queue(100) imgs_queue = Queue(300) # 遍历获取页面URL并存储起来 for index in range(1, 89): url = 'http://bing.plmeizi.com/?page=%s' % index pages_queue.put(url) # 实例化生产者消费者线程对象并执行 for x in range(8): t = Producer(pages_queue, imgs_queue) t.start() for x in range(10): t = Consumer(pages_queue, imgs_queue) t.start()
如此一来,我们的多线程爬虫就编写完毕。下图是我的运行结果(效果还是比较满意的):