爬蟲神器 feapder 與 scrapy 深度對比分析!

大家好,我是早起。

本篇文章在源碼層面比對 feapder、scrapy 、scrapy-redis 的設計。

閱讀本文後,會加深您對 scrapy 以及 feapder 的瞭解,以及爲什麼推薦使用 feapder

scrapy 分析


1、解析函數或數據入庫出錯,不會重試,會造成一定的數據丟失

scrapy 自帶的重試中間件只支持請求重試,解析函數內異常或者數據入庫異常不會重試,但爬蟲在請求數據時,往往會有一些意想不到的頁面返回來,若我們解析異常了,這條任務豈不是丟了。

當然有些大佬可以通過一些自定義中間件的方式或者加異常捕獲的方式來解決,我們這裏只討論自帶的。

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 是一條條傳過來的,沒法直接批量入庫,但數據量大的時候,我們往往是需要批量入庫的,以節省數據庫的性能開銷,加快入庫速度

scrapy-redis 分析


scrapy-redis 任務隊列使用 redis 做的,初始任務存在 [spider_name]:start_urls裏,爬蟲產生的子鏈接存在[spider_name]:requests下,那麼我們先看下 redis 裏的任務

1、redis 中的任務可讀性不好

我們看下子鏈任務,可以看到存儲的是序列化後的,這種可讀性不好

2、取任務時直接彈出,會造成任務丟失

我們分析下 scrapy-redis 幾種任務隊列,取任務時都是直接把任務彈出來,如果任務剛彈出來爬蟲就意外退出,那剛彈出的這條任務就會丟失。

  1. FifoQueue(先進先出隊列) 使用 list 集合

  2. PriorityQueue(優先級隊列),使用 zset 集合

  3. LifoQueue(先進後出隊列),使用 list 集合

scrapy-redis 默認使用 PriorityQueue 隊列,即優先級隊列

3、 去重耗內存

使用 redis 的 set 集合對 request 指紋進行去重,這種面對海量數據去重對 redis 內存容量要求很高

4、需單獨維護個下發種子任務的腳本

feapder 分析


feapder 內置 AirSpiderSpiderBatchSpider三種爬蟲,AirSpider 對標 Scrapy,Spider 對標 scrapy-redis,BatchSpider 則是應於週期性採集的需求,如每週採集一次商品的銷量等場景

 上述問題解決方案

1、解析函數或數據入庫出錯,不會重試,會造成一定的數據丟失

feapder 對請求、解析、入庫進行了全面的異常捕獲,任何位置出現異常會自動重試請求,若有不想重試的請求也可指定

2、運行方式,需藉助命令行,不方便調試

feapder 支持直接運行,跟普通的 python 腳本沒區別,可以藉助 pycharm 調試。

除了斷點調試,feapder 還支持將爬蟲轉爲 Debug 爬蟲,Debug 爬蟲模式下,可指定請求與解析函數,生產的任務與數據不會污染正常環境

3、 入庫 pipeline,不能批量入庫

feapder 生產的數據會暫存內存的隊列裏,積攢一定量級或每 0.5 秒批量傳給 pipeline,方便批量入庫

def save_items(self, table, items: List[Dict]) -> bool:
    pass

這裏有人會有疑問

  1. 數據放到內存裏了,會不會造成擁堵?

    答:不會,這裏限制了最高能積攢 5000 條的上限,若到達上限後,爬蟲線程會強制將數據入庫,然後再生產數據

  2. 若爬蟲意外退出,數據會不會丟?

    答:不會,任務會在數據入庫後再刪除,若意外退出了,產生這些數據的任務會重做

  3. 入庫失敗了怎麼辦?

    答:入庫失敗,任務會重試,數據會重新入庫,若失敗次數到達配置的上限會報警

4、redis 中的任務可讀性不好

feapder 對請求裏常用的字段沒有序列化,只有那些 json 不支持的對象才進行序列化

5、取任務時直接彈出,會造成任務丟失

feapder 在獲取任務時,沒直接彈出,任務採用 redis 的 zset 集合存儲,每次只取小於當前時間搓分數的任務,同時將取到的任務分數修改爲當前時間搓 + 10 分鐘,防止其他爬蟲取到重複的任務。若爬蟲意外退出,這些取到的任務其實還在任務隊列裏,並沒有丟失

6、去重耗內存

