scrapy中添加cookie踩坑記錄


【問題發現】

爬蟲項目中,為了防止被封號(提供的可用賬號太少),對於能不登錄就可以抓取的內容采用不帶cookie的策略,只有必要的內容才帶上cookie去訪問。

本來想着很簡單:在每個拋出來的Request的meta中帶上一個標志位,通過在CookieMiddleware中查看這個標志位,決定是否是給這個Request是否裝上Cookie。

實現的代碼大致如下:

class CookieMiddleware(object):
    """
    每次請求都隨機從賬號池中選擇一個賬號去訪問
    """


    def __init__(self):
        client = pymongo.MongoClient(MONGO_URI)
        self.account_collection = client[MONGO_DATABASE][ACCOUNT_COLLECTION]


    def process_request(self, request, spider):
        if 'target' in request.meta: 
            logging.debug('進入到process_request了')
            flag = request.meta['target']
            if flag != 'no':
                all_count = self.account_collection.find({'status': 'success'}).count()
                if all_count == 0:
                    raise Exception('當前賬號池為空')
                random_index = random.randint(0, all_count - 1)
                random_account = self.account_collection.find({'status': 'success'})[random_index]
                
                request.cookies = json.loads(random_account['cookie'])
            else:
                logging.debug('對XXX的請求不做處理')
        else:
            all_count = self.account_collection.find({'status': 'success'}).count()
            if all_count == 0:
                raise Exception('當前賬號池為空')
            random_index = random.randint(0, all_count - 1)
            random_account = self.account_collection.find({'status': 'success'})[random_index]
            
            request.cookies = json.loads(random_account['cookie'])


在settings.py中的配置如下:

DOWNLOADER_MIDDLEWARES = {
   'eyny.middlewares.CookieMiddleware': 550,
}

到這里可能有些大佬已經能夠看出端倪了,和我一樣認為這么寫沒啥問題的同志們繼續往下看。

在這么編寫完之后,我正常開啟了項目,還適當調高了並發量,然后第二天發現賬號被封了。在debug過程中看到在抓取不需要攜帶cookie的url的時候,依然攜帶了cookie,並且cookie是被放在了header中,經過我花費了兩個多小時查看框架源碼之后,終於發現了原因。

【原因&解決方案】

在scrapy的settings目錄下的default_settings.py文件中,初始聲明了一些DOWNLOADER_MIDDLEWARES_BASE,這些middlewares的聲明如下:

DOWNLOADER_MIDDLEWARES_BASE = {
    # Engine side
    'scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware': 100,
    'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware': 300,
    'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware': 350,
    'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware': 400,
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': 500,
    'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550,
    'scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware': 560,
    'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware': 580,
    'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 590,
    'scrapy.downloadermiddlewares.redirect.RedirectMiddleware': 600,
    'scrapy.downloadermiddlewares.cookies.CookiesMiddleware': 700,
    'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 750,
    'scrapy.downloadermiddlewares.stats.DownloaderStats': 850,
    'scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware': 900,
    # Downloader side
}


可以看到在DOWNLOADER_MIDDLEWARES_BASE中也聲明了一個CookiesMiddleware,而且是700,也就是說比我們寫的CookieMiddleware(500)要靠后執行,而且在debug過程中也看到,在執行完我們編寫的CookieMiddleware之后,header中沒有攜帶cookie,但是在執行完scrapy.downloadermiddlewares.cookies.CookiesMiddleware: 700之后,在header中看到了cookie,這說明cookie是scrapy幫我們自動加了。

我們打開scrapy.downloadermiddlewares.cookies.CookiesMiddleware的實現源碼,主要關注process_request方法:

class CookiesMiddleware(object):
    """This middleware enables working with sites that need cookies"""


    def __init__(self, debug=False):
        self.jars = defaultdict(CookieJar)
        self.debug = debug


    @classmethod
    def from_crawler(cls, crawler):
        if not crawler.settings.getbool('COOKIES_ENABLED'):
            raise NotConfigured
        return cls(crawler.settings.getbool('COOKIES_DEBUG'))


    def process_request(self, request, spider):
        if request.meta.get('dont_merge_cookies', False):
            return


        cookiejarkey = request.meta.get("cookiejar")
        jar = self.jars[cookiejarkey]
        cookies = self._get_request_cookies(jar, request)
        for cookie in cookies:
            jar.set_cookie_if_ok(cookie, request)


        # set Cookie header
        request.headers.pop('Cookie', None)
        jar.add_cookie_header(request)
        self._debug_cookie(request, spider)


  def process_response(self, request, response, spider):
        if request.meta.get('dont_merge_cookies', False):
            return response


        # extract cookies from Set-Cookie and drop invalid/expired cookies
        cookiejarkey = request.meta.get("cookiejar")
        jar = self.jars[cookiejarkey]
        jar.extract_cookies(response, request)
        self._debug_set_cookie(response, spider)


        return response


在上面的代碼中,最重要的是process_request方法中的內容,可以看到首先從request.meta中查看有沒有dont_merge_cookies屬性,如果沒有或者為false,就不運行剩下的方法,卧槽,這就是我們要找的方法呀!是不是好簡單…

【特別注意】

如果要使用dont_merge_cookies=true,那么需要我們自己將cookie加入到header中,通過**request.cookies = json.loads(random_account[‘cookie’])**方式添加的cookie,scrapy也不再會幫我們合並到header 中了。

【解決方案】
我們的解決方法就是在request的meta中加入dont_merge_cookies屬性,並設置為true,在CookieMiddleware中,我們將cookie添加在header中,而不是賦值給request.cookies

問題解決了,但是這么簡單是不是很不爽,所以就繼續想看看是為什么scrapy可以自動給我們加上cookie,這個接下來就需要讀下面的代碼了。

繼續看process_request方法中的內容,在檢查完CookiesMiddleware屬性之后,然后會在request.meta中查找cookiejar屬性的值,然后用這個值去自己的CookiJar管理器中查找是否有這個cookieJar,scrapy的cookieJar管理器使用的是self.jars=defaultdict(CookieJar)。如果沒有攜帶cookiejar屬性,則返回默認的CookieJar。

然后通過 _get_request_cookies方法獲得我們放在request.cookies中的cookie內容,然后遍歷這個cookies內容,將所有的內容保存在cookieJar的_cookies屬性中。

scrapy.http.cookies.CookieJar通過在屬性中設置self.jar=http.cookiejar.CookieJar,來實現CookieJar大部分的功能。

接下來是幫我們把cookies放在header中,

 # set Cookie header
request.headers.pop('Cookie', None)
jar.add_cookie_header(request)
self._debug_cookie(request, spider)


先把request.headers中的Cookie屬性刪除,然后把剛剛保存在jar中的cookies包裝到request.headers中。

scrapy.http.cookies.CookieJar 中添加cookies的代碼如下:

def add_cookie_header(self, request):
        wreq = WrappedRequest(request)
        self.policy._now = self.jar._now = int(time.time())


        # the cookiejar implementation iterates through all domains
        # instead we restrict to potential matches on the domain
        req_host = urlparse_cached(request).hostname
        if not req_host:
            return


        if not IPV4_RE.search(req_host):
            hosts = potential_domain_matches(req_host)
            if '.' not in req_host:
                hosts += [req_host + ".local"]
        else:
            hosts = [req_host]


        cookies = []
        for host in hosts:
            if host in self.jar._cookies:
                cookies += self.jar._cookies_for_domain(host, wreq)


        attrs = self.jar._cookie_attrs(cookies)
        if attrs:
            if not wreq.has_header("Cookie"):
                wreq.add_unredirected_header("Cookie", "; ".join(attrs))


        self.processed += 1
        if self.processed % self.check_expired_frequency == 0:
            # This is still quite inefficient for large number of cookies
            self.jar.clear_expired_cookies()


這段代碼的大致思路就是,從url中解析出host,然后根據host從jar._cookies屬性中獲取到cookie並包裝到header中,並且每包裝一次就對這次的cookie計數,如果達到了過期檢查次數,就對jar中的cookie做一次清空。

【最后】

看到這里,最需要注意的是:經過上面的過程,scrapy幫我們將cookies保存在了一個默認的CookieJar中,每當我們執行

cookiejarkey = request.meta.get("cookiejar")
jar = self.jars[cookiejarkey]


這段代碼的時候,實際上並沒有拿到新的CookieJar,拿到的是那個默認的CookieJar,他在defaultdict中的key是None,所以雖然我們沒有給request.cookies賦值,但是實際上add_cookie_header方法,將cookieJar中保存的cookie又給我們包裝到了header中。

- END -

有熱門推薦????


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM