Python 多線程爬取案例
前言
簡單的爬蟲隻有一個進程、一個線程,因此稱為單線程爬蟲
。單線程爬蟲每次隻訪問一個頁面,不能充分利用計算機的網絡帶寬。一個頁面最多也就幾百KB,所以爬蟲在爬取一個頁面的時候,多出來的網速和從發起請求到得到源代碼中間的時間都被浪費瞭。如果可以讓爬蟲同時訪問10個頁面,就相當於爬取速度提高瞭10倍。為瞭達到這個目的,就需要使用多線程技術
瞭。
微觀上的單線程,在宏觀上就像同時在做幾件事。這種機制在 I/O(Input/Output,輸入/輸出)密集型的操作
上影響不大,但是在CPU計算密集型的操作
上面,由於隻能使用CPU的一個核,就會對性能產生非常大的影響。所以涉及計算密集型的程序,就需要使用多進程。
爬蟲屬於I/O密集型的程序,所以使用多線程可以大大提高爬取效率。
一、多進程庫(multiprocessing)
multiprocessing
本身是Python的多進程庫
,用來處理與多進程相關的操作。但是由於進程與進程之間不能直接共享內存和堆棧資源,而且啟動新的進程開銷也比線程大得多,因此使用多線程來爬取比使用多進程有更多的優勢。
multiprocessing下面有一個dummy模塊
,它可以讓Python的線程使用multiprocessing的各種方法。
dummy下面有一個Pool類
,它用來實現線程池。這個線程池有一個map()方法
,可以讓線程池裡面的所有線程都“同時”執行一個函數。
測試案例 計算0~9的每個數的平方
# 循環 for i in range(10): print(i ** i)
也許你的第一反應會是上面這串代碼,循環不就行瞭嗎?反正就10個數!
這種寫法當然可以得到結果,但是代碼是一個數一個數地計算,效率並不高。而如果使用多線程的技術,讓代碼同時計算很多個數的平方,就需要使用 multiprocessing.dummy
來實現:
from multiprocessing.dummy import Pool # 平方函數 def calc_power2(num): return num * num # 定義三個線程池 pool = Pool(3) # 定義循環數 origin_num = [x for x in range(10)] # 利用map讓線程池中的所有線程‘同時'執行calc_power2函數 result = pool.map(calc_power2, origin_num) print(f'計算1-10的平方分別為:{result}')
在上面的代碼中,先定義瞭一個函數用來計算平方,然後初始化瞭一個有3個線程的線程池。這3個線程負責計算10個數字的平方,誰先計算完手上的這個數,誰就先取下一個數繼續計算,直到把所有的數字都計算完成為止。
在這個例子中,線程池的 map()
方法接收兩個參數,第1個參數是函數名,第2個參數是一個列表。註意:第1個參數僅僅是函數的名字,是不能帶括號的。第2個參數是一個可迭代的對象,這個可迭代對象裡面的每一個元素都會被函數 clac_power2()
接收來作為參數。除瞭列表以外,元組、集合或者字典都可以作為 map()
的第2個參數。
二、多線程爬蟲
由於爬蟲是 I/O密集型
的操作,特別是在請求網頁源代碼的時候,如果使用單線程來開發,會浪費大量的時間來等待網頁返回,所以把多線程技術應用到爬蟲中,可以大大提高爬蟲的運行效率。
下面通過兩段代碼來對比單線程爬蟲和多線程爬蟲爬取CSDN首頁
的性能差異:
import time import requests from multiprocessing.dummy import Pool # 自定義函數 def query(url): requests.get(url) start = time.time() for i in range(100): query('https://www.csdn.net/') end = time.time() print(f'單線程循環訪問100次CSDN,耗時:{end - start}') start = time.time() url_list = [] for i in range(100): url_list.append('https://www.csdn.net/') pool = Pool(5) pool.map(query, url_list) end = time.time() print(f'5線程訪問100次CSDN,耗時:{end - start}')
從運行結果可以看到,一個線程用時約69.4s
,5個線程用時約14.3s
,時間是單線程的五分之一
左右。從時間上也可以看到5個線程“同時運行”的效果。
但並不是說線程池設置得越大越好。從上面的結果也可以看到,5個線程運行的時間其實比一個線程運行時間的五分之一(13.88s
)要多一點。這多出來的一點其實就是線程切換的時間。這也從側面反映瞭Python的多線程在微觀上還是串行的。
因此,如果線程池設置得過大,線程切換導致的開銷可能會抵消多線程帶來的性能提升。線程池的大小需要根據實際情況來確定,並沒有確切的數據。
三、案例實操
從 https://www.kanunu8.com/book2/11138/ 爬取
《北歐眾神》
所有章節的網址,再通過一個多線程爬蟲將每一章的內容爬取下來。在本地創建一個“北歐眾神”文件夾,並將小說中的每一章分別保存到這個文件夾中,且每一章保存為一個文件。
import re import os import requests from multiprocessing.dummy import Pool # 爬取的主網站地址 start_url = 'https://www.kanunu8.com/book2/11138/' """ 獲取網頁源代碼 :param url: 網址 :return: 網頁源代碼 """ def get_source(url): html = requests.get(url) return html.content.decode('gbk') # 這個網頁需要使用gbk方式解碼才能讓中文正常顯示 """ 獲取每一章鏈接,儲存到一個列表中並返回 :param html: 目錄頁源代碼 :return: 每章鏈接 """ def get_article_url(html): article_url_list = [] article_block = re.findall('正文(.*?)<div class="clear">', html, re.S)[0] article_url = re.findall('<a href="(\d*.html)" rel="external nofollow" rel="external nofollow" >', article_block, re.S) for url in article_url: article_url_list.append(start_url + url) return article_url_list """ 獲取每一章的正文並返回章節名和正文 :param html: 正文源代碼 :return: 章節名,正文 """ def get_article(html): chapter_name = re.findall('<h1>(.*?)<br>', html, re.S)[0] text_block = re.search('<p>(.*?)</p>', html, re.S).group(1) text_block = text_block.replace(' ', '') # 替換 網頁空格符 text_block = text_block.replace('<p>', '') # 替換 <p></p> 中的嵌入的 <p></p> 中的 <p> return chapter_name, text_block """ 將每一章保存到本地 :param chapter: 章節名, 第X章 :param article: 正文內容 :return: None """ def save(chapter, article): os.makedirs('北歐眾神', exist_ok=True) # 如果沒有"北歐眾神"文件夾,就創建一個,如果有,則什麼都不做" with open(os.path.join('北歐眾神', chapter + '.txt'), 'w', encoding='utf-8') as f: f.write(article) """ 根據正文網址獲取正文源代碼,並調用get_article函數獲得正文內容最後保存到本地 :param url: 正文網址 :return: None """ def query_article(url): article_html = get_source(url) chapter_name, article_text = get_article(article_html) # print(chapter_name) # print(article_text) save(chapter_name, article_text) if __name__ == '__main__': toc_html = get_source(start_url) toc_list = get_article_url(toc_html) pool = Pool(4) pool.map(query_article, toc_list)
四、案例解析
1、獲取網頁內容
# 爬取的主網站地址 start_url = 'https://www.kanunu8.com/book2/11138/' def get_source(url): html = requests.get(url) return html.content.decode('gbk') # 這個網頁需要使用gbk方式解碼才能讓中文正常顯示
這一部分並不難,主要就是指明需要爬取的網站,並通過 request.get()
的請求方式獲取網站,在通過 content.decode()
獲取網頁的解碼內容,其實就是獲取網頁的源代碼。
2、獲取每一章鏈接
def get_article_url(html): article_url_list = [] # 根據正文鎖定每一章節的鏈接區域 article_block = re.findall('正文(.*?)<div class="clear">', html, re.S)[0] # 獲取到每一章的鏈接 article_url = re.findall('<a href="(\d*.html)" rel="external nofollow" rel="external nofollow" >', article_block, re.S) for url in article_url: article_url_list.append(start_url + url) return
這裡需要獲取到每一章的鏈接,首先我們根據正文鎖定每一章節的鏈接區域,然後在鏈接區域中獲取到每一章的鏈接,形成列表返回。
在獲取每章鏈接的時候,通過頁面源碼可以發現均為數字開頭
,.html結尾
,於是利用正則 (\d*.html)
匹配即可:
3、獲取每一章的正文並返回章節名和正文
def get_article(html): chapter_name = re.findall('<h1>(.*?)<br>', html, re.S)[0] text_block = re.search('<p>(.*?)</p>', html, re.S).group(1) text_block = text_block.replace(' ', '') # 替換 網頁空格符 text_block = text_block.replace('<p>', '') # 替換 <p></p> 中的嵌入的 <p></p> 中的 <p> return chapter_name,
這裡利用正則分別匹配出每章的標題和正文內容:
格式化後:
4、將每一章保存到本地
""" 將每一章保存到本地 :param chapter: 章節名, 第X章 :param article: 正文內容 :return: None """ def save(chapter, article): os.makedirs('北歐眾神', exist_ok=True) # 如果沒有"北歐眾神"文件夾,就創建一個,如果有,則什麼都不做" with open(os.path.join('北歐眾神', chapter + '.txt'), 'w', encoding='utf-8') as f: f.write(article)
這裡獲取到我們處理好的文章標題及內容,並將其寫入本地磁盤。首先創建文件夾,然後打開文件夾以 章節名
+.txt
結尾存儲每章內容。
5、多線程爬取文章
""" 根據正文網址獲取正文源代碼,並調用get_article函數獲得正文內容最後保存到本地 :param url: 正文網址 :return: None """ def query_article(url): article_html = get_source(url) chapter_name, article_text = get_article(article_html) # print(chapter_name) # print(article_text) save(chapter_name, article_text) if __name__ == '__main__': toc_html = get_source(start_url) toc_list = get_article_url(toc_html) pool = Pool(4) pool.map(query_article, toc_list)
這裡 query_article
調用 get_source
、get_article
函數獲取以上分析的內容,再調用 save
函數進行本地存儲,主入口main中創建線程池,包含4個線程。
map()方法
,可以讓線程池裡面的所有線程都“同時”執行一個函數。 同時map()
方法接收兩個參數,第1個參數是函數名,第2個參數是一個列表。這裡我們需要對每一個章節進行爬取,所以應該是遍歷章節鏈接的列表
(調用 get_article_url
獲取),執行 query_article
方法進行爬取保存。
最後運行程序即可!
到此這篇關於Python 多線程爬取案例的文章就介紹到這瞭,更多相關Python 多線程爬取內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!