干貨!爬蟲框架 Feapder 和 Scrapy 的對比分析
這是「進擊的Coder」的第 476?篇技術分享作者:Boris來源:程序員技術寶典
“
閱讀本文大概需要 8 分鐘。
本篇文章在源碼層面比對 feapder、scrapy 、scrapy-redis 的設計,閱讀本文后,會加深您對 scrapy 以及 feapder 的了解,以及為什么推薦使用 feapder
scrapy 分析
1. 解析函數(shù)或數(shù)據(jù)入庫出錯,不會重試,會造成一定的數(shù)據(jù)丟失
scrapy 自帶的重試中間件只支持請求重試,解析函數(shù)內(nèi)異?;蛘邤?shù)據(jù)入庫異常不會重試,但爬蟲在請求數(shù)據(jù)時,往往會有一些意想不到的頁面返回來,若我們解析異常了,這條任務豈不是丟了。
當然有些大佬可以通過一些自定義中間件的方式或者加異常捕獲的方式來解決,我們這里只討論自帶的。
2. 運行方式,需借助命令行,不方便調試
若想直接運行,需編寫如下文件,麻煩
from?scrapy?import?cmdline
name?=?'spider_name'
cmd?=?'scrapy?crawl?{0}'.format(name)
cmdline.execute(cmd.split()
為什么必須通過命令行方式呢?因為 scrapy 是通過這種方式來加載項目中的settings.py文件的
3. 入庫 pipeline,不能批量入庫
class?TestScrapyPipeline(object):
????def?process_item(self,?item,?spider):
????????return?item
pipelines 里的 item 是一條條傳過來的,沒法直接批量入庫,但數(shù)據(jù)量大的時候,我們往往是需要批量入庫的,以節(jié)省數(shù)據(jù)庫的性能開銷,加快入庫速度
scrapy-redis 分析
scrapy-redis 任務隊列使用 redis 做的,初始任務存在 [spider_name]:start_urls里,爬蟲產(chǎn)生的子鏈接存在[spider_name]:requests下,那么我們先看下 redis 里的任務
1. redis 中的任務可讀性不好

我們看下子鏈任務,可以看到存儲的是序列化后的,這種可讀性不好
2. 取任務時直接彈出,會造成任務丟失
我們分析下 scrapy-redis 幾種任務隊列,取任務時都是直接把任務彈出來,如果任務剛彈出來爬蟲就意外退出,那剛彈出的這條任務就會丟失。
FifoQueue(先進先出隊列) 使用 list 集合

PriorityQueue(優(yōu)先級隊列),使用 zset 集合

LifoQueue(先進后出隊列),使用 list 集合

scrapy-redis 默認使用 PriorityQueue 隊列,即優(yōu)先級隊列
3. 去重耗內(nèi)存
使用 redis 的 set 集合對 request 指紋進行去重,這種面對海量數(shù)據(jù)去重對 redis 內(nèi)存容量要求很高
4. 需單獨維護個下發(fā)種子任務的腳本
feapder 分析
feapder 內(nèi)置 AirSpider、Spider、BatchSpider三種爬蟲,AirSpider 對標 Scrapy,Spider 對標 scrapy-redis,BatchSpider 則是應于周期性采集的需求,如每周采集一次商品的銷量等場景
上述問題解決方案
1. 解析函數(shù)或數(shù)據(jù)入庫出錯,不會重試,會造成一定的數(shù)據(jù)丟失
feapder 對請求、解析、入庫進行了全面的異常捕獲,任何位置出現(xiàn)異常會自動重試請求,若有不想重試的請求也可指定
2. 運行方式,需借助命令行,不方便調試
feapder 支持直接運行,跟普通的 python 腳本沒區(qū)別,可以借助 pycharm 調試。
除了斷點調試,feapder 還支持將爬蟲轉為 Debug 爬蟲,Debug 爬蟲模式下,可指定請求與解析函數(shù),生產(chǎn)的任務與數(shù)據(jù)不會污染正常環(huán)境
3. 入庫 pipeline,不能批量入庫
feapder 生產(chǎn)的數(shù)據(jù)會暫存內(nèi)存的隊列里,積攢一定量級或每 0.5 秒批量傳給 pipeline,方便批量入庫
def?save_items(self,?table,?items:?List[Dict])?->?bool:
????pass
這里有人會有疑問
數(shù)據(jù)放到內(nèi)存里了,會不會造成擁堵?
答:不會,這里限制了最高能積攢 5000 條的上限,若到達上限后,爬蟲線程會強制將數(shù)據(jù)入庫,然后再生產(chǎn)數(shù)據(jù)
若爬蟲意外退出,數(shù)據(jù)會不會丟?
答:不會,任務會在數(shù)據(jù)入庫后再刪除,若意外退出了,產(chǎn)生這些數(shù)據(jù)的任務會重做
入庫失敗了怎么辦?
答:入庫失敗,任務會重試,數(shù)據(jù)會重新入庫,若失敗次數(shù)到達配置的上限會報警
4. redis 中的任務可讀性不好
feapder 對請求里常用的字段沒有序列化,只有那些 json 不支持的對象才進行序列化

5. 取任務時直接彈出,會造成任務丟失
feapder 在獲取任務時,沒直接彈出,任務采用 redis 的 zset 集合存儲,每次只取小于當前時間搓分數(shù)的任務,同時將取到的任務分數(shù)修改為當前時間搓 +10 分鐘,防止其他爬蟲取到重復的任務。若爬蟲意外退出,這些取到的任務其實還在任務隊列里,并沒有丟失
6. 去重耗內(nèi)存
feapder 支持三種去重方式
- 內(nèi)存去重:采用可擴展的 bloomfilter 結構,基于內(nèi)存,去重一萬條數(shù)據(jù)約 0.5 秒,一億條數(shù)據(jù)占用內(nèi)存約 285MB
- 臨時去重:采用 redis 的 zset 集合存儲數(shù)據(jù)的 md5 值,去重可指定時效性。去重一萬條數(shù)據(jù)約 0.26 秒,一億條數(shù)據(jù)占用內(nèi)存約 1.43G
- 永久去重:采用可擴展的 bloomfilter 結構,基于 redis,去重一萬條數(shù)據(jù)約 0.5 秒,一億條數(shù)據(jù)占用內(nèi)存約 285 MB
7. 分布式爬蟲需單獨維護個下發(fā)種子任務的腳本
feapder 沒種子任務和子鏈接的分別,yield feapder.Request都會把請求下發(fā)到任務隊列,我們可以在start_requests編寫下發(fā)種子任務的邏輯
這里又有人會有疑問了
我爬蟲啟動多份時,
start_requests不會重復調用,重復下發(fā)種子任務么?答:不會,分布式爬蟲在調用
start_requests時,會加進程鎖,保證只能有一個爬蟲調用這個函數(shù)。并且若任務隊列中有任務時,爬蟲會走斷點續(xù)爬的邏輯,不會執(zhí)行start_requests那支持手動下發(fā)任務么
答:支持,按照 feapder 的任務格式,往 redis 里扔任務就好,爬蟲支持常駐等待任務
三種爬蟲簡介
1. AirSpider
使用PriorityQueue作為內(nèi)存任務隊列,不支持分布式,示例代碼
import?feapder
class?AirSpiderDemo(feapder.AirSpider):
????def?start_requests(self):
????????yield?feapder.Request("https://www.baidu.com")
????def?parse(self,?request,?response):
????????print(response)
if?__name__?==?"__main__":
????AirSpiderDemo().start()
2. Spider
分布式爬蟲,支持啟多份,爬蟲意外終止,重啟后會斷點續(xù)爬
import?feapder
class?SpiderDemo(feapder.Spider):
????#?自定義數(shù)據(jù)庫,若項目中有setting.py文件,此自定義可刪除
????__custom_setting__?=?dict(
????????REDISDB_IP_PORTS="localhost:6379",?REDISDB_USER_PASS="",?REDISDB_DB=0
????)
????def?start_requests(self):
????????yield?feapder.Request("https://www.baidu.com")
????def?parse(self,?request,?response):
????????print(response)
if?__name__?==?"__main__":
????SpiderDemo(redis_key="xxx:xxx").start()
3. BatchSpider
批次爬蟲,擁有分布式爬蟲所有特性,支持分布式
import?feapder
class?BatchSpiderDemo(feapder.BatchSpider):
????#?自定義數(shù)據(jù)庫,若項目中有setting.py文件,此自定義可刪除
????__custom_setting__?=?dict(
????????REDISDB_IP_PORTS="localhost:6379",
????????REDISDB_USER_PASS="",
????????REDISDB_DB=0,
????????MYSQL_IP="localhost",
????????MYSQL_PORT=3306,
????????MYSQL_DB="feapder",
????????MYSQL_USER_NAME="feapder",
????????MYSQL_USER_PASS="feapder123",
????)
????def?start_requests(self,?task):
????????yield?feapder.Request("https://www.baidu.com")
????def?parse(self,?request,?response):
????????print(response)
if?__name__?==?"__main__":
????spider?=?BatchSpiderDemo(
????????redis_key="xxx:xxxx",??#?redis中存放任務等信息的根key
????????task_table="",??#?mysql中的任務表
????????task_keys=["id",?"xxx"],??#?需要獲取任務表里的字段名,可添加多個
????????task_state="state",??#?mysql中任務狀態(tài)字段
????????batch_record_table="xxx_batch_record",??#?mysql中的批次記錄表
????????batch_name="xxx",??#?批次名字
????????batch_interval=7,??#?批次周期?天為單位?若為小時?可寫?1?/?24
????)
????#?spider.start_monitor_task()?#?下發(fā)及監(jiān)控任務
????spider.start()?#?采集
任務調度過程:
- 從 mysql 中批量取出一批種子任務
- 下發(fā)到爬蟲
- 爬蟲獲取到種子任務后,調度到 start_requests,拼接實際的請求,下發(fā)到 redis
- 爬蟲從 redis 中獲取到任務,調用解析函數(shù)解析數(shù)據(jù)
- 子鏈接入 redis,數(shù)據(jù)入庫
- 種子任務完成,更新種子任務狀態(tài)
- 若 redis 中任務量過少,則繼續(xù)從 mysql 中批量取出一批未做的種子任務下發(fā)到爬蟲
封裝了批次(周期)采集的邏輯,如我們指定 7 天一個批次,那么如果爬蟲 3 天就將任務做完,爬蟲重啟也不會重復采集,而是等到第 7 天之后啟動的時候才會采集下一批次。
同時批次爬蟲會預估采集速度,若按照當前速度在指定的時間內(nèi)采集不完,會發(fā)出報警
feapder 項目結構
上述的三種爬蟲例子修改配置后可以直接運行,但對于大型項目,可能會有就好多爬蟲組成。feapder 支持創(chuàng)建項目,項目結構如下:

main.py 為啟動入口
feapder 部署
feapder 有對應的管理平臺?feaplat,當然這個管理平臺也支持部署其他腳本
在任務列表里配置啟動命令,調度周期以及爬蟲數(shù)等。爬蟲數(shù)這個對于分布式爬蟲是非常爽的,可一鍵啟動幾十上百份爬蟲,再也不需要一個個部署了
-w1791任務啟動后,可看到實例及實時日志
-w1785爬蟲監(jiān)控面板可實時看到爬蟲運行情況,監(jiān)控數(shù)據(jù)保留半年,滾動刪除

采集效率測試
請求百度 1 萬次,線程都開到 300,測試耗時
scrapy:
class?BaiduSpider(scrapy.Spider):
????name?=?'baidu'
????allowed_domains?=?['baidu.com']
????start_urls?=?['https://baidu.com/']?*?10000
????def?parse(self,?response):
????????print(response)
結果
{'downloader/request_bytes':?4668123,
?'downloader/request_count':?20002,
?'downloader/request_method_count/GET':?20002,
?'downloader/response_bytes':?17766922,
?'downloader/response_count':?20002,
?'downloader/response_status_count/200':?10000,
?'downloader/response_status_count/302':?10002,
?'finish_reason':?'finished',
?'finish_time':?datetime.datetime(2021,?9,?13,?12,?22,?26,?638611),
?'log_count/DEBUG':?20003,
?'log_count/INFO':?9,
?'memusage/max':?74240000,
?'memusage/startup':?58974208,
?'response_received_count':?10000,
?'scheduler/dequeued':?20002,
?'scheduler/dequeued/memory':?20002,
?'scheduler/enqueued':?20002,
?'scheduler/enqueued/memory':?20002,
?'start_time':?datetime.datetime(2021,?9,?13,?12,?19,?58,?489472)}
耗時:148.149139 秒
feapder:
import?feapder
import?time
class?AirSpiderDemo(feapder.AirSpider):
????def?start_requests(self):
????????for?i?in?range(10000):
????????????yield?feapder.Request("https://www.baidu.com")
????def?parse(self,?request,?response):
????????print(response)
????def?start_callback(self):
????????self.start_time?=?time.time()
????def?end_callback(self):
????????print("耗時:{}".format(time.time()?-?self.start_time))
if?__name__?==?"__main__":
????AirSpiderDemo(thread_count=300).start()
結果:耗時:136.10122799873352
總結
本文主要分析了scrapy及scrapy-redis的痛點以及feapder是如何解決的,當然 scrapy 也有優(yōu)點,比如社區(qū)活躍、中間件靈活等。但在保證數(shù)據(jù)及任務不丟的場景,報警監(jiān)控等場景feapder完勝scrapy。并且feapder是基于實際業(yè)務,做過大大小小 100 多個項目,耗時 5 年打磨出來的,因此可滿足絕大多數(shù)爬蟲需求
效率方面,請求百度 1 萬次,同為 300 線程的情況下,feapder 耗時 136 秒,scrapy 耗時 148 秒,算上網(wǎng)絡的波動,其實效率差不多。
feapder 爬蟲文檔:https://boris-code.gitee.io/feapder/#/

feaplat 管理平臺:https://boris-code.gitee.io/feapder/#/feapder_platform/%E7%88%AC%E8%99%AB%E7%AE%A1%E7%90%86%E7%B3%BB%E7%BB%9F
爬蟲管理系統(tǒng)不僅支持
feapder、scrapy,且支持執(zhí)行任何腳本,可以把該系統(tǒng)理解成腳本托管的平臺 。支持集群
工作節(jié)點根據(jù)配置定時啟動,執(zhí)行完釋放,不常駐
一個worker內(nèi)只運行一個爬蟲,worker彼此之間隔離,互不影響。
支持管理員和普通用戶兩種角色
可自定義爬蟲端鏡像

End
「進擊的Coder」專屬學習群已正式成立,搜索「CQCcqc4」添加崔慶才的個人微信或者掃描下方二維碼拉您入群交流學習。
及時收看更多好文
↓↓↓
崔慶才的「進擊的Coder」知識星球已正式成立,感興趣的可以查看《我創(chuàng)辦了一個知識星球》了解更多內(nèi)容,歡迎您的加入:
