使用代理的爬蟲

信息源是搜狗微信,就爬到的數據保存到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將數據存到數據庫中

 

保存到數據庫

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章