Python?多線程爬取案例
前言
簡(jiǎn)單的爬蟲(chóng)只有一個(gè)進(jìn)程、一個(gè)線程,因此稱為??單線程爬蟲(chóng)??。單線程爬蟲(chóng)每次只訪問(wèn)一個(gè)頁(yè)面,不能充分利用計(jì)算機(jī)的網(wǎng)絡(luò)帶寬。一個(gè)頁(yè)面最多也就幾百KB,所以爬蟲(chóng)在爬取一個(gè)頁(yè)面的時(shí)候,多出來(lái)的網(wǎng)速和從發(fā)起請(qǐng)求到得到源代碼中間的時(shí)間都被浪費(fèi)了。如果可以讓爬蟲(chóng)同時(shí)訪問(wèn)10個(gè)頁(yè)面,就相當(dāng)于爬取速度提高了10倍。為了達(dá)到這個(gè)目的,就需要使用??多線程技術(shù)??了。
微觀上的單線程,在宏觀上就像同時(shí)在做幾件事。這種機(jī)制在 ??I/O(Input/Output,輸入/輸出)密集型的操作??上影響不大,但是在??CPU計(jì)算密集型的操作??上面,由于只能使用CPU的一個(gè)核,就會(huì)對(duì)性能產(chǎn)生非常大的影響。所以涉及計(jì)算密集型的程序,就需要使用多進(jìn)程。
爬蟲(chóng)屬于I/O密集型的程序,所以使用多線程可以大大提高爬取效率。
一、多進(jìn)程庫(kù)(multiprocessing)
??multiprocessing?? 本身是??Python的多進(jìn)程庫(kù)??,用來(lái)處理與多進(jìn)程相關(guān)的操作。但是由于進(jìn)程與進(jìn)程之間不能直接共享內(nèi)存和堆棧資源,而且啟動(dòng)新的進(jìn)程開(kāi)銷(xiāo)也比線程大得多,因此使用多線程來(lái)爬取比使用多進(jìn)程有更多的優(yōu)勢(shì)。
multiprocessing下面有一個(gè)??dummy模塊?? ,它可以讓Python的線程使用multiprocessing的各種方法。
dummy下面有一個(gè)??Pool類(lèi)?? ,它用來(lái)實(shí)現(xiàn)線程池。這個(gè)線程池有一個(gè)??map()方法??,可以讓線程池里面的所有線程都“同時(shí)”執(zhí)行一個(gè)函數(shù)。
測(cè)試案例 計(jì)算0~9的每個(gè)數(shù)的平方
# 循環(huán) for i in range(10): print(i ** i)
也許你的第一反應(yīng)會(huì)是上面這串代碼,循環(huán)不就行了嗎?反正就10個(gè)數(shù)!
這種寫(xiě)法當(dāng)然可以得到結(jié)果,但是代碼是一個(gè)數(shù)一個(gè)數(shù)地計(jì)算,效率并不高。而如果使用多線程的技術(shù),讓代碼同時(shí)計(jì)算很多個(gè)數(shù)的平方,就需要使用 ??multiprocessing.dummy?? 來(lái)實(shí)現(xiàn):
from multiprocessing.dummy import Pool
# 平方函數(shù)
def calc_power2(num):
return num * num
# 定義三個(gè)線程池
pool = Pool(3)
# 定義循環(huán)數(shù)
origin_num = [x for x in range(10)]
# 利用map讓線程池中的所有線程‘同時(shí)'執(zhí)行calc_power2函數(shù)
result = pool.map(calc_power2, origin_num)
print(f'計(jì)算1-10的平方分別為:{result}')在上面的代碼中,先定義了一個(gè)函數(shù)用來(lái)計(jì)算平方,然后初始化了一個(gè)有3個(gè)線程的線程池。這3個(gè)線程負(fù)責(zé)計(jì)算10個(gè)數(shù)字的平方,誰(shuí)先計(jì)算完手上的這個(gè)數(shù),誰(shuí)就先取下一個(gè)數(shù)繼續(xù)計(jì)算,直到把所有的數(shù)字都計(jì)算完成為止。
在這個(gè)例子中,線程池的 ??map()?? 方法接收兩個(gè)參數(shù),第1個(gè)參數(shù)是函數(shù)名,第2個(gè)參數(shù)是一個(gè)列表。注意:第1個(gè)參數(shù)僅僅是函數(shù)的名字,是不能帶括號(hào)的。第2個(gè)參數(shù)是一個(gè)可迭代的對(duì)象,這個(gè)可迭代對(duì)象里面的每一個(gè)元素都會(huì)被函數(shù) ??clac_power2()?? 接收來(lái)作為參數(shù)。除了列表以外,元組、集合或者字典都可以作為 ??map()?? 的第2個(gè)參數(shù)。

二、多線程爬蟲(chóng)
由于爬蟲(chóng)是 ??I/O密集型?? 的操作,特別是在請(qǐng)求網(wǎng)頁(yè)源代碼的時(shí)候,如果使用單線程來(lái)開(kāi)發(fā),會(huì)浪費(fèi)大量的時(shí)間來(lái)等待網(wǎng)頁(yè)返回,所以把多線程技術(shù)應(yīng)用到爬蟲(chóng)中,可以大大提高爬蟲(chóng)的運(yùn)行效率。
下面通過(guò)兩段代碼來(lái)對(duì)比單線程爬蟲(chóng)和多線程爬蟲(chóng)爬取??CSDN首頁(yè)??的性能差異:
import time
import requests
from multiprocessing.dummy import Pool
# 自定義函數(shù)
def query(url):
requests.get(url)
start = time.time()
for i in range(100):
query('https://www.csdn.net/')
end = time.time()
print(f'單線程循環(huán)訪問(wèn)100次CSDN,耗時(shí):{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線程訪問(wèn)100次CSDN,耗時(shí):{end - start}')
從運(yùn)行結(jié)果可以看到,一個(gè)線程用時(shí)約??69.4s??,5個(gè)線程用時(shí)約??14.3s??,時(shí)間是單線程的??五分之一??左右。從時(shí)間上也可以看到5個(gè)線程“同時(shí)運(yùn)行”的效果。
但并不是說(shuō)線程池設(shè)置得越大越好。從上面的結(jié)果也可以看到,5個(gè)線程運(yùn)行的時(shí)間其實(shí)比一個(gè)線程運(yùn)行時(shí)間的五分之一(??13.88s??)要多一點(diǎn)。這多出來(lái)的一點(diǎn)其實(shí)就是線程切換的時(shí)間。這也從側(cè)面反映了Python的多線程在微觀上還是串行的。
因此,如果線程池設(shè)置得過(guò)大,線程切換導(dǎo)致的開(kāi)銷(xiāo)可能會(huì)抵消多線程帶來(lái)的性能提升。線程池的大小需要根據(jù)實(shí)際情況來(lái)確定,并沒(méi)有確切的數(shù)據(jù)。
三、案例實(shí)操
從 ? ?https://www.kanunu8.com/book2/11138/?? 爬取?
?《北歐眾神》??所有章節(jié)的網(wǎng)址,再通過(guò)一個(gè)多線程爬蟲(chóng)將每一章的內(nèi)容爬取下來(lái)。在本地創(chuàng)建一個(gè)“北歐眾神”文件夾,并將小說(shuō)中的每一章分別保存到這個(gè)文件夾中,且每一章保存為一個(gè)文件。
import re
import os
import requests
from multiprocessing.dummy import Pool
# 爬取的主網(wǎng)站地址
start_url = 'https://www.kanunu8.com/book2/11138/'
"""
獲取網(wǎng)頁(yè)源代碼
:param url: 網(wǎng)址
:return: 網(wǎng)頁(yè)源代碼
"""
def get_source(url):
html = requests.get(url)
return html.content.decode('gbk') # 這個(gè)網(wǎng)頁(yè)需要使用gbk方式解碼才能讓中文正常顯示
"""
獲取每一章鏈接,儲(chǔ)存到一個(gè)列表中并返回
:param html: 目錄頁(yè)源代碼
: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
"""
獲取每一章的正文并返回章節(jié)名和正文
:param html: 正文源代碼
:return: 章節(jié)名,正文
"""
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('?', '') # 替換 ? 網(wǎng)頁(yè)空格符
text_block = text_block.replace('<p>', '') # 替換 <p></p> 中的嵌入的 <p></p> 中的 <p>
return chapter_name, text_block
"""
將每一章保存到本地
:param chapter: 章節(jié)名, 第X章
:param article: 正文內(nèi)容
:return: None
"""
def save(chapter, article):
os.makedirs('北歐眾神', exist_ok=True) # 如果沒(méi)有"北歐眾神"文件夾,就創(chuàng)建一個(gè),如果有,則什么都不做"
with open(os.path.join('北歐眾神', chapter + '.txt'), 'w', encoding='utf-8') as f:
f.write(article)
"""
根據(jù)正文網(wǎng)址獲取正文源代碼,并調(diào)用get_article函數(shù)獲得正文內(nèi)容最后保存到本地
:param url: 正文網(wǎng)址
: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、獲取網(wǎng)頁(yè)內(nèi)容
# 爬取的主網(wǎng)站地址
start_url = 'https://www.kanunu8.com/book2/11138/'
def get_source(url):
html = requests.get(url)
return html.content.decode('gbk') # 這個(gè)網(wǎng)頁(yè)需要使用gbk方式解碼才能讓中文正常顯示這一部分并不難,主要就是指明需要爬取的網(wǎng)站,并通過(guò) ??request.get()?? 的請(qǐng)求方式獲取網(wǎng)站,在通過(guò) ??content.decode()?? 獲取網(wǎng)頁(yè)的解碼內(nèi)容,其實(shí)就是獲取網(wǎng)頁(yè)的源代碼。

