首頁 > 軟體

Python 多執行緒爬取案例

2022-08-16 18:02:02

前言

簡單的爬蟲只有一個程序、一個執行緒,因此稱為​​單執行緒爬蟲​​。單執行緒爬蟲每次只存取一個頁面,不能充分利用計算機的網路頻寬。一個頁面最多也就幾百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 多執行緒爬取內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com