在本節實踐中,我們將借助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()
如此一來,我們的多線程爬蟲就編寫完畢。下圖是我的運行結果(效果還是比較滿意的):