feapder 支持三種去重方式

  1. 內存去重:採用可擴展的 bloomfilter 結構,基於內存,去重一萬條數據約 0.5 秒,一億條數據佔用內存約 285MB

  2. 臨時去重:採用 redis 的 zset 集合存儲數據的 md5 值,去重可指定時效性。去重一萬條數據約 0.26 秒,一億條數據佔用內存約 1.43G

  3. 永久去重:採用可擴展的 bloomfilter 結構,基於 redis,去重一萬條數據約 0.5 秒,一億條數據佔用內存約 285MB

7、分佈式爬蟲需單獨維護個下發種子任務的腳本

feapder 沒種子任務和子鏈接的分別,yield feapder.Request都會把請求下發到任務隊列,我們可以在start_requests編寫下發種子任務的邏輯

這裏又有人會有疑問了

  1. 我爬蟲啓動多份時,start_requests不會重複調用,重複下發種子任務麼?

    答:不會,分佈式爬蟲在調用start_requests時,會加進程鎖,保證只能有一個爬蟲調用這個函數。並且若任務隊列中有任務時,爬蟲會走斷點續爬的邏輯,不會執行start_requests

  2. 那支持手動下發任務麼

    答:支持,按照 feapder 的任務格式,往 redis 裏扔任務就好,爬蟲支持常駐等待任務

三種爬蟲簡介


 1. AirSpider

使用PriorityQueue作爲內存任務隊列,不支持分佈式,示例代碼

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

分佈式爬蟲,支持啓多份,爬蟲意外終止,重啓後會斷點續爬

import feapder


class SpiderDemo(feapder.Spider):
    # 自定義數據庫,若項目中有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):
    # 自定義數據庫,若項目中有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_,
        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中任務狀態字段
        batch_record_table="xxx_batch_record",  # mysql中的批次記錄表
        batch_,  # 批次名字
        batch_interval=7,  # 批次週期 天爲單位 若爲小時 可寫 1 / 24
    )

    # spider.start_monitor_task() # 下發及監控任務
    spider.start() # 採集

任務調度過程:

  1. 從 mysql 中批量取出一批種子任務

  2. 下發到爬蟲

  3. 爬蟲獲取到種子任務後,調度到 start_requests,拼接實際的請求,下發到 redis

  4. 爬蟲從 redis 中獲取到任務,調用解析函數解析數據

  5. 子鏈接入 redis,數據入庫

  6. 種子任務完成,更新種子任務狀態

  7. 若 redis 中任務量過少,則繼續從 mysql 中批量取出一批未做的種子任務下發到爬蟲

封裝了批次(週期)採集的邏輯,如我們指定 7 天一個批次,那麼如果爬蟲 3 天就將任務做完,爬蟲重啓也不會重複採集,而是等到第 7 天之後啓動的時候纔會採集下一批次。

同時批次爬蟲會預估採集速度,若按照當前速度在指定的時間內採集不完,會發出報警

feapder 項目結構


上述的三種爬蟲例子修改配置後可以直接運行,但對於大型項目,可能會有就好多爬蟲組成。feapder 支持創建項目,項目結構如下:

main.py 爲啓動入口

feapder 部署


feapder 有對應的管理平臺 feaplat,當然這個管理平臺也支持部署其他腳本

  1. 在任務列表裏配置啓動命令,調度週期以及爬蟲數等。爬蟲數這個對於分佈式爬蟲是非常爽的,可一鍵啓動幾十上百份爬蟲,再也不需要一個個部署了

    -w1791

  2. 任務啓動後,可看到實例及實時日誌

    -w1785

  3. 爬蟲監控面板可實時看到爬蟲運行情況,監控數據保留半年,滾動刪除

採集效率測試


請求百度 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

總結一下


本文主要分析了scrapyscrapy-redis的痛點以及feapder是如何解決的,當然 scrapy 也有優點,比如社區活躍、中間件靈活等。但在保證數據及任務不丟的場景,報警監控等場景feapder完勝scrapy。並且feapder是基於實際業務,做過大大小小 100 多個項目,耗時 5 年打磨出來的,因此可滿足絕大多數爬蟲需求

效率方面,請求百度 1 萬次,同爲 300 線程的情況下,feapder 耗時 136 秒,scrapy 耗時 148 秒,算上網絡的波動,其實效率差不多。

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

  1. 爬蟲管理系統不僅支持 feapder、scrapy,且支持執行任何腳本,可以把該系統理解成腳本託管的平臺 。

  2. 支持集羣

  3. 工作節點根據配置定時啓動,執行完釋放,不常駐

  4. 一個 worker 內只運行一個爬蟲,worker 彼此之間隔離,互不影響。

  5. 支持管理員和普通用戶兩種角色

  6. 可自定義爬蟲端鏡像

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/cMDzQiU0leE6cX4HLS5V0A