信息源是搜狗微信,就爬到的數據保存到MySQL中
搜狗對微信公眾號和文章做了整合,我們可以直接通過鏈接搜索到相關的公眾號和文章
例如搜索NBA,搜索的結果的URL中有很多無關的GET請求的參數,手動將無關的請求參數去掉,其中只保留type和query,其中type表示的是搜索微信文章,query表示搜索關鍵詞為NBA https://weixin.sogou.com/weixin?query=NBA&type=2&page=2
要注意的點就是如果沒有輸入賬號的話,那么只能看到十頁內容,登錄之后可以看到一百頁的內容,如果想要抓取更多的內容,就需要登錄並使用cookies來進行爬取,搜狗微信的反爬能力很強,如果要是連續的刷新話站點就會彈出驗證碼
網絡請求出現了302跳轉,返回狀態碼是302,這時候就進入了驗證界面,所以可以得出結論,如果服務器返回的狀態碼是302而不是200的話就說明IP訪問次數過高了,IP早到了封禁,此次請求失敗
要是遇到這種情況,我們可以選擇識別這個驗證碼並進行解封操作,或者也可以選擇IP代理來進行直接切換
對於反爬能力很強的網站來說,如果我們遇到這種返回狀態就需要重試,所以可以采取另外一種爬取方式,借助數據庫來自己構造一個爬蟲隊列,將待爬取的請求都放到隊列中,如果請求失敗了就重新放回到隊列中,等待被重新進行調用 --> 這里可以借助redis的隊列,要是碰到新的請求就加入隊列中,或者有需要重試的請求也加入到隊列中。在調度的時候要是隊列不為空的話就將請求挨個取出來執行,得到響應的內容,提取出來我們想要的東西
采取MySQL進行存儲,需要借助與pymysql庫,將爬取的結果構造成一個字典,實現動態存儲
功能:
1、借助Redis數據庫構造爬蟲隊列,來實現請求的存取
2、實現異常處理,失敗的請求重新加入隊列
3、實現翻頁和提取文章列表並對應加入到隊列中
4、實現微信文章的提取
5、保存到數據庫中
構造Request
如果是要用隊列來存儲請求,那么就需要實現一個請求Request的數據結構,在這個請求頭中必須要包含的一些信息(請求URL、請求頭、請求方式、超時時間等),還有就是對於某個請求我們要實現對應的方法來處理它的響應,所以也就需要一個回調函數,每次翻頁的操作都需要代理來實現,所以也就需要一個代理的參數,最后就是要是一個請求的失敗次數過多,那么就不再需要重新進行請求了,所以還要對失敗次數進行記錄
上面說說到的參數都是Request的一部分,組成了一個完整的Request放到隊列中去等待調度,這樣從隊列中拿出來的時候直接執行Request就好了
實現:
我們可以采用繼承requests庫中的Request對象的方式來實現我們所需要的數據結構,在requests庫中已經有了Request對象,它將請求作為一個整體的對象去執行,當得到響應之后在進行返回,其實在requests庫中所構造的Request對象中,已經包含了請求方式、請求鏈接、請求頭這些參數了,但是跟我們想要的還是差了幾個。我們需要的是一個特定的數據結構,所以可以在原先的基礎上加入剩下的幾個屬性,在這里我們繼承Request對象,重新實現一個請求
TIMEOUT = 10 from requests import Request class WeixinRequest(Request): def __init__(self, url, callback, method='GET', headers=None, need_proxy=False, fail_time=0, timeout=TIMEOUT): Request.__init__(self, method, url, headers) # 回調函數 self.callback = callback # 代理 self.need_proxy = need_proxy # 失敗次數 self.fail_time = fail_time # 超時時間 self.timeout = timeout
首先init方法先調用了Request的init方法,然后加入了額外的幾個參數,callback、need_proxy、timeout,分別表示回調函數、是否需要代理進行爬取、失敗次數、超時時間
我們可以將新定義的Request看成是一個整體來進行執行,每個Request都是獨立的,每個請求中都有自己的屬性,例如,我們可以調用callback就可以知道這個請求的響應應該調用哪個方法來執行,調用fail_time就可以知道已經失敗了多少次了,是否需要進行丟棄等等
實現請求隊列
在構造請求隊列的時候其實就是實現請求的存取操作,所以就可以利用redis中的rpush和lpop方法
注意:存取的時候不能直接存Request對象,redis里面存的是字符串。所以在存Request對象之前我們要先把它序列化,取出來的時候再將它反序列化,可以利用pickle模塊實現
from pickle import dumps, loads from request import WeixinRequest class RedisQueue(): def __init__(self): """初始化 Redis""" self.db = StrictRedis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD) def add(self, request): """ 向隊列添加序列化后的 Request :param request: 請求對象 :param fail_time: 失敗次數 :return: 添加結果 """ if isinstance(request, WeixinRequest): return self.db.rpush(REDIS_KEY, dumps(request)) return False def pop(self): """ 取出下一個 Request 並反序列化 :return: Request or None """ if self.db.llen(REDIS_KEY): return loads(self.db.lpop(REDIS_KEY)) else: return False def empty(self): return self.db.llen(REDIS_KEY) == 0
寫了一個RedisQueue類,在init方法中初始化了一個StrictRedis對象,之后實現了add方法,首先判斷Request的類型,如果是我們自己定義的Request對象的話,那么就利用pickle序列化之后調用rpush方法加入到隊列中去。pop方法則相反,調用lpop方法將請求從隊列中拿出去,然后調用pickle的loads方法轉成我們自定義的Request類型
在調度的時候只需要新建一個RedisQueue對象,然后再調用add方法在隊列中傳入Request對象,就可以實現入隊操作了,調用pop方法就可以取出下一個Request對象
創建IP代理池
准備第一個請求
class Spider(): base_url = 'http://weixin.sogou.com/weixin' keyword = 'NBA' headers = { } session = Session() queue = RedisQueue() def start(self): """初始化工作""" # 全局更新 Headers self.session.headers.update(self.headers) start_url = self.base_url + '?' + urlencode({'query': self.keyword, 'type': 2}) weixin_request = WeixinRequest(url=start_url, callback=self.parse_index, need_proxy=True) # 調度第一個請求 self.queue.add(weixin_request)
在這里定義了Spider類,設置了很多全局變量,headers就是請求頭,在你的瀏覽器中登錄賬號,然后再開發者工具中將請求頭復制出來,一定要帶上cookie字段,因為這里面保存了你的登錄狀態,然后就是初始化Session和RedisQueue對象,分別來執行請求和存儲請求
這里面的start方法全局更新了headers,使得所有的請求都能應用到cookies,然后構造了一個起始的URL,之后用這個URL構造了一個Request對象。回調函數是當前類中的parse_index方法,也就是當這個請求成功之后就用parse_index來處理和解析。need_proxy參數設置為True,表示的是執行這個請求需要用到代理。最后我們用到了RedisQueue的add方法,將這個請求加入到隊列中,等待調度
調度請求
當地一個請求加入之后,調度就開始了。我們首先從隊列中取出這個請求,將它的結果解析出來,生成新的請求加入到隊列中,然后拿出新的請求,將結果來進行解析,在生成新的請求加入到隊列中,就這樣不斷的循環,知道隊列中沒有請求為止,就代表爬取結束了
VALID_STATUSES = [200] def schedule(self): """ 調度請求 :return: """ while not self.queue.empty(): weixin_request = self.queue.pop() callback = weixin_request.callback print('Schedule', weixin_request.url) response = self.request(weixin_request) if response and response.status_code in VALID_STATUSES: results = list(callback(response)) if results: for result in results: print('New Result', result) if isinstance(result, WeixinRequest): self.queue.add(result) if isinstance(result, dict): self.mysql.insert('articles', result) else: self.error(weixin_request) else: self.error(weixin_request)
在schedule方法中,其實就是一個內部循環,來判斷這個隊列是否為空,當隊列不為空的時候,調用pop方法從隊列中取出一個請求,調用requests方法來執行這個請求,
from requests import ReadTimeout, ConnectionError def request(self, weixin_request): """ 執行請求 :param weixin_request: 請求 :return: 響應 """ try: if weixin_request.need_proxy: proxy = get_proxy() if proxy: proxies = { 'http': 'http://' + proxy, 'https': 'https://' + proxy } return self.session.send(weixin_request.prepare(), timeout=weixin_request.timeout, allow_redirects=False, proxies=proxies) return self.session.send(weixin_request.prepare(), timeout=weixin_request.timeout, allow_redirects=False) except (ConnectionError, ReadTimeout) as e: print(e.args) return False
首先要判斷這個請求是否需要代理,如果需要代理,就調用get_proxy方法獲取代理,然后調用Session的send方法執行這個請求。這里的請求調用了prepare方法轉化成了Prepared Request,同時設置allow_redirects為False,timeout是該請求的超時時間,最后響應返回
執行request方法之后會得到兩種結果,一種就是False,也就是請求失敗了,另一種就是Response對象,這之前可以對狀態碼進行判斷,要是狀態碼合法的話就進行解析,否則就重新將請求放回隊列中
如果狀態碼合法,解析的時候會調用Request對象的回調函數進行解析,
from pyquery import PyQuery as pq def parse_index(self, response): """ 解析索引頁 :param response: 響應 :return: 新的響應 """ doc = pq(response.text) items = doc('.news-box .news-list li .txt-box h3 a').items() for item in items: url = item.attr('href') weixin_request = WeixinRequest(url=url, callback=self.parse_detail) yield weixin_request next = doc('#sogou_next').attr('href') if next: url = self.base_url + str(next) weixin_request = WeixinRequest(url=url, callback=self.parse_index, need_proxy=True) yield weixin_request
在這個回調函數中主要就是做了兩件事,1、獲取本頁所有微信文章的鏈接2、獲取下一頁的鏈接,在構造成Request對象之后通過yield進行返回,然后,schedule方法將返回的結果進行遍歷,利用isinstance方法判斷返回的結果,如果返回的結果是Request對象的話,就重新加入到隊列中去,到這里第一遍循環就結束了
其實這個時候while循環還會繼續執行。隊列已經包含第一頁內容的文章詳情頁請求和下一頁請求,所以第二次循環得到的下一個請求就是下一頁文章詳情頁的鏈接,程序重新調用request方法獲取其響應,然后調用它對應的回調函數解析,這個時候詳情頁請求的回調方法就不同了
def parse_detail(self, response): """ 解析詳情頁 :param response: 響應 :return: 微信公眾號文章 """ doc = pq(response.text) data = {'title': doc('.rich_media_title').text(), 'content': doc('.rich_media_content').text(), 'date': doc('#post-date').text(), 'nickname': doc('#js_profile_qrcode> div > strong').text(), 'wechat': doc('#js_profile_qrcode> div > p:nth-child(3) > span').text()} yield data
這個回調函數解析了微信文章詳情頁的內容,提取出來了它的標題、正文文本、發布日期、發布人昵稱、微信公眾號名稱。將這些信息組合成一個字典進行返回,結果返回之后還需要判斷類型,如果是字典類型,就通過mysql將數據存到數據庫中
保存到數據庫