爬蟲養成記--千軍萬馬來相見(詳解多線程)

爬蟲養成記–千軍萬馬來相見(詳解多線程)

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

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

前情回顧

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

慢在哪裏?

首先我們將之前所寫的爬蟲程序以流程圖的方式將其表示出來,通過這種更直觀的方式來分析程序在速度上的瓶頸。下面程序流程圖中紅色箭頭標明瞭程序獲取一張圖片時所要執行的步驟。
程序流程圖
大多數的程序設計語言其代碼執行順序都是同步執行(JS 可以通過回調函數處理異步,但是大多數還是同步的,Python 也有異步庫,也可以異步執行),也就是說在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張圖片用時

因爲網絡波動的原因,採用多線程後並不能獲得理論上的速度提升,不過顯而易見的時多線程能大幅度提升程序速度,且數據量越大效果越明顯。

總結

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

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

預告

敬請期待爬蟲進階記~

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章