【問題發現】
爬蟲項目中,為了防止被封號(提供的可用賬號太少),對於能不登錄就可以抓取的內容采用不帶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 -
有熱門推薦????
-
-
【加群獲取學習資料QQ群:901381280】 【各種爬蟲源碼獲取方式】 識別文末二維碼,回復:爬蟲源碼 歡迎關注公眾號:Python爬蟲數據分析挖掘,方便及時閱讀最新文章 回復【開源源碼】免費獲取更多開源項目源碼