Python雖然在爬蟲領(lǐng)域占用十分重要的地位瘫絮,但這時涨冀,經(jīng)常有人會嘲諷到:Python這么慢的語言用來寫爬蟲一點都不香,Java檀何、Go......哪個不比Python快坝恪!
雖然Python性能確實不好频鉴,但改善優(yōu)化這方面栓辜,其實也是比較簡單的。接下來筆者將使用一個常見的設(shè)計模式:生產(chǎn)者消費者模式垛孔,編寫一個簡書博客文章的爬蟲藕甩。
思路
首先,我們需要先了解一下生產(chǎn)者消費者模式周荐。
生產(chǎn)者消費者模式是通過一個容器來解決生產(chǎn)者和消費者的強(qiáng)耦合問題的狭莱。簡單來講,就是使用一個線程安全的容器進(jìn)行多線程的分工操作概作。也就是說腋妙,生產(chǎn)者消費者模式一定是一個異步模式。
關(guān)于線程安全的容器讯榕,在Python中第一個想到的就是隊列骤素,當(dāng)然,使用redis的數(shù)據(jù)結(jié)構(gòu)當(dāng)作該容器也是可以的愚屁。
然后济竹,先確定生產(chǎn)者以及消費者。生產(chǎn)者在該爬蟲中的任務(wù)即請求頁面并解析頁面霎槐,生成url鏈接送浊,放入到容器中;消費者則是對容器中的鏈接進(jìn)行二次請求丘跌,提取博客內(nèi)容以及保存袭景。
本文中將使用redis作為容器唁桩,并使用queue作為頁碼隊列。
依賴庫
import os
import re
import random
import threading
import requests
import redis
import fake_useragent
from queue import Queue
from bs4 import BeautifulSoup
- os模塊主要用于進(jìn)行文件路徑操作與文件保存
- re模塊在本文中主要用于替換數(shù)據(jù)而不是解析頁面浴讯,當(dāng)然朵夏,也可以用來解析頁面
- random模塊為隨機(jī)模塊
- threading模塊即多線程模塊
- requests模塊依然用于進(jìn)行頁面請求
- redis模塊用于操作redis數(shù)據(jù)庫
- fake_useragent模塊用于自動生成user-agent蔼啦,也可以直接復(fù)制瀏覽器的user-agent
- queue模塊即阻塞隊列
- bs4模塊用于解析頁面榆纽,提取數(shù)據(jù),也可使用xpath語法或者正則
生產(chǎn)者類
首先捏肢,生產(chǎn)者顧名思義奈籽,是需要生產(chǎn)數(shù)據(jù),也就是爬蟲中的頁面請求部分鸵赫。
class Producer(threading.Thread):
"""生產(chǎn)者類"""
def __init__(self, cli, queue):
super().__init__()
self.cli = cli
self.queue = queue
def get_html(self):
"""
獲取頁面源代碼
:return: response.text/None
"""
pass
def get_url(self, text):
"""
生產(chǎn)數(shù)據(jù)(url)
:param text: 頁面源代碼
:return: None
"""
pass
def run(self):
"""
生產(chǎn)者線程執(zhí)行函數(shù)
:return: None
"""
pass
上述代碼為該生產(chǎn)者類的一個簡單框架衣屏,既然是一個多線程爬蟲,自然要繼承threading.Thread這個類辩棒,再init方法里面也要通過super實現(xiàn)父類的init方法狼忱。
在init方法里有兩個屬性,分別代表了1個redis對象和一個隊列對象一睁。redis對象很明顯钻弄,是用來對redis進(jìn)行讀寫的,而這個對列對象者吁,則是1個頁碼池窘俺,出于線程安全考慮,使用了隊列复凳。
get_html這個方法比較簡單瘤泪,首先在頁碼池里面獲取1個頁碼,然后使用requests進(jìn)行http請求育八,返回請求頁面的源代碼对途,如果池中沒有頁碼了,則返回None髓棋。
get_url這個方法需要在頁面源代碼中提取我們需要的信息实檀,即文章鏈接。獲取到的文章鏈接將保存在redis中的集合對象中仲锄,以免出現(xiàn)重復(fù)文章劲妙。
run方法是線程的執(zhí)行方法,也就是該生產(chǎn)者類的”發(fā)動機(jī)“儒喊,詳細(xì)代碼如下镣奋。
def run(self):
"""
生產(chǎn)者線程執(zhí)行函數(shù)
:return: None
"""
while True:
# redis集合中的數(shù)據(jù)大于1000時結(jié)束循環(huán)(沒有實際意義,起到控制程序工作量的作用)
if self.cli.scard('jianshu:start_urls') > 1000:
break
else:
res = self.get_html()
if res is None:
continue
else:
self.get_url(res)
print(f'name:{threading.current_thread().name} is over!!!')
消費者類
消費者怀愧,自然要消費數(shù)據(jù)侨颈,當(dāng)redis中開始出現(xiàn)文章鏈接時余赢,消費者類就要大顯身手了。
class Consumer(threading.Thread):
"""消費者類"""
def __init__(self, cli):
super().__init__()
self.cli = cli
def get_data(self):
"""
二次請求鏈接
:return: response.text
"""
pass
def analysis(self, text):
"""
獲取頁面中的需要保存的html
:param text: 頁面源代碼
:return: section
"""
pass
def write_in(self, section):
"""
將獲取到的數(shù)據(jù)保存到本地
:param section: section標(biāo)簽中的內(nèi)容
:return: None
"""
pass
def run(self):
"""
消費者線程執(zhí)行函數(shù)
:return: None
"""
pass
在消費者類中哈垢,init方法不需要再傳入頁碼了妻柒,消費者類的工作基本上都圍繞在redis與頁面解析上。
get_data方法類似生產(chǎn)者類中的get_html方法耘分,即請求鏈接举塔,返回頁面源代碼。
analysis方法則是需要進(jìn)行頁面解析求泰,即獲取文章頁面的html內(nèi)容央渣。因為markdown是完全兼容html格式的,所以將獲取到的html的文章內(nèi)容部分保存為markdown渴频,是可以還原簡書的文章格式的芽丹。通過解析可看出,只需要保存< section >標(biāo)簽內(nèi)的內(nèi)容即可卜朗。即該方法返回< section >的soup對象拔第。
write_in方法則是將獲取到的html文本保存為markdown的格式,雖然描述起來簡單场钉,但這一步是最麻煩的蚊俺。因為,我們需要去修改文章內(nèi)的圖片鏈接惹悄,將圖片下載到本地春叫,每篇文章都需要1個獨立文件夾,再將圖片鏈接替換為相對路徑即可實現(xiàn)文章的保存泣港。
關(guān)于run方法暂殖,如下所示。
def run(self):
"""
消費者線程執(zhí)行函數(shù)
:return: None
"""
while True:
text = self.get_data()
if text is None:
continue
else:
section = self.analysis(text)
self.write_in(section)
# 如果redis集合中沒有數(shù)據(jù)則退出循環(huán)
if self.cli.scard('jianshu:start_urls') == 0:
break
print('大功告成5鄙础G好俊!')
需要注意的是坡氯,創(chuàng)建線程時晨横,盡量要保證生產(chǎn)的線程多一點,否則上述代碼的消費者if語句會直接退出程序箫柳,這個if語句也只是用來控制程序的手形,可隨意增改。
結(jié)語
上述兩個類的具體實現(xiàn)可以自由發(fā)揮悯恍,其實說到底库糠,爬蟲程序的主觀性實在是太大了。涮毫。瞬欧。關(guān)鍵一點在于贷屎,不要讓別人的代碼影響了你的思路,優(yōu)秀的爬蟲工程師往往都能第一時間想到解決問題的思路艘虎,然后一步步去解決思路里的麻煩唉侄。
爬蟲程序并沒有特么難的地方,往往都是一些”麻煩“在不斷的困擾你野建,有時候勸退你的往往都是麻煩属划,這時候只要耐心就好了。
關(guān)于該爬蟲的具體實現(xiàn)贬墩,可參考下面的Github鏈接榴嗅。
https://github.com/macxin123/spider/blob/master/jianshu/threading_jianshu.py
由于最近工作比較忙,所以鴿了很久陶舞。。绪励。上述的爬蟲也已經(jīng)完成很久了肿孵,所以在可用性上也許已經(jīng)崩了,不過還是那句話疏魏,爬蟲最重要的還是思路以及愿不愿意去解決麻煩停做。
爬蟲界的最大的麻煩也許就是驗證碼了,下篇文章筆者將寫一篇關(guān)于驗證碼與爬蟲的案例大莫,敬請期待蛉腌!