媽媽不再用擔憂爬蟲被封號了!手把手教你搭建Cookies池

不少時候,在爬取沒有登陸的狀況下,咱們也能夠訪問一部分頁面或請求一些接口,由於畢竟網站自己須要作SEO,不會對全部頁面都設置登陸限制。
html

可是,不登陸直接爬取會有一些弊端,弊端主要有如下兩點。git

  • 設置了登陸限制的頁面沒法爬取。如某論壇設置了登陸纔可查看資源,某博客設置了登陸纔可查看全文等,這些頁面都須要登陸帳號才能夠查看和爬取。github

  • 一些頁面和接口雖然能夠直接請求,可是請求一旦頻繁,訪問就容易被限制或者IP直接被封,可是登陸以後就不會出現這樣的問題,所以登陸以後被反爬的可能性更低。web

下面咱們就第二種狀況作一個簡單的實驗。以微博爲例,咱們先找到一個Ajax接口,例如新浪財經官方微博的信息接口https://m.weibo.cn/api/container/getIndex?uid=1638782947&luicode=20000174&type=uid&value=1638782947&containerid=1005051638782947,若是用瀏覽器直接訪問,返回的數據是JSON格式,以下圖所示,其中包含了新浪財經官方微博的一些信息,直接解析JSON便可提取信息。redis

可是,這個接口在沒有登陸的狀況下會有請求頻率檢測。若是一段時間內訪問太過頻繁,好比打開這個連接,一直不斷刷新,則會看到請求頻率太高的提示,以下圖所示。數據庫

若是從新打開一個瀏覽器窗口,打開https://passport.weibo.cn/signin/login?entry=mweibo&r=https://m.weibo.cn/,登陸微博帳號以後從新打開此連接,則頁面正常顯示接口的結果,而未登陸的頁面仍然顯示請求過於頻繁,以下圖所示。json

圖中左側是登陸了帳號以後請求接口的結果,右側是未登陸帳號請求接口的結果,兩者的接口連接是徹底同樣的。未登陸狀態沒法正常訪問,而登陸狀態能夠正常顯示。flask

所以,登陸帳號能夠下降被封禁的機率。api

咱們能夠嘗試登陸以後再作爬取,被封禁的概率會小不少,可是也不能徹底排除被封禁的風險。若是一直用同一個帳號頻繁請求,那就有可能遇到請求過於頻繁而封號的問題。瀏覽器

若是須要作大規模抓取,咱們就須要擁有不少帳號,每次請求隨機選取一個帳號,這樣就下降了單個帳號的訪問頻率,被封的機率又會大大下降。

那麼如何維護多個帳號的登陸信息呢?這時就須要用到Cookies池了。接下來咱們看看Cookies池的構建方法。

1、本節目標

咱們以新浪微博爲例來實現一個Cookies池的搭建過程。Cookies池中保存了許多新浪微博帳號和登陸後的Cookies信息,而且Cookies池還須要定時檢測每一個Cookies的有效性,若是某Cookies無效,那就刪除該Cookies並模擬登陸生成新的Cookies。同時Cookies池還須要一個很是重要的接口,即獲取隨機Cookies的接口,Cookies運行後,咱們只需請求該接口,便可隨機得到一個Cookies並用其爬取。

因而可知,Cookies池須要有自動生成Cookies、定時檢測Cookies、提供隨機Cookies等幾大核心功能。

2、準備工做

搭建以前確定須要一些微博的帳號。須要安裝好Redis數據庫並使其正常運行。須要安裝Python的RedisPy、requests、Selelnium、Flask庫。另外,還須要安裝Chrome瀏覽器並配置好ChromeDriver。

3、Cookies池架構

Cookies的架構和代理池相似,一樣是4個核心模塊,以下圖所示。

Cookies池架構的基本模塊分爲4塊:存儲模塊、生成模塊、檢測模塊、接口模塊。每一個模塊的功能以下。

  • 存儲模塊負責存儲每一個帳號的用戶名密碼以及每一個帳號對應的Cookies信息,同時還須要提供一些方法來實現方便的存取操做。

  • 生成模塊負責生成新的Cookies。此模塊會從存儲模塊逐個拿取帳號的用戶名和密碼,而後模擬登陸目標頁面,判斷登陸成功,就將Cookies返回並交給存儲模塊存儲。

  • 檢測模塊須要定時檢測數據庫中的Cookies。在這裏咱們須要設置一個檢測連接,不一樣的站點檢測連接不一樣,檢測模塊會逐個拿取帳號對應的Cookies去請求連接,若是返回的狀態是有效的,那麼此Cookies沒有失效,不然Cookies失效並移除。接下來等待生成模塊從新生成便可。

  • 接口模塊須要用API來提供對外服務的接口。因爲可用的Cookies可能有多個,咱們能夠隨機返回Cookies的接口,這樣保證每一個Cookies都有可能被取到。Cookies越多,每一個Cookies被取到的機率就會越小,從而減小被封號的風險。

以上設計Cookies池的的基本思路和前面講的代理池有類似之處。接下來咱們設計總體的架構,而後用代碼實現該Cookies池。

4、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())複製代碼

這裏咱們新建了一個RedisClient類,初始化__init__()方法有兩個關鍵參數typewebsite,分別表明類型和站點名稱,它們就是用來拼接Hash名稱的兩個字段。若是這是存儲帳戶的Hash,那麼此處的typeaccountswebsiteweibo,若是是存儲Cookies的Hash,那麼此處的typecookieswebsiteweibo

接下來還有幾個字段表明了Redis的鏈接信息,初始化時得到這些信息後初始化StrictRedis對象,創建Redis鏈接。

name()方法拼接了typewebsite,組成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_dbaccounts_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 帳號 14747223314 密碼 asdf1129
正在測試Cookies 用戶名 14747219309
Cookies有效 14747219309
正在測試Cookies 用戶名 14740626332
Cookies有效 14740626332
正在測試Cookies 用戶名 14740691419
Cookies有效 14740691419
正在測試Cookies 用戶名 14740618009
Cookies有效 14740618009
正在測試Cookies 用戶名 14740636046
Cookies有效 14740636046
正在測試Cookies 用戶名 14747222472
Cookies有效 14747222472
Cookies檢測完成
驗證碼位置 420 580 384 544
成功匹配
拖動順序 [1, 4, 2, 3]
成功獲取到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的獲取。

5、本節代碼

本節代碼地址爲:https://github.com/Python3WebSpider/CookiesPool。


本資源首發於崔慶才的我的博客靜覓: Python3網絡爬蟲開發實戰教程 | 靜覓

如想了解更多爬蟲資訊,請關注個人我的微信公衆號:進擊的Coder

weixin.qq.com/r/5zsjOyvEZ… (二維碼自動識別)

相關文章
相關標籤/搜索