單線程爬蟲已經寫過了,這篇文章就對上一篇爬蟲進行改造,改成多線程的,上期文章鏈接: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 將圖片信息保存到文本
這裏有一些參考資料:
- 多線程,多進程,分佈式爬蟲爬取方案(不涉及代碼):https://zhuanlan.zhihu.com
- 這有一個多線程的例子(沒有繼承Thread):https://blog.csdn.net
- 多線程,多進程操作和概念:http://www.py3study.com/Article/details/id/2145.html
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}")