1. 程式人生 > >媽媽再也不用擔心爬蟲被封號了!手把手教你搭建Cookies池

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

很多時候,在爬取沒有登入的情況下,我們也可以訪問一部分頁面或請求一些介面,因為畢竟網站本身需要做SEO,不會對所有頁面都設定登入限制。

但是,不登入直接爬取會有一些弊端,弊端主要有以下兩點。

  • 設定了登入限制的頁面無法爬取。如某論壇設定了登入才可檢視資源,某部落格設定了登入才可檢視全文等,這些頁面都需要登入賬號才可以檢視和爬取。

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

下面我們就第二種情況做一個簡單的實驗。以微博為例,我們先找到一個Ajax介面,例如新浪財經官方微博的資訊介面https://m.weibo.cn/api/container/getIndex?uid=1638782947&luicode=20000174&type=uid&value=1638782947&containerid=1005051638782947,如果用瀏覽器直接訪問,返回的資料是JSON格式,如下圖所示,其中包含了新浪財經官方微博的一些資訊,直接解析JSON即可提取資訊。

但是,這個介面在沒有登入的情況下會有請求頻率檢測。如果一段時間內訪問太過頻繁,比如開啟這個連結,一直不斷重新整理,則會看到請求頻率過高的提示,如下圖所示。

如果重新開啟一個瀏覽器視窗,開啟https://passport.weibo.cn/signin/login?entry=mweibo&r=https://m.weibo.cn/,登入微博賬號之後重新開啟此連結,則頁面正常顯示介面的結果,而未登入的頁面仍然顯示請求過於頻繁,如下圖所示。

圖中左側是登入了賬號之後請求介面的結果,右側是未登入賬號請求介面的結果,二者的介面連結是完全一樣的。未登入狀態無法正常訪問,而登入狀態可以正常顯示。

因此,登入賬號可以降低被封禁的概率。

我們可以嘗試登入之後再做爬取,被封禁的機率會小很多,但是也不能完全排除被封禁的風險。如果一直用同一個賬號頻繁請求,那就有可能遇到請求過於頻繁而封號的問題。

如果需要做大規模抓取,我們就需要擁有很多賬號,每次請求隨機選取一個賬號,這樣就降低了單個賬號的訪問頻率,被封的概率又會大大降低。

那麼如何維護多個賬號的登入資訊呢?這時就需要用到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個核心模組,如下圖所示。

Cookies池架構的基本模組分為4塊:儲存模組、生成模組、檢測模組、介面模組。每個模組的功能如下。

  • 儲存模組負責儲存每個賬號的使用者名稱密碼以及每個賬號對應的Cookies資訊,同時還需要提供一些方法來實現方便的存取操作。

  • 生成模組負責生成新的Cookies。此模組會從儲存模組逐個拿取賬號的使用者名稱和密碼,然後模擬登入目標頁面,判斷登入成功,就將Cookies返回並交給儲存模組儲存。

  • 檢測模組需要定時檢測資料庫中的Cookies。在這裡我們需要設定一個檢測連結,不同的站點檢測連結不同,檢測模組會逐個拿取賬號對應的Cookies去請求連結,如果返回的狀態是有效的,那麼此Cookies沒有失效,否則Cookies失效並移除。接下來等待生成模組重新生成即可。

  • 介面模組需要用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())

如果需要Python方面的入門知識可以點選這個連結獲取入門資料

這裡我們新建了一個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的獲取。

如果需要Python方面的入門知識可以點選這個連結獲取入門資料