摘要
本文主要介紹一下犀光科技, 抓取系統中的數據採集模塊中的app採集部分,是基於scrapy-redis和anyproxy來實現的.
這裡只介紹比較簡單的一種場景:只要二次下載就可以抓取想要的內容,一次是下載列表頁,另一次是下次詳情頁.
基本架構圖
基本流程
用anyproxy截獲要抓取app的基本信息:請求url;請求頭;請求包體(如果有), 將這三個信息組包,並發到app_proxy服務app_proxy服務將收到的信息緩存:以供注入模塊查詢注入模塊,向app_proxy獲取到:請求url;請求頭;請求包體 打包注入到scrapy-redis的master隊列中scrapy-redis 將從隊列中獲取的:請求url;請求頭;請求包體 原封不動的發出去,基本上就可以拿到想要的信息,將下載到得信息,發到 app_parse_svr服務去解析,獲取格式化內容;如果是slave模塊,則將格式化內容輸出就可以了下面詳細介紹圖中的5個模塊
截獲必要信息模塊
anyproxy安裝,這個網上很多就不介紹了,可以參考這個地址這個主要是用nodejs實現一個截獲信息,並能將信息發送到app_proxy服務啟動anyproxy服務時,加載上面寫到的nodejs文件就可以了這個模塊,可以做成自動點擊,這樣就可以解決時效性的問題app_proxy模塊
此服務主要有二個接口,如下:
上報接口(report_message):POST 方法, 接收anyproxy截獲的信息,並保存
{ "token": "xxxx", #唯一標識 (必填) "url": "xxxxx", #請求URL(必填) "req_headers": "xxxx", #請求頭部,urlencode編碼(必填) "req_body": "xxxx" #請求包體urlencode編碼(可不填)}
提供查詢接口(query): Get 方法
參數: token=xxx返回結構:{ 'errno': 0, 'err_msg': 'success', 'data'}
注入模塊
這個主要是定時將要抓取的app信息,根據token從app_proxy服務中獲取信息,並將其打包成json結構,並壓入scrapy-redis隊列中,供其抓取。json結構如下:
{ "url": "xxxx", #列表頁url (必填) "headers": {}, # key-value 鍵值對 (必填) "body": "xxxx", # (可不填) "meta": { "shell_name": "xxx", #腳本文件名 (必填) "fun_name": "xxxx", #腳本函數名 (必填) ... } # key-value 鍵值對, 主要用來傳遞一些信息}
scrapy-redis模塊
此模塊基於master和slave實現的, master負責下載列表頁;slave 負責下載詳情頁.
原有的scrapy-redis,redis隊列中,只能放入url,不支持json結構,所以要改造一下,在spider目錄下,添加一個基類,假設名類叫CommSpider,maser和slave都繼承此基類, CommSpider基本定義如下:
# -*- coding:utf-8 -*-from scrapy.http import Requestfrom scrapy_redis.utils import bytes_to_strfrom scrapy_redis.spiders import RedisSpiderclass CommSpider(RedisSpider): def make_request_from_data(self, data): url, headers, body, meta = self.parse_redis_data(bytes_to_str(data, self.redis_encoding)) if url == '': #非json結構,按原來的邏輯處理 return self.make_requests_from_url(dec_data) else: # 將data,通過meta信息傳遞,以供下載完成時使用, 假設其key是 REDIS_DATA_GUID if body != '': return Request(url, method='POST', headers=headers, body=body.encode('utf-8'), meta={REDIS_DATA_GUID: data}, dont_filter=True) else: return Request(url, method='GET', headers=headers, meta={REDIS_DATA_GUID: data}, dont_filter=True) defparse_redis_data(self, data): #解析出url, headers, body和meta xxxxx return url, headers, body, meta
此模塊,只負責將內容下載下來, 不做具體的解析工作,所有解析工作都交由app_parse_svr來處理,具體的輸出內容master和slave是不同的.
mastermaster負責下載列表頁,並將從app_parse_svr獲取得格式化好的內容,解析出各個詳情頁,打包成json結構,壓入slave隊列中,master類簡單定義如下(這裡只寫一個大概的過程,不能運行的):
# -*- coding:utf-8 -*-import base64import tracebackimport jsonfrom .comm_spider import CommSpiderfrom ..log_handler import LogHandlerfrom ..settings import INFO, WARNING, ERRORfrom ..settings import MASTER_NAME, MASTER_KEY, REDIS_DATA_GUIDclassMaster(CommSpider):def__init__(self, *kargs, **kwargs): super(Master, self).__init__(*kargs, *kwargs) self._log_handler = LogHandler() name = MASTER_NAME redis_key = MASTER_KEY defparse(self, response):try: #1 獲取redis中的信息, 注意要將其base64解碼,這裡就寫個名字表示一下就好了 redis_info = decode_base64( response.meta[REDIS_DATA_GUID] if REDIS_DATA_GUID in response.meta else'') #2 解析出shell_name和fun_name#3 將shell_name, fun_name 和 response.text 傳遞給 app_parse_svr,讓其解析,並返回。#4 解析返回的內容,並將期打包成json格式壓入slave的redis隊列中 (這個結構在app_parse_svr模塊裡介紹)except Exception: traceback.print_exc()
slave 負責下載詳情頁內容, 並將從app_parse_svr獲取得格式化好的數據,輸出就可以了, 例如,可以輸出到kafka,做後續處理, slave類簡單定義如下:
# -*- coding:utf-8 -*-import tracebackimport jsonimport timefrom .comm_spider import CommSpiderfrom ..log_handler import LogHandlerfrom ..settings import SLAVE_NAME, SLAVE_KEY,REDIS_DATA_GUIDclassSlave(CommSpider):def__init__(self, *kargs, **kwargs): super(Slave, self).__init__(*kargs, *kwargs) self._log_handler = LogHandler() name = SLAVE_NAME redis_key = SLAVE_KEY defparse(self, response):try: #1 獲取redis中的信息, 注意要將其base64解碼,這裡就寫個名字表示一下就好了 redis_info = decode_base64( response.meta[REDIS_DATA_GUID] if REDIS_DATA_GUID in response.meta else'') #2 解析出shell_name、fun_name和ext#3 將shell_name、fun_name、ext 和 response.text 傳遞給 app_parse_svr,讓其解析,並返回。#4 解出ext,這個是格式化好的數據,輸出即可except Exception: traceback.print_exc()
app_parse_svr 模塊
此服務主要負責解析內容,並打包成指定的格式返回
此服務主要功能,就是可動態加載指定的解析腳本(我是用python來實現的), 解析指定的內容,這樣在增加新app時,只需按下面的格式增加一個腳本來解析和打包內容,完成後放到指定的目錄下就好了,服務無須改動,方便擴展.
請求包結構
{ "shell_name":"xxx", //腳本名字: 例 jrtt.py (必填) "parse_function":"xxxx", //解析函數名字 (必填) "content":"xxxx", //base64 編碼 (必填) "ext":{ //擴展項:主要用來傳項腳本必要信息,給腳本使用 (可不填) }}
響應包結構
列表頁:{ "errno":0, "errmsg":"success", "detail":[ { "next_url": "xxxx", //下一個解析的url (必填)"next_fun": "xxxx", //下一個解析內容的腳本函數(必填)"news_title": "xxxx", (可不填)"new_tags": "xxxxx", (可不填)... }, ..... ]}詳情頁:{ "errno":0, "errmsg":"success", "detail":{ "next_url": "xxxx", //下一個解析的url "next_fun": "xxxx", //下一個解析內容的腳本函數"ext":{ } }}註:如果"next_url"和"next_fun"為空,則表時是最終的詳情頁結果,ext 就是news的json格式
結尾
到這裡,此文就接近尾聲了,簡單總結一下: 此抓取總共由5個模塊組成,它們各司其職,並且每個模塊功能儘可能簡單,方便擴展。後續增加新app抓取,基本上只需增加一個解析腳本放到app_parser_svr服務指定的目錄下就可以了,其它的不需改動.