2、獲取每一章鏈接
def get_article_url(html):
article_url_list = []
# 根據(jù)正文鎖定每一章節(jié)的鏈接區(qū)域
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這里需要獲取到每一章的鏈接,首先我們根據(jù)正文鎖定每一章節(jié)的鏈接區(qū)域,然后在鏈接區(qū)域中獲取到每一章的鏈接,形成列表返回。

在獲取每章鏈接的時(shí)候,通過(guò)頁(yè)面源碼可以發(fā)現(xiàn)均為??數(shù)字開(kāi)頭??,??.html結(jié)尾??,于是利用正則 ??(\d*.html)?? 匹配即可:

3、獲取每一章的正文并返回章節(jié)名和正文
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('?', '') # 替換 ? 網(wǎng)頁(yè)空格符
text_block = text_block.replace('<p>', '') # 替換 <p></p> 中的嵌入的 <p></p> 中的 <p>
return chapter_name,這里利用正則分別匹配出每章的標(biāo)題和正文內(nèi)容:

格式化后:

4、將每一章保存到本地
"""
將每一章保存到本地
:param chapter: 章節(jié)名, 第X章
:param article: 正文內(nèi)容
:return: None
"""
def save(chapter, article):
os.makedirs('北歐眾神', exist_ok=True) # 如果沒(méi)有"北歐眾神"文件夾,就創(chuàng)建一個(gè),如果有,則什么都不做"
with open(os.path.join('北歐眾神', chapter + '.txt'), 'w', encoding='utf-8') as f:
f.write(article)這里獲取到我們處理好的文章標(biāo)題及內(nèi)容,并將其寫(xiě)入本地磁盤(pán)。首先創(chuàng)建文件夾,然后打開(kāi)文件夾以 ??章節(jié)名??+??.txt?? 結(jié)尾存儲(chǔ)每章內(nèi)容。
5、多線程爬取文章
""" 根據(jù)正文網(wǎng)址獲取正文源代碼,并調(diào)用get_article函數(shù)獲得正文內(nèi)容最后保存到本地 :param url: 正文網(wǎng)址 :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?? 調(diào)用 ??get_source??、??get_article?? 函數(shù)獲取以上分析的內(nèi)容,再調(diào)用 ??save?? 函數(shù)進(jìn)行本地存儲(chǔ),主入口main中創(chuàng)建線程池,包含4個(gè)線程。
??map()方法??,可以讓線程池里面的所有線程都“同時(shí)”執(zhí)行一個(gè)函數(shù)。 ??同時(shí)map()?? 方法接收兩個(gè)參數(shù),第1個(gè)參數(shù)是函數(shù)名,第2個(gè)參數(shù)是一個(gè)列表。這里我們需要對(duì)每一個(gè)章節(jié)進(jìn)行爬取,所以應(yīng)該是遍歷??章節(jié)鏈接的列表??(調(diào)用 ??get_article_url?? 獲?。?,執(zhí)行 ??query_article?? 方法進(jìn)行爬取保存。
最后運(yùn)行程序即可!
到此這篇關(guān)于Python 多線程爬取案例的文章就介紹到這了,更多相關(guān)Python 多線程爬取內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python 正則表達(dá)式爬蟲(chóng)使用案例解析
這篇文章主要介紹了Python 正則表達(dá)式爬蟲(chóng)使用案例解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-09-09
Python?任務(wù)自動(dòng)化工具nox?的配置與?API詳情
這篇文章主要介紹了Python?任務(wù)自動(dòng)化工具nox?的配置與?API詳情,Nox?會(huì)話是通過(guò)被@nox.session裝飾的標(biāo)準(zhǔn)?Python?函數(shù)來(lái)配置的,具體詳情下文相關(guān)介紹需要的小伙伴可以參考一下2022-07-07
TensorFlow實(shí)現(xiàn)自定義Op方式
今天小編就為大家分享一篇TensorFlow實(shí)現(xiàn)自定義Op方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-02-02
10款最佳Python開(kāi)發(fā)工具推薦,每一款都是神器
這篇文章主要介紹了10款最佳Python開(kāi)發(fā)工具推薦,每一款都是神器,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2020-10-10
python文字轉(zhuǎn)語(yǔ)音實(shí)現(xiàn)過(guò)程解析
這篇文章主要介紹了python文字轉(zhuǎn)語(yǔ)音實(shí)現(xiàn)過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11
Python datetime 格式化 明天,昨天實(shí)例
這篇文章主要介紹了Python datetime 格式化 明天,昨天實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-03-03
Python編程實(shí)現(xiàn)的簡(jiǎn)單Web服務(wù)器示例
這篇文章主要介紹了Python編程實(shí)現(xiàn)的簡(jiǎn)單Web服務(wù)器功能,涉及Python URL請(qǐng)求與響應(yīng)相關(guān)操作技巧,需要的朋友可以參考下2017-06-06

