很多時候,在爬取沒有登入的情況下,我們也可以通路一部分頁面或請求一些接口,因為畢竟網站本身需要做SEO,不會對所有頁面都設定登入限制。
但是,不登入直接爬取會有一些弊端,弊端主要有以下兩點。
設定了登入限制的頁面無法爬取。如某論壇設定了登入才可檢視資源,某部落格設定了登入才可檢視全文等,這些頁面都需要登入賬号才可以檢視和爬取。

一些頁面和接口雖然可以直接請求,但是請求一旦頻繁,通路就容易被限制或者IP直接被封,但是登入之後就不會出現這樣的問題,是以登入之後被反爬的可能性更低。
下面我們就第二種情況做一個簡單的實驗。以微網誌為例,我們先找到一個Ajax接口,例如新浪财經官方微網誌的資訊接口https://m.weibo.cn/api/container/getIndex?uid=1638782947&luicode=20000174&type=uid&value=1638782947&containerid=1005051638782947,如果用浏覽器直接通路,傳回的資料是JSON格式,如下圖所示,其中包含了新浪财經官方微網誌的一些資訊,直接解析JSON即可提取資訊。
但是,這個接口在沒有登入的情況下會有請求頻率檢測。如果一段時間内通路太過頻繁,比如打開這個連結,一直不斷重新整理,則會看到請求頻率過高的提示,如下圖所示。
圖中左側是登入了賬号之後請求接口的結果,右側是未登入賬号請求接口的結果,二者的接口連結是完全一樣的。未登入狀态無法正常通路,而登入狀态可以正常顯示。
是以,登入賬号可以降低被封禁的機率。
我們可以嘗試登入之後再做爬取,被封禁的幾率會小很多,但是也不能完全排除被封禁的風險。如果一直用同一個賬号頻繁請求,那就有可能遇到請求過于頻繁而封号的問題。
如果需要做大規模抓取,我們就需要擁有很多賬号,每次請求随機選取一個賬号,這樣就降低了單個賬号的通路頻率,被封的機率又會大大降低。
那麼如何維護多個賬号的登入資訊呢?這時就需要用到Cookies池了。接下來我們看看Cookies池的建構方法。
一、本節目标
我們以新浪微網誌為例來實作一個Cookies池的搭建過程。Cookies池中儲存了許多新浪微網誌賬号和登入後的Cookies資訊,并且Cookies池還需要定時檢測每個Cookies的有效性,如果某Cookies無效,那就删除該Cookies并模拟登入生成新的Cookies。同時Cookies池還需要一個非常重要的接口,即擷取随機Cookies的接口,Cookies運作後,我們隻需請求該接口,即可随機獲得一個Cookies并用其爬取。
由此可見,Cookies池需要有自動生成Cookies、定時檢測Cookies、提供随機Cookies等幾大核心功能。
二、準備工作
搭建之前肯定需要一些微網誌的賬号。需要安裝好Redis資料庫并使其正常運作。需要安裝Python的RedisPy、requests、Selelnium、Flask庫。另外,還需要安裝Chrome浏覽器并配置好ChromeDriver。
三、Cookies池架構
Cookies的架構和代理池類似,同樣是4個核心子產品,如下圖所示。




