1、分佈式爬蟲html
前面咱們瞭解Scrapy爬蟲框架的基本用法 這些框架都是在同一臺主機運行的 爬取效率有限 若是多臺主機協同爬取 爬取效率必然成倍增加
這就是分佈式爬蟲的優點python
1. 分佈式爬蟲基本原理git
1.1 分佈式爬蟲架構github
Scrapy 單機爬蟲中有一個本地爬取隊列Queue 這個隊列是利用 deque 模塊實現的 若是新的 Request 生成就會放在隊列裏面 隨後 Request被
Scheduler調度 以後 Request 交給 Downloader 執行爬取 簡單的調度架構如圖 單主機爬蟲架構redis
若是兩個 Scheduler同時從隊列中取 Request 每一個 Scheduler 都有其對應的 Downloader 那麼在帶寬足夠 正常爬取且不考慮隊列存取壓力
的狀況下 爬取效率會翻倍算法
這樣 Scheduler 能夠拓展多個 Downloader 也能夠多拓展幾個 而爬取隊列Queue 必須始終爲一 也就是所謂的 共享爬取隊列 這樣才能保證
Scheduler 從隊列裏調度某個 Request 以後其餘 Scheduler 不會重複調度此 Request 就能夠多個 Scheduler 同步爬取 這就是分佈式爬蟲的雛形
簡單的調度架構如圖 分佈式爬蟲架構mongodb
須要多臺主機同時運行爬蟲任務協同爬取 而協同爬取的前提就是共享爬取隊列 這樣各臺主機就不要各自維護爬取隊列 而從共享爬取隊列存取
Request 可是各臺主機仍是與各自的 Scheduler 和 Downloader 因此調度和下載功能分別完成 不考慮隊列存取性能消耗 爬取效率仍是會成倍提升
如圖 主機與從機數據庫
1.2 維護爬取隊列json
隊列用什麼維護 首先考慮的就是性能問題 基於內存存儲的Redis 支持多種數據結構 例如 列表 集合 有序集合 等 存取操做也相對簡單api
redis 支持的這幾種數據結構存儲各有優勢
列表 有 lpush() lpop() rpush() rpop() 方法 咱們能夠用它來實現先進先出式爬取隊列 也能夠實現先進後出棧式爬取隊列
集合 元素是無序不重複的 能夠很是方便的實現隨機排序且不重複的爬取隊列
有序集合 帶有分數標識 而 Scrapy 的 Request 也有優先級的控制 能夠用它來實現帶優先級的調度隊列
須要根據具體爬蟲的需求靈活選擇不一樣隊列
1.3 如何去重
scrapy 有自動去重 使用了python中的集合 集合記錄了 Scrapy中每一個 Request的指紋
其內部使用的是hashlib 的 sha1 方法 計算的字段包括 Request 的 method URL Body Headers
這裏面只要有一點不一樣 那麼計算的結果就不一樣 計算獲得的結果是加密後的字符串 也就是指紋
每一個Request 都有獨有的指紋 指紋就是一個字符串 判斷字符串是否重複比判斷 Request 對象是否重複容易的多
scrapy中實現
def __init__(self): self.fingerprints = set() def request_seen(self,request): fp = self.request_fingerprints(request) if fp in self.fingerprints: return True self.fingerprints.add(fp)
對於分佈式爬蟲 確定不能利用每一個爬蟲各自的集合來去重 這樣作仍是每一個主機單獨維護本身的集合 不能作到共享 多臺主機
若是生成了相同的request 只能各自去重 各個主機之間就沒法作到去重
redis集合
redis提供集合數據結構 在redis集合中存儲每一個 Request的指紋
在向 Request 隊列中加入 Request 前首先驗證這個 Request的指紋是否已經加入集合中
若是已存在 則不添加 Request到隊列 若是不存在 則將 Request 添加入隊列並將指紋加入集合
利用一樣的原理 不一樣的存儲結構 實現了分佈式 Request的去重
1.4 防止中斷
在 scrapy中 爬蟲運行時的Request隊列放在內存中 爬蟲運行中斷後 這個隊列空間就被釋放了 隊列就被銷燬了 因此爬蟲一旦運行中斷
爬蟲再次運行就至關於全新的爬取過程
要作到中斷後繼續爬取 能夠將隊列保存起來 下次爬取 直接讀取保存數據便可獲取上次爬取隊列 在scrapy中指定爬取隊列存儲路徑便可
路徑使用JOB_DIR變量標識 可使用命令實現
scrapy crawl spider -s JOB_DIR=crawlS/spider
詳細設置 參考官方文檔 https://doc.scrapy.org/en/latest/topics/jobs.html
在 scrapy 實際把爬取隊列保存到本地 第二次爬取直接讀取並恢復隊列 分佈式中爬取隊列自己就是數據庫保存 若是中斷了
數據庫中request依然存在 下次啓動就會接着上次中斷的地方繼續爬取
1.5 架構實現
實現這個架構 首先要實現共享的爬取隊列 還要實現去重 重寫 Scheduler 能夠從共享爬取隊列存儲 Request
Scrapy-Redis 提供了分佈式的隊列 調度器 去重等功能 GitHub地址
https://github.com/rmax/scrapy-redis
2. Scrapy-Redis 源碼解析
首先下載 源代碼
核心源碼在
scrapy-redis/src/scrapy_redis
2.1 爬取隊列
源碼文件爲 queue.py
父類Base 中 _encode_request 和 _decode_request 分別能夠實現序列化和反序列化
緣由 把Request對象存儲到數據庫中 數據庫沒法直接存儲對象 須要先將 Request 序列化轉成字符串
父類中__len__ push pop 都是未實現的 直接使用會報異常
源碼中有三個子類實現
FifoQueue 類 繼承父類 重寫三個方法 都是對server 對象的操做 此爬取隊列使用了Redis的列表 序列化後的 Request存入列表中
push調用 lpush 從列表左側存儲數據 pop調用rpop 操做 從列表右側取出數據
Request 在列表中存取順序是 左側進 右側出 是有序的進出 先進先出
LifoQueue 類 與 FifoQueue相反 使用lpop操做 左側出 push 依然使用lpush 左側入 先進後出 後進先出 存取方式相似棧
PriorityQueue 類 優先級隊列 存儲結果是有序集合
2.2 去重過濾
源碼文件 dupefilter.py
使用的是redis中的集合數據結構
request_seen 和 scrapy中 request_seen 方法相似 使用的是數據庫存儲方式
鑑別重複方式仍是使用指紋 依靠request_fingerprint 方法獲取 直接向集合添加指紋 添加成功返回1 表示指紋不存在集合中
代碼中最後返回結果斷定添加結果是否爲0 若是返回1 斷定false 不重複 不然斷定重複
2.4 調度器
源碼文件 scheduler.py
核心方法存取方法
enqueue_request向隊列中添加 Request 調用 Queue 的push 操做 還有統計和日誌操做
next_request 從隊列取出 Request 調用 Queue 的pop操做 此隊裏中若是還有 Request 則直接取出 爬取繼續 若是爲空 爬取從新開始
總結
1.爬取隊列的實現 提供三種隊列 使用redis的列表或者集合來維護
2.去重的實現 使用redis集合來保存 Request 的指紋 提供重複過濾
3.中斷就從新爬取的實現 中斷後 reids的隊列沒有清空 爬取再次啓動 調度器 next_request 會從隊列中取到下一個 Request 爬取繼續
以上就是 scrapy-redis中的源碼解析 Scrapy-Redis還提供了 Spider Item Pipline 的實現 不過它們並非必須使用
3.分佈式爬蟲實現
利用 Scrapy-Redis 實現分佈式對接
須要安裝 Scrapy-Redis pip install scrapy-redis
驗證 import scrapy_redis 無報錯表示安裝成功
3.1 搭建 Redis服務器
要實現分佈式部署 多臺主機須要共享爬取隊列和去重集合 而在兩部份內容都是存於 Redis數據庫中的 須要搭建一個公網訪問的 Redis服務器
推薦使用Linux服務器 能夠購買阿里雲 騰訊雲 等提供的雲主機 通常都會配有公網IP
須要記錄redis 的運行 IP 端口 地址
3.2 配置 Scrapy-Redis
修改 settings 配置文件
將調度器的類和去重類替換爲 Scrapy-Redis 提供的類
SCHEDULER = 'scrapy_redis.scheduler.Scheduler' DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter'
配置redis鏈接信息
REDIS_URL = 'redis://password@host:port'
配置調度隊列 (可選)
默認使用 PriorityQueue 可在 settings中修改
SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.PriorityQueue'
配置持久化 (可選)
默認false 會在爬取完成後 清空爬取隊列 和去重指紋集合
SCHEDULER_PERSIST = True (不清空)
在強制中斷爬蟲運行時 不會自動清空
配置重爬 (可選)
默認false
SCHEDULER_FLUSH_ON_START = True #每次爬取後清空隊列和指紋
單機爬蟲 比較方便 分佈式不經常使用
Pipline配置 (可選)
默認不啓動 scrapy-redis 實現一個存儲到 Redis 的 item pipeline 若是啓用 爬蟲會把生成的item 存儲到 redis數據庫中
數據量比較大的狀況下通常不這麼作 由於redis是基於內存的 利用它是處理速度快的特性 存儲就太浪費了
ITEM_PIPELINES = { 'scrapy_redis.pipelines.RedisPipline:300',}
配置存儲目標
能夠在服務器搭建一個MongoDB 服務 存儲目標放在同一個MongoDB中
配置修改
MONGO_URL = 'mongodb://user:password@host:port'
3.3 運行
將爬蟲代碼部署到各臺主機 便可啓動爬取
每臺主機啓動爬蟲後 就會配置redis數據庫中調度request 作到爬取隊列共享和指紋集合共享 同時每臺主機佔用各自的帶寬和處理器
不會互相影響。
拓展
scrapy-redis 的去重機制是佔用內存的 指紋存儲到redis集合中每一個指紋長度40 每一位都是16進制
每一個十六進制佔用4b 一個指紋佔用空間20B 一億個佔用2GB 爬取數量達到上億級別時 redis佔用的內存就會變的很大 僅僅只是指紋存儲
還有隊列存儲的佔用 若是多個Scrapy項目同時爬取 內存開銷就是問題
瞭解 Bloom Filter 中文名布隆過濾器 檢測元素是否在集合中 空間利用效率很是高 大大節省存儲空間
使用位數組表示帶檢測集合 快速用機率算法判斷一個元素是否在集合中 達到去重效果
初始狀態下 聲明一個包含m位的爲數組 全部位都是0
有一個待檢測集合 表示爲 S={X1,X2,...Xn}須要檢測X是否已經存在集合S中 在 Bloom Filter 算法中 首先使用K個相互獨立 隨機的散列函數
將集合S中的每一個元素 X1,X2,...Xn 映射到長度爲M的位數組上 散列函數獲得結果記做位置索引 而後將位數組該位置索引的位置1
例如 取K爲3 表示三個散列函數 X1通過三個散列函數映射獲得 結果分別爲 1,4,8, X2通過三個散列函數 映射獲得結果分別爲 4,6,10
位數組的 1,4,6,8,10 五位就會置1
若是有新的元素X 判斷X是否在S集合 仍然用K個散列函數求X映射結果
若是全部結果對應的位數組位置均爲1 那麼X屬於S集合 若是有一個不爲1 則X不屬於S集合
M,n,K 知足關係 M>nK 位數組的長度M要比集合元素n和散列函數K的乘積還要大
判斷的方法很高效 能夠解決Redis內存不足的問題
2、分佈式爬蟲的部署
將scrapy項目 放到各個主機運行時 可能採用文件上傳或者GIT同步的方式 都須要各臺主機都進行操做 若是有100臺 1000臺 工做量沒法預計
1. scrapyd分佈式部署
是一個運行Scrapy爬蟲的服務程序提供了一系列HTTP接口 幫助部署 啓動 中止 刪除 爬蟲程序 支持版本管理 同時能夠管理多個爬蟲任務
使用時須要調用接口 官方文檔 https://scrapyd.readthedocs.io
daemonstatus.json 查看scrapyd服務和狀態 addversion.json 部署 scrapy項目 打包Egg文件 傳入項目名和版本 schedule.json 負責調度 scrapy項目運行 cancel.json 取消某個爬蟲任務 listprojects.json 列出部署的項目描述 listversions.json 獲取某個項目的全部版本 listspiders.json 獲取某個項目的最新版本 listjobs.json 獲取某個項目運行的全部任務詳情 delversion.json 刪除某個項目版本 delproject.json 刪除某個項目
1.2版本後不會自動生成配置文件 須要手動添加 文件名scrapy.conf
內容配置 參考https://scrapyd.readthedocs.io/en/stable/config.html
2. scrapyd API的使用
對scrapyd的封裝 官方文檔 http://python-scrapyd-api.readthedocs.io
3. Scrapy-Client的使用
使用說明 https://github.com/scrapy/scrapyd-client#scrapyd-deploy
4. 雲主機部署
不少服務商都提供雲主機服務 例如 阿里雲 騰訊雲 Azure Amazon 等不一樣服務商提供了不一樣的批量部署雲主機的方式。