python多線程實例

單線程爬蟲已經寫過了,這篇文章就對上一篇爬蟲進行改造,改成多線程的,上期文章鏈接:https://blog.csdn.net,對比單線程,相同的任務量多線程可以從107秒降到8秒左右(主要指獲取圖片鏈接並寫到文件中)

對於多線程爬蟲,常見有2種寫法,一種是繼承threading.Thread類,還有一種是直接使用,至於線程池什麼的,我還沒了解過,本篇文章是直接使用threading.Thread。
一般來說:

  • CPU密集型代碼(各種循環處理、計算等等):使用多進程
  • IO密集型代碼(文件處理、網絡爬蟲等):使用多線程

不管是多線程,多進程還是分佈式爬蟲,核心的東西就是任務分配和任務同步。

  • 任務分配:參考我學習的其他語言,都是要爲線程指定任務,很難寫,python就比好寫了,通俗來說,就是將你要做的任務,都放在一起(放在對列裏),而線程會無時無刻都訪問你的任務對列,有,他就執行,沒有就等待,就很方便完成了分配(因爲你不需要爲線程指定某一批任務,他會自己取)。
  • 線程同步:python對列裏有個很神奇的方法,叫join(),只要一個隊列使用這種方法,隊列不爲空,程序就不會繼續往下執行,就像斷點一樣。通過他,我們就可以進行線程同步了,只要隊列不爲空,說明還有任務,程序就不繼續執行,等待線程執行任務。
  • 做同樣任務的線程,但往往執行的任務又不太一樣,比如寫文件,線程都是寫文件,但是要寫到不同的文本中去,這時,我就需要爲我們的線程傳參了,通過參數來限定線程:把你要傳遞的參數放到一個list中,再把這個list對象,放入到隊列中
  • 此外,要學會將我們的功能,充分解耦,然後定義到具體的函數中去,再爲線程指定功能,線程就可以去取隊列裏的參數,完成工作了
  • 還有一個要說明的是,線程和主線程的關係,有兩種:主線結束,所有線程都會被殺死;主線結束,其他線程依然會運行。是可以通過線程的屬性來指定的。

這裏以獲取圖片的URL並保存到文件中爲例:(保存圖片的可看代碼註釋)

  • 每個線程都是一個死循環的函數,不停的檢查隊列裏是否有任務,通過queue.join()方法,實現線程同步,只要隊列不空,主程序不執行,等待線程執行任務,直到所以任務執行完畢,隊列爲空,主程序繼續執行,最後主程序結束,其他線程才被殺死

定義隊列:(每個隊列存放的對象都是是一個list,用於分類別,方括號裏的就是參數)

  • 開始頁面發送請求隊列:放分類網址的 start_url_q(開始就定義好的隊列) [“主題名稱”, “開始網址”]
  • 解析隊列:放返回的內容 start_content_q [“主題名稱”, “開始網頁內容”]
  • 套圖跳轉URL隊列:放套圖鏈接網址 next_image_url_q [“主題名稱”, “套圖名稱”, “跳轉網址”]
  • 解析隊列:放返回內容 next_image_content_q [“主題名稱”, “套圖名稱”, “跳轉網址內容”]
  • 圖片鏈接地址隊列:放圖片具體的信息 image_info_q [“主題名稱”, “套圖名稱”, “圖片鏈接”]

定義流程:(函數)

  • 獲取分類網頁:send_start_url 發送請求,將返回的內容放入 start_content
  • 解析開始網頁:get_start_content 將解析的套路鏈接放入 next_image_url
  • 獲取套圖網頁:send_next_image_url 發送請求,將返回的內容放入 next_image_content
  • 解析套圖網頁:get_next_image_content 將解析的下一個網頁地址和圖片鏈接分別放入 next_image_content 和 image_info
  • 保存圖片:save_image_info 將圖片信息保存到文本

這裏有一些參考資料:

import os
import sys
import time
import queue
import requests
import threading
from lxml import etree

class Reptile:
    "爬蟲類:獲取圖片的URL"
    def __init__(self):
        super().__init__()
        self.base_url = [["明星壁紙", "http://www.win4000.com/wallpaper_205_0_10_1.html"],
                         ["美食壁紙", "http://www.win4000.com/wallpaper_2361_0_10_1.html"],
                         ["卡通動漫", "http://www.win4000.com/wallpaper_192_0_10_1.html"],
                         ["遊戲壁紙", "http://www.win4000.com/wallpaper_191_0_10_1.html"]]
        self.headers = {  # 自定義請求頭
            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'}
        self.image_url_dir = os.path.join(sys.path[0], "image_url")  # 圖片鏈接保存的目錄
        self.image_dir = os.path.join(sys.path[0], "image") # 保存圖片的目錄

        self.q_lock = threading.Lock() 
        self.start_url_q = queue.Queue() # 開始頁面發送請求隊列:放分類網址的 start_url(開始就定義好的隊列) ["主題名稱", "開始網址"]
        self.start_content_q = queue.Queue() # 解析隊列:放返回的內容 start_content ["主題名稱", "開始網頁內容"]
        self.next_image_url_q = queue.Queue() # 套圖跳轉URL隊列:放套圖鏈接網址 next_image_url ["主題名稱", "套圖名稱", "跳轉網址"]
        self.next_image_content_q = queue.Queue() # 解析隊列:放返回內容 next_image_content ["主題名稱", "套圖名稱", "跳轉網址內容"]
        self.image_info_q = queue.Queue() # 圖片鏈接地址隊列:放圖片具體的信息 image_info ["主題名稱", "套圖名稱", "圖片鏈接"]

    def init_base_url(self):  # 初始化爬取網址
        for base in self.base_url:
            self.start_url_q.put(base)
            os.makedirs(os.path.join(self.image_url_dir, base[0]))
            # os.makedirs(os.path.join(self.image_dir, base[0]))

    def send_start_url(self):
        while True:  # 每一個線程都會不斷的請求內容
            start_url = self.start_url_q.get()  # ["主題名稱", "開始網址"]
            resp = requests.get(url=start_url[1], headers=self.headers)
            self.start_content_q.put([start_url[0], resp.text])
            self.start_url_q.task_done()

    def get_start_content(self):  # 解析開始網頁
        while True:
            start_content = self.start_content_q.get()  # ["主題名稱", "開始網頁內容"]
            content = etree.HTML(start_content[1])
            next_urls = content.xpath('./body/div[4]/div/div[3]/div/div/div/div/div/ul/li/a/@href')  # 跳轉地址
            for url in next_urls:
                self.next_image_url_q.put([start_content[0], url])
            self.start_content_q.task_done()

    def send_next_image_url(self):  # 發送套圖網頁請求
        while True:
            next_image_url = self.next_image_url_q.get() # ["主題名稱", "跳轉網址"]
            resp = requests.get(url=next_image_url[1], headers=self.headers)
            self.next_image_content_q.put([next_image_url[0], resp.text])
            self.next_image_url_q.task_done()

    def get_next_image_content(self):  # 解析套圖網頁
        while True:
            # ["主題名稱", "跳轉網址內容"]
            next_image_content = self.next_image_content_q.get()
            content = etree.HTML(next_image_content[1])
            title = content.xpath('./body/div[4]/div/div[2]/div/div[2]/div/div[@class="pic-meinv"]/a/img/@title')[0]  # 套圖名稱
            next_url = content.xpath('./body/div[4]/div/div[2]/div/div[2]/div/div[@class="pic-meinv"]/a/@href')[0]  # 下一張圖片所在網址
            image_url = content.xpath('./body/div[4]/div/div[2]/div/div[2]/div/div[@class="pic-meinv"]/a/img/@src')[0]  # 圖片資源
            if next_url[-7] == '_':
                self.next_image_url_q.put([next_image_content[0], next_url])
            self.image_info_q.put([next_image_content[0], title, image_url])
            self.next_image_content_q.task_done()

    def save_image_info(self):  # 保存圖片信息
        while True:
            image_info = self.image_info_q.get()  # ["主題名稱", "套圖名稱", "圖片地址"]
            # 保存圖片鏈接
            self.q_lock.acquire()
            file_name = os.path.join(
                self.image_url_dir, image_info[0], image_info[1] + ".txt")
            with open(file_name, "a", encoding="utf-8") as output:
                output.write(
                    image_info[0] + "," + image_info[1] + "," + image_info[2] + "\n")
            self.q_lock.release()

            self.image_info_q.task_done()

    def run(self):
        thread_list = []

        # 使用線程初始化URL
        thread_list.append(threading.Thread(target=self.init_base_url))

        # 使用線程請求開始頁面
        thread_list.append(threading.Thread(target=self.send_start_url))

        # 使用3個線程解析開始網頁內容
        for i in range(3):
            thread_list.append(threading.Thread(target=self.get_start_content))

        # 使用10個線程發送跳轉請求
        for i in range(10):
            thread_list.append(threading.Thread(
                target=self.send_next_image_url))

        # 使用10個線程解析跳轉網頁內容
        for i in range(10):
            thread_list.append(threading.Thread(
                target=self.get_next_image_content))

        # 使用10個線程保存文件
        for i in range(10):
            thread_list.append(threading.Thread(target=self.save_image_info))

        # 開啓線程
        for th in thread_list:
            th.setDaemon(True)  # 主進程結束,線程會立馬被結束
            th.start()

        # 線程同步,等待所有工作做完
        for q in [self.start_url_q, self.start_content_q, self.next_image_url_q, self.next_image_content_q, self.image_info_q]:
            q.join()  # 隊列爲空再執行其他操作


class DownloadImage():
    def __init__(self):
        super().__init__()
        self.image_url_dir = os.path.join(sys.path[0], "image_url")  # 圖片鏈接保存的目錄
        self.image_dir = os.path.join(sys.path[0], "image") # 保存圖片的目錄
        self.image_num = 0
        self.image_current_num = 0

        self.q_lock = threading.Lock()
        self.address_q = queue.Queue() # 地址隊列
        self.image_url_q = queue.Queue() # 圖片鏈接隊列
        self.image_content_q = queue.Queue() # 圖片內容隊列

        self.headers = {  # 自定義請求頭
            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'}
    
    def init_address(self): # 將所有地址保存到隊列中去
        dires = os.listdir(self.image_url_dir)
        for dire in dires:
            files = os.listdir(os.path.join(self.image_url_dir, dire))

            # 得到所有文件
            for file in files:
                address = os.path.join(self.image_url_dir, dire, file)
                self.address_q.put(address)

                # 建立保持圖片的目錄
                image_dire = os.path.join(self.image_dir, dire, file)[:-4]
                if not os.path.exists(image_dire):
                    os.makedirs(image_dire)

    def get_image_url(self): # 獲取圖片的URL
        while True:
            address = self.address_q.get()
            self.q_lock.acquire()
            with open(address, "r", encoding="utf-8") as input:
                rows = input.readlines()
                for row in rows:
                    self.image_url_q.put(row[:-1])
                    self.image_num += 1
            self.q_lock.release()
            self.address_q.task_done()

    def send_image_url(self): # 請求圖片內容
        while True:
            row = self.image_url_q.get().split(",") # ['美食壁紙', '唯美甜食冰淇淋圖片桌面壁紙', 'http://pic1.win4000.com/wallpaper/2020-02-19/5e4cee929b3d6.jpg']
            # resp = requests.get(url=row[2], headers=self.headers, stream=True)
            resp = requests.get(url=row[2], headers=self.headers)
            self.image_content_q.put([row[0], row[1], row[2][-16:-5], resp])
            self.image_url_q.task_done()

    def get_image_content(self): # 保存圖片內容
        while True:
            content = self.image_content_q.get()
            self.q_lock.acquire()
            image_name = os.path.join(self.image_dir, content[0], content[1], content[2] + ".jpg")
            with open(image_name, "wb+") as output:
                output.write(content[3].content)
            self.image_current_num += 1
            print(f"共計{self.image_num},正在下載第{self.image_current_num}張,完成度:{(self.image_current_num / self.image_num) * 100}%")
            self.q_lock.release()
            self.image_content_q.task_done()

    def run(self):
        thread_list = []
        self.init_address()

        # 獲取圖片的URL
        for i in range(3):
            thread_list.append(threading.Thread(target=self.get_image_url))
        
        # 請求圖片內容
        for i in range(10):
            thread_list.append(threading.Thread(target=self.send_image_url))

        # 保存圖片內容
        for i in range(10):
            thread_list.append(threading.Thread(target=self.get_image_content))
        
        # 啓動線程
        for t in thread_list:
            t.setDaemon(True)
            t.start()
        
        # 線程同步
        for q in [self.address_q, self.image_url_q, self.image_content_q]:
            q.join()        

if __name__ == '__main__':
    t = time.time()
    reptile = Reptile()
    reptile.run() # 獲取圖片鏈接
    print(f"get_url used time:{time.time() - t}")
    # get_url used time:7.833039045333862

    t = time.time()
    downloadImage = DownloadImage()
    downloadImage.run()  # 下載圖片
    print(f"get_url used time:{time.time() - t}")
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章