爬蟲養成記--千軍萬馬來相見(詳解多執行緒)

圖雀社群發表於2020-03-23

爬蟲養成記--千軍萬馬來相見(詳解多執行緒)

本文由圖雀社群成員 燦若星空 寫作而成,歡迎加入圖雀社群,一起創作精彩的免費技術教程,予力程式設計行業發展。

如果您覺得我們寫得還不錯,記得 點贊 + 關注 + 評論 三連???,鼓勵我們寫出更好的教程?

前情回顧

在上篇教程爬蟲養成記--順藤摸瓜回首掏(女生定製篇)中我們通過分析網頁之間的聯絡,串起一條線,從而爬取大量的小哥哥圖片,但是一張一張的爬取速度未免也有些太慢,在本篇教程中將會與大家分享提高爬蟲速率的神奇技能——多執行緒。

慢在哪裡?

首先我們將之前所寫的爬蟲程式以流程圖的方式將其表示出來,通過這種更直觀的方式來分析程式在速度上的瓶頸。下面程式流程圖中紅色箭頭標明瞭程式獲取一張圖片時所要執行的步驟。

程式流程圖
大多數的程式設計語言其程式碼執行順序都是同步執行(JavaScript為非同步),也就是說在Python程式中只有上一條語句執行完成了,下一條語句才會開始執行。從流程圖中也可以看出來,只有第一頁的圖片抓取完成了,第二頁的圖片才會開始下載…………,當整個圖集所有的圖片都處理完了,下一個圖集的圖片才會開始進行遍歷下載。此過程如序列流程圖中藍色箭頭所示:
序列流程圖

從圖中可以看出當程式入到每個分叉點時也就是進入for迴圈時,在迴圈佇列中的每個任務(比如遍歷圖集or下載圖片)就只能等著前面一個任務完成,才能開始下面一個任務。就是因為需要等待,才拖慢了程式的速度。

這就像食堂打飯一樣,如果只有一個視窗,每個同學打飯時長為一分鐘,那麼一百個學生就有99個同學需要等待,100個同學打飯的總時長為1+2+3+……+ 99 + 100 = 5050分鐘。如果哪天食堂同時開放了100個視窗,那麼100個同學打飯的總時間將變為1分鐘,時間縮短了五千多倍!

如何提速?

我們現在所使用的計算機都擁有多個CPU,就相當於三頭六臂的哪吒,完全可以多心多用。如果可以充分發掘計算機的算力,將上述序列的執行順序改為並行執行(如下並行流程圖所示),那麼在整個程式的執行的過程中將消滅等待的過程,速度會有質的飛躍!

並行執行圖

從單執行緒到多執行緒

單執行緒 = 序列 從序列流程圖中可以看出紅色箭頭與藍色箭頭是首尾相連,一環扣一環。這稱之為序列。

多執行緒 = 並行 從並行流程圖中可以看出紅色箭頭每到一個分叉點就直接產生了分支,多個分支共同執行。此稱之為並行。

當然在整個程式當中,不可能一開始就搞個並行執行,序列是並行的基礎,它們兩者相輔相成。只有當程式出現分支(進入for迴圈)此時多執行緒可以派上用場,為每一個分支開啟一個執行緒從而加速程式的執行。對於萌新可以粗暴簡單地理解:沒有for迴圈,就不用多執行緒。對於有一定程式設計經驗的同學可以這樣理解:當程式中出現耗時操作時,要另開一個執行緒處理此操作。所謂耗時操做比如:檔案IO、網路IO……。

動手實踐

定義一個執行緒類

Python3中提供了threading模組用於幫助使用者構建多執行緒程式。我們首先將基於此模組來自定義一個執行緒類,用於消滅遍歷圖集時所需要的等待。

執行緒ID

程式執行時會開啟很多個執行緒,為了後期方便管理這些執行緒,可以線上程類的構造方法中新增threadID這一引數,為每個執行緒賦予唯一的ID號

所執行目標方法的引數

一般來說定義一個執行緒類主要目的是讓此執行緒去執行一個耗時的方法,所以這個執行緒類的構造方法中所需要傳入所要執行目的方法的引數。比如 handleTitleLinks 這個類主要用來執行getBoys() (參見文末中的完整程式碼)這一方法。getBoys() 所需一個標題的連結作為引數,所以在handleTitleLinks的構造方法中也需要傳入一個連結。

呼叫目標方法

執行緒類需要一個run(),在此方法中傳入引數,呼叫所需執行的目標方法即可。

class handleTitleLinks (threading.Thread):
    def __init__(self,threadID,link):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.link = link
    def run(self):
        print ("start handleTitleLinks:" + self.threadID)
        getBoys(self.link)
        print ("exit handleTitleLinks:" + self.threadID)
複製程式碼

例項化執行緒物件代替目標方法

當把執行緒類定義好之後,找到曾經耗時的目標方法,例項化一個執行緒物件將其代替即可。

def main():
    baseUrl = "https://www.nanrentu.cc/sgtp/"
    response = requests.get(baseUrl,headers=headers)
    if response.status_code == 200:
        with open("index.html",'w',encoding="utf-8") as f:
            f.write(response.text)
        doc = pq(response.text)
        # 得到所有圖集的標題連線
        titleLinks = doc('.h-piclist > li > a').items()
        # 遍歷這些連線
        for link in titleLinks:
        	# 替換目標方法,開啟執行緒
            handleTitleLinks(uuid.uuid1().hex,link).start()
            # getBoys(link)
複製程式碼

如法炮製

我們已經定義了一個執行緒去處理每個圖集,但是在處理每個圖集的過程中還會有分支(參見程式並行執行圖)去下載圖集中的圖片。此時需要再定義一個執行緒用來下載圖片,即定義一個執行緒去替換getImg()。

class handleGetImg (threading.Thread):
    def __init__(self,threadID,urlArray):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.url = url
    def run(self):
        print ("start handleGetImg:" + self.threadID)
        getPic(self.urlArray)
        print ("exit handleGetImg:" + self.threadID)
複製程式碼

改造後完整程式碼如下:

#!/usr/bin/python3
import requests
from pyquery import PyQuery as pq
import uuid
import threading

headers = {
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36',
    'cookie': 'UM_distinctid=170a5a00fa25bf-075185606c88b7-396d7407-100200-170a5a00fa3507; CNZZDATA1274895726=1196969733-1583323670-%7C1583925652; Hm_lvt_45e50d2aec057f43a3112beaf7f00179=1583326696,1583756661,1583926583; Hm_lpvt_45e50d2aec057f43a3112beaf7f00179=1583926583'
}
def saveImage(imgUrl,name):
    imgResponse = requests.get(imgUrl)
    fileName = "學習檔案/%s.jpg" % name
    if imgResponse.status_code == 200:
        with open(fileName, 'wb') as f:
            f.write(imgResponse.content)
            f.close()

# 根據連結找到圖片並下載           
def getImg(url):
    res = requests.get(url,headers=headers)
    if res.status_code == 200:
        doc = pq(res.text)
        imgSrc = doc('.info-pic-list > a > img').attr('src')
        print(imgSrc)
        saveImage(imgSrc,uuid.uuid1().hex)

# 遍歷組圖連結
def getPic(urlArray):
    for url in urlArray:
        # 替換方法
        handleGetImg(uuid.uuid1().hex,url).start()
        # getImg(url)
    

def createUrl(indexUrl,allPage):
    baseUrl = indexUrl.split('.html')[0]
    urlArray = []
    for i in range(1,allPage):
        tempUrl = baseUrl+"_"+str(i)+".html"
        urlArray.append(tempUrl)
    return urlArray

def getBoys(link):
    # 摸瓜第1步:獲取首頁連線
    picIndex = link.attr('href')
    #  摸瓜第2步:開啟首頁,提取末頁連結,得出組圖頁數
    res = requests.get(picIndex,headers=headers)
    print("當前正在抓取的 picIndex: " + picIndex)
    if res.status_code == 200:
        with open("picIndex.html",'w',encoding="utf-8") as f:
            f.write(res.text)
        doc = pq(res.text)
        lastLink = doc('.page > ul > li:nth-last-child(2) > a').attr('href')
        # 字串分割,得出全部的頁數
        if(lastLink is None):
            return
        # 以.html 為分割符進行分割,取結果陣列中的第一項
        temp = lastLink.split('.html')[0]
        # 再以下劃線 _ 分割,取結果陣列中的第二項,再轉為數值型
        allPage = int(temp.split('_')[1])
        # 摸瓜第3步:根據首尾連結構造url
        urlArray = createUrl(picIndex,allPage)
        # 摸瓜第4步:儲存圖片,摸瓜成功
        getPic(urlArray)

def main():
    baseUrl = "https://www.nanrentu.cc/sgtp/"
    response = requests.get(baseUrl,headers=headers)
    if response.status_code == 200:
        with open("index.html",'w',encoding="utf-8") as f:
            f.write(response.text)
        doc = pq(response.text)
        # 得到所有圖集的標題連線
        titleLinks = doc('.h-piclist > li > a').items()
        # 遍歷這些連線
        for link in titleLinks:
            # 替換方法,開啟執行緒
            handleTitleLinks(uuid.uuid1().hex,link).start()
            # getBoys(link)

# 處理組圖連結的執行緒類
class handleTitleLinks (threading.Thread):
    def __init__(self,threadID,link):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.link = link
    def run(self):
        print ("start handleTitleLinks:" + self.threadID)
        getBoys(self.link)
        print ("exit handleTitleLinks:" + self.threadID)
# 下載圖片的執行緒類
class handleGetImg (threading.Thread):
    def __init__(self,threadID,url):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.url = url
    def run(self):
        print ("start handleGetImg:" + self.threadID)
        getImg(self.url)
        print ("exit handleGetImg:" + self.threadID)

if __name__ == "__main__":
    main()
複製程式碼

效能對比

單執行緒100張圖片用時
多執行緒100張圖片用時
多執行緒200張圖片用時

因為網路波動的原因,採用多執行緒後並不能獲得理論上的速度提升,不過顯而易見的時多執行緒能大幅度提升程式速度,且資料量越大效果越明顯。

總結

至此爬蟲養成記系列文章,可以告一段落了。我們從零開始一步一步地學習瞭如何獲取網頁,然後從中分析出所要下載的圖片;還學習瞭如何分析網頁之間的聯絡,從而獲取到更多的圖片;最後又學習瞭如何利用多執行緒提高程式執行的效率。

希望各位看官能從這三篇文章中獲得啟發,體會到分析、設計並實現爬蟲程式時的各種方法與思想,從而能夠舉一反三,寫出自己所需的爬蟲程式~ 加油!??

預告

敬請期待爬蟲進階記~

如果您覺得我們寫得還不錯,記得 點贊 + 關注 + 評論 三連???,鼓勵我們寫出更好的教程?

想要學習更多精彩的實戰技術教程?來圖雀社群逛逛吧。

爬蟲養成記--千軍萬馬來相見(詳解多執行緒)

相關文章