接口子產品需要用API來提供對外服務的接口。由于可用的Cookies可能有多個,我們可以随機傳回Cookies的接口,這樣保證每個Cookies都有可能被取到。Cookies越多,每個Cookies被取到的機率就會越小,進而減少被封号的風險。
以上設計Cookies池的的基本思路和前面講的代理池有相似之處。接下來我們設計整體的架構,然後用代碼實作該Cookies池。
四、Cookies池的實作
首先分别了解各個子產品的實作過程。
1. 存儲子產品
其實,需要存儲的内容無非就是賬号資訊和Cookies資訊。賬号由使用者名和密碼兩部分組成,我們可以存成使用者名和密碼的映射。Cookies可以存成JSON字元串,但是我們後面得需要根據賬号來生成Cookies。生成的時候我們需要知道哪些賬号已經生成了Cookies,哪些沒有生成,是以需要同時儲存該Cookies對應的使用者名資訊,其實也是使用者名和Cookies的映射。這裡就是兩組映射,我們自然而然想到Redis的Hash,于是就建立兩個Hash,結構分别如下圖所示。
Hash的Key就是賬号,Value對應着密碼或者Cookies。另外需要注意,由于Cookies池需要做到可擴充,存儲的賬号和Cookies不一定單單隻有本例中的微網誌,其他站點同樣可以對接此Cookies池,是以這裡Hash的名稱可以做二級分類,例如存賬号的Hash名稱可以為accounts:weibo,Cookies的Hash名稱可以為cookies:weibo。如要擴充知乎的Cookies池,我們就可以使用accounts:zhihu和cookies:zhihu,這樣比較友善。
接下來我們建立一個存儲子產品類,用以提供一些Hash的基本操作,代碼如下:
import random
import redis
class RedisClient(object):
def __init__(self, type, website, host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD):
"""
初始化Redis連接配接
:param host: 位址
:param port: 端口
:param password: 密碼
"""
self.db = redis.StrictRedis(host=host, port=port, password=password, decode_responses=True)
self.type = type
self.website = website
def name(self):
"""
擷取Hash的名稱
:return: Hash名稱
"""
return "{type}:{website}".format(type=self.type, website=self.website)
def set(self, username, value):
"""
設定鍵值對
:param username: 使用者名
:param value: 密碼或Cookies
:return:
"""
return self.db.hset(self.name(), username, value)
def get(self, username):
"""
根據鍵名擷取鍵值
:param username: 使用者名
:return:
"""
return self.db.hget(self.name(), username)
def delete(self, username):
"""
根據鍵名删除鍵值對
:param username: 使用者名
:return: 删除結果
"""
return self.db.hdel(self.name(), username)
def count(self):
"""
擷取數目
:return: 數目
"""
return self.db.hlen(self.name())
def random(self):
"""
随機得到鍵值,用于随機Cookies擷取
:return: 随機Cookies
"""
return random.choice(self.db.hvals(self.name()))
def usernames(self):
"""
擷取所有賬戶資訊
:return: 所有使用者名
"""
return self.db.hkeys(self.name())
def all(self):
"""
擷取所有鍵值對
:return: 使用者名和密碼或Cookies的映射表
"""
return self.db.hgetall(self.name())
這裡我們建立了一個
RedisClien
t類,初始化
__init__()
方法有兩個關鍵參數
type
和
website
,分别代表類型和站點名稱,它們就是用來拼接Hash名稱的兩個字段。如果這是存儲賬戶的Hash,那麼此處的
type
為
accounts
、
website
weibo
,如果是存儲Cookies的Hash,那麼此處的
type
cookies
website
weibo
。
接下來還有幾個字段代表了Redis的連接配接資訊,初始化時獲得這些資訊後初始化
StrictRedis
對象,建立Redis連接配接。
name()
方法拼接了
type
website
,組成Hash的名稱。
set()
get()
delete()
方法分别代表設定、擷取、删除Hash的某一個鍵值對,
count()
擷取Hash的長度。
比較重要的方法是
random()
,它主要用于從Hash裡随機選取一個Cookies并傳回。每調用一次
random()
方法,就會獲得随機的Cookies,此方法與接口子產品對接即可實作請求接口擷取随機Cookies。
2. 生成子產品
生成子產品負責擷取各個賬号資訊并模拟登入,随後生成Cookies并儲存。我們首先擷取兩個Hash的資訊,看看賬戶的Hash比Cookies的Hash多了哪些還沒有生成Cookies的賬号,然後将剩餘的賬号周遊,再去生成Cookies即可。
這裡主要邏輯就是找出那些還沒有對應Cookies的賬号,然後再逐個擷取Cookies,代碼如下:
for username in accounts_usernames:
if not username in cookies_usernames:
password = self.accounts_db.get(username)
print('正在生成Cookies', '賬号', username, '密碼', password)
result = self.new_cookies(username, password)
因為我們對接的是新浪微網誌,前面我們已經破解了新浪微網誌的四宮格驗證碼,在這裡我們直接對接過來即可,不過現在需要加一個擷取Cookies的方法,并針對不同的情況傳回不同的結果,邏輯如下所示:
def get_cookies(self):
return self.browser.get_cookies()
def main(self):
self.open()
if self.password_error():
return {
'status': 2,
'content': '使用者名或密碼錯誤'
}
# 如果不需要驗證碼直接登入成功
if self.login_successfully():
cookies = self.get_cookies()
return {
'status': 1,
'content': cookies
}
# 擷取驗證碼圖檔
image = self.get_image('captcha.png')
numbers = self.detect_image(image)
self.move(numbers)
if self.login_successfully():
cookies = self.get_cookies()
return {
'status': 1,
'content': cookies
}
else:
return {
'status': 3,
'content': '登入失敗'
}
這裡傳回結果的類型是字典,并且附有狀态碼
status
,在生成子產品裡我們可以根據不同的狀态碼做不同的處理。例如狀态碼為1的情況,表示成功擷取Cookies,我們隻需要将Cookies儲存到資料庫即可。如狀态碼為2的情況,代表使用者名或密碼錯誤,那麼我們就應該把目前資料庫中存儲的賬号資訊删除。如狀态碼為3的情況,則代表登入失敗的一些錯誤,此時不能判斷是否使用者名或密碼錯誤,也不能成功擷取Cookies,那麼簡單提示再進行下一個處理即可,類似代碼實作如下所示:
result = self.new_cookies(username, password)
# 成功擷取
if result.get('status') == 1:
cookies = self.process_cookies(result.get('content'))
print('成功擷取到Cookies', cookies)
if self.cookies_db.set(username, json.dumps(cookies)):
print('成功儲存Cookies')
# 密碼錯誤,移除賬号
elif result.get('status') == 2:
print(result.get('content'))
if self.accounts_db.delete(username):
print('成功删除賬号')
else:
print(result.get('content'))
如果要擴充其他站點,隻需要實作
new_cookies()
方法即可,然後按此處理規則傳回對應的模拟登入結果,比如1代表擷取成功,2代表使用者名或密碼錯誤。
代碼運作之後就會周遊一次尚未生成Cookies的賬号,模拟登入生成新的Cookies。
3. 檢測子產品
我們現在可以用生成子產品來生成Cookies,但還是免不了Cookies失效的問題,例如時間太長導緻Cookies失效,或者Cookies使用太頻繁導緻無法正常請求網頁。如果遇到這樣的Cookies,我們肯定不能讓它繼續儲存在資料庫裡。
是以我們還需要增加一個定時檢測子產品,它負責周遊池中的所有Cookies,同時設定好對應的檢測連結,我們用一個個Cookies去請求這個連結。如果請求成功,或者狀态碼合法,那麼該Cookies有效;如果請求失敗,或者無法擷取正常的資料,比如直接跳回登入頁面或者跳到驗證頁面,那麼此Cookies無效,我們需要将該Cookies從資料庫中移除。
此Cookies移除之後,剛才所說的生成子產品就會檢測到Cookies的Hash和賬号的Hash相比少了此賬号的Cookies,生成子產品就會認為這個賬号還沒生成Cookies,那麼就會用此賬号重新登入,此賬号的Cookies又被重新更新。
檢測子產品需要做的就是檢測Cookies失效,然後将其從資料中移除。
為了實作通用可擴充性,我們首先定義一個檢測器的父類,聲明一些通用元件,實作如下所示:
class ValidTester(object):
def __init__(self, website='default'):
self.website = website
self.cookies_db = RedisClient('cookies', self.website)
self.accounts_db = RedisClient('accounts', self.website)
def test(self, username, cookies):
raise NotImplementedError
def run(self):
cookies_groups = self.cookies_db.all()
for username, cookies in cookies_groups.items():
self.test(username, cookies)
在這裡定義了一個父類叫作
ValidTester
,在
__init__()
方法裡指定好站點的名稱
website
,另外建立兩個存儲子產品連接配接對象
cookies_db
accounts_db
,分别負責操作Cookies和賬号的Hash,
run()
方法是入口,在這裡是周遊了所有的Cookies,然後調用
test()
方法進行測試,在這裡
test()
方法是沒有實作的,也就是說我們需要寫一個子類來重寫這個
test()
方法,每個子類負責各自不同網站的檢測,如檢測微網誌的就可以定義為
WeiboValidTester
,實作其獨有的
test()
方法來檢測微網誌的Cookies是否合法,然後做相應的處理,是以在這裡我們還需要再加一個子類來繼承這個
ValidTester
,重寫其
test()
方法,實作如下:
import json
import requests
from requests.exceptions import ConnectionError
class WeiboValidTester(ValidTester):
def __init__(self, website='weibo'):
ValidTester.__init__(self, website)
def test(self, username, cookies):
print('正在測試Cookies', '使用者名', username)
try:
cookies = json.loads(cookies)
except TypeError:
print('Cookies不合法', username)
self.cookies_db.delete(username)
print('删除Cookies', username)
return
try:
test_url = TEST_URL_MAP[self.website]
response = requests.get(test_url, cookies=cookies, timeout=5, allow_redirects=False)
if response.status_code == 200:
print('Cookies有效', username)
print('部分測試結果', response.text[0:50])
else:
print(response.status_code, response.headers)
print('Cookies失效', username)
self.cookies_db.delete(username)
print('删除Cookies', username)
except ConnectionError as e:
print('發生異常', e.args)
test()
方法首先将Cookies轉化為字典,檢測Cookies的格式,如果格式不正确,直接将其删除,如果格式沒問題,那麼就拿此Cookies請求被檢測的URL。
test()
方法在這裡檢測微網誌,檢測的URL可以是某個Ajax接口,為了實作可配置化,我們将測試URL也定義成字典,如下所示:
TEST_URL_MAP = {
'weibo': 'https://m.weibo.cn/'
}
如果要擴充其他站點,我們可以統一在字典裡添加。對微網誌來說,我們用Cookies去請求目标站點,同時禁止重定向和設定逾時時間,得到Response之後檢測其傳回狀态碼。如果直接傳回200狀态碼,則Cookies有效,否則可能遇到了302跳轉等情況,一般會跳轉到登入頁面,則Cookies已失效。如果Cookies失效,我們将其從Cookies的Hash裡移除即可。
4. 接口子產品
生成子產品和檢測子產品如果定時運作就可以完成Cookies實時檢測和更新。但是Cookies最終還是需要給爬蟲來用,同時一個Cookies池可供多個爬蟲使用,是以我們還需要定義一個Web接口,爬蟲通路此接口便可以取到随機的Cookies。我們采用Flask來實作接口的搭建,代碼如下所示:
import json
from flask import Flask, g
app = Flask(__name__)
# 生成子產品的配置字典
GENERATOR_MAP = {
'weibo': 'WeiboCookiesGenerator'
}
@app.route('/')
def index():
return '<h2>Welcome to Cookie Pool System</h2>'
def get_conn():
for website in GENERATOR_MAP:
if not hasattr(g, website):
setattr(g, website + '_cookies', eval('RedisClient' + '("cookies", "' + website + '")'))
return g
@app.route('/<website>/random')
def random(website):
"""
擷取随機的Cookie, 通路位址如 /weibo/random
:return: 随機Cookie
"""
g = get_conn()
cookies = getattr(g, website + '_cookies').random()
return cookies
我們同樣需要實作通用的配置來對接不同的站點,是以接口連結的第一個字段定義為站點名稱,第二個字段定義為擷取的方法,例如,/weibo/random是擷取微網誌的随機Cookies,/zhihu/random是擷取知乎的随機Cookies。
5. 排程子產品
最後,我們再加一個排程子產品讓這幾個子產品配合運作起來,主要的工作就是驅動幾個子產品定時運作,同時各個子產品需要在不同程序上運作,實作如下所示:
import time
from multiprocessing import Process
from cookiespool.api import app
from cookiespool.config import *
from cookiespool.generator import *
from cookiespool.tester import *
class Scheduler(object):
@staticmethod
def valid_cookie(cycle=CYCLE):
while True:
print('Cookies檢測程序開始運作')
try:
for website, cls in TESTER_MAP.items():
tester = eval(cls + '(website="' + website + '")')
tester.run()
print('Cookies檢測完成')
del tester
time.sleep(cycle)
except Exception as e:
print(e.args)
@staticmethod
def generate_cookie(cycle=CYCLE):
while True:
print('Cookies生成程序開始運作')
try:
for website, cls in GENERATOR_MAP.items():
generator = eval(cls + '(website="' + website + '")')
generator.run()
print('Cookies生成完成')
generator.close()
time.sleep(cycle)
except Exception as e:
print(e.args)
@staticmethod
def api():
print('API接口開始運作')
app.run(host=API_HOST, port=API_PORT)
def run(self):
if API_PROCESS:
api_process = Process(target=Scheduler.api)
api_process.start()
if GENERATOR_PROCESS:
generate_process = Process(target=Scheduler.generate_cookie)
generate_process.start()
if VALID_PROCESS:
valid_process = Process(target=Scheduler.valid_cookie)
valid_process.start()
這裡用到了兩個重要的配置,即産生子產品類和測試子產品類的字典配置,如下所示:
# 産生子產品類,如擴充其他站點,請在此配置
GENERATOR_MAP = {
'weibo': 'WeiboCookiesGenerator'
}
# 測試子產品類,如擴充其他站點,請在此配置
TESTER_MAP = {
'weibo': 'WeiboValidTester'
}
這樣的配置是為了友善動态擴充使用的,鍵名為站點名稱,鍵值為類名。如需要配置其他站點可以在字典中添加,如擴充知乎站點的産生子產品,則可以配置成:
GENERATOR_MAP = {
'weibo': 'WeiboCookiesGenerator',
'zhihu': 'ZhihuCookiesGenerator',
}
Scheduler裡将字典進行周遊,同時利用
eval()
動态建立各個類的對象,調用其入口
run()
方法運作各個子產品。同時,各個子產品的多程序使用了multiprocessing中的Process類,調用其
start()
方法即可啟動各個程序。
另外,各個子產品還設有子產品開關,我們可以在配置檔案中自由設定開關的開啟和關閉,如下所示:
# 産生子產品開關
GENERATOR_PROCESS = True
# 驗證子產品開關
VALID_PROCESS = False
# 接口子產品開關
API_PROCESS = True
定義為True即可開啟該子產品,定義為False即關閉此子產品。
至此,我們的Cookies就全部完成了。接下來我們将子產品同時開啟,啟動排程器,控制台類似輸出如下所示:
API接口開始運作
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
Cookies生成程序開始運作
Cookies檢測程序開始運作
正在測試Cookies 使用者名 14747219309
正在生成Cookies 賬号 14747223314 密碼 asdf1129
Cookies有效 14747219309
正在測試Cookies 使用者名 14740691419
正在測試Cookies 使用者名 14740626332
Cookies有效 14740626332
Cookies有效 14740691419
Cookies有效 14740636046
正在測試Cookies 使用者名 14740618009
Cookies有效 14740618009
正在測試Cookies 使用者名 14740636046
拖動順序 [1, 4, 2, 3]
正在測試Cookies 使用者名 14747222472
Cookies有效 14747222472
Cookies檢測完成
驗證碼位置 420 580 384 544
成功比對
成功擷取到Cookies {'SUHB': '08J77UIj4w5n_T', 'SCF': 'AimcUCUVvHjswSBmTswKh0g4kNj4K7_U9k57YzxbqFt4SFBhXq3Lx4YSNO9VuBV841BMHFIaH4ipnfqZnK7W6Qs.', 'SSOLoginState': '1501439488', '_T_WM': '99b7d656220aeb9207b5db97743adc02', 'M_WEIBOCN_PARAMS': 'uicode%3D20000174', 'SUB': '_2A250elZQDeRhGeBM6VAR8ifEzTuIHXVXhXoYrDV6PUJbkdBeLXTxkW17ZoYhhJ92N_RGCjmHpfv9TB8OJQ..'}
成功儲存Cookies
以上所示是程式運作的控制台輸出内容,我們從中可以看到各個子產品都正常啟動,測試子產品逐個測試Cookies,生成子產品擷取尚未生成Cookies的賬号的Cookies,各個子產品并行運作,互不幹擾。
我們可以通路接口擷取随機的Cookies,如下圖所示。
爬蟲隻需要請求該接口就可以實作随機Cookies的擷取。
五、本節代碼
本節代碼位址為:https://github.com/Python3WebSpider/CookiesPool。
原文釋出時間為:2018-06-24
本文作者:崔慶才
本文來自雲栖社群合作夥伴“
Python愛好者社群”,了解相關資訊可以關注“
”。