第十部分 模擬登錄(模擬登錄GitHub並爬取、Cookies池的搭建)


前言:有些頁面的信息在爬蟲時需要登錄才能查看。打開網頁登錄后,在客戶端生成了Cookies,在Cookies中保存了SessionID的信息,登錄后的請求都會攜帶生成后的Cookies發送給服務器。服務器根據Cookies判斷出對應的SessionID,進而找到會話。如果當前會話有效,服務器就判斷用戶當前已登錄,返回請求的頁面信息,這樣就可以看到登錄后的頁面。

這里主要是獲取登錄后Cookies。要獲取Cookies可以手動在瀏覽器輸入用戶名和密碼后,再把Cookies復制出來,這樣做就增加了人工工作量,爬蟲的目的是自動化,需要用程序來完成這個過程,也就是用程序來模擬登錄。下面來了解模擬登錄相關方法及如何維護一個Cookies池。

一、 模擬登錄並爬取GitHub
模擬登錄的原理在於登錄后Cookies的維護。

了解模擬登錄GitHub的過程,同時爬取登錄后才可以訪問的頁面信息,如好友動態、個人信息等內容。

需要使用到的庫有:requests和 lxml 庫。

1、 分析登錄過程
打開GitHub的登錄頁面https://github.com/login,輸入用戶名和密碼,打開開發者工具,勾選Preserve Log選項,這表示顯示持續日志。點擊登錄按鈕,就會在開發者工具下方顯示各個請求過程。點擊第一個請求(session),進入其詳情頁面,如圖1-1所示。
圖1-1  session請求詳情面

                                                                             圖1-1     session請求詳情面
從圖上可看到請求的URL是 https://github.com/session,請求方式為POST。繼續往下看,可以觀察到它的Request Headers和Form Data 這兩部分內容。如圖1-2所示。
圖1-2  Request Headers和Form Data詳情頁面

                                                                                 圖1-2     Request Headers和Form Data詳情頁面
Headers里面包含了 Cookies、Host、Origin、Referer、User-Agent等信息。Form Data包含了6個字段,commit 是固定的字符串Sign in,utf8 是一個勾選字符,authenticity_token 較長,初步判斷是一個Base64加密的字符串,login是登錄的用戶名,password是登錄的密碼,webauthn-support是頁面認證,默認是supported。

由上可知,現在不能構造的內容有 Cookies和 authenticity_token。下面繼續看下這兩部分內容如何獲取。在登錄前訪問的是登錄頁面,該頁面是以GET形式訪問的。輸入用戶名和密碼,點擊登錄按鈕,瀏覽器發送這兩部分信息,也就是說Cookies和 authenticity_token一定是在訪問登錄頁面時候設置的。

再次退出登錄,清空Cookies,回到登錄頁。重新登錄,截獲發生的請求,如圖1-3所示。
圖1-3  截獲的請求

                                                                                               圖1-3     截獲的請求
在截獲的請求中,Response Headers有一個 Set-Cookie 字段。這就是設置 Cookies 的過程。另外,在Response Headers中沒有和authenticity_token相關的信息,這個 authenticity_token 可能隱藏在其他地方或者計算出來的。不過在網頁的源代碼中,搜索 authenticity_token 相關的字段,發現了源代碼里面隱藏着此信息,是由一個隱藏式表單元素。如圖1-4所示。
圖1-4  表單元素之authenticity_token

                                                                                       圖1-4     表單元素之authenticity_token
到此,已經獲取到了所有信息,接下來實現模擬登錄。

2、模擬登錄代碼實例
先來定義一個Login 類,初始化一些變量,代碼如下所示:

 1 import requests  2 from lxml import etree  3 class Login():  4     """登錄類,初始化一些變量"""
 5     def __init__(self):  6         self.headers = {  7             'Referer': 'https://github.com/login',  8             'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',  9             'Host': 'github.com', 10  } 11         self.login_url = 'https://github.com/login'
12         self.post_url = 'https://github.com/session'
13         self.logined_url = 'https://github.com/settings/profile'    # 登錄成功后的頁面
14         self.session = requests.Session()

這段代碼中最重要的一個變量是requests庫的 Session,它可以維持一個會話,而且可以自動處理 Cookies,不用擔心 Cookies的問題。接下來,訪問登錄頁面還要完成兩件事,一是通過登錄頁面獲取初始的 Cookies,二是提取出 authenticity_token。下面實現一個token()方法,代碼如下所示:

1 def token(self): 2     response = self.session.get(self.login_url, headers=self.headers) 3     selector = etree.HTML(response.text) 4     token = selector.xpath('//div//input[2]/@value')    # 注意獲取到的是一個列表類型
5     return token

這里用Session對象的 get() 方法訪問GitHub的登錄頁面,接着用XPath解析出登錄所需的 authenticity_token 信息並返回。現在已經獲取初始的 Cookies和authenticity_token,下面開始模擬登錄,實現一個 login() 方法,代碼如下所示:

 1 def login(self, email, password):  2     post_data = {  3         'commit': 'Sign in',  4         'utf8': '',  5         'authenticity_token': self.token()[0],  6         'login': email,  7         'password': password,  8         'webauthn-support': 'supported'
 9  } 10     response = self.session.post(self.post_url, data=post_data, headers=self.headers) 11     if response.status_code == 200: 12  self.dynamics(response.text) 13 
14     response = self.session.get(self.logined_url, headers=self.headers) 15     if response.status_code == 200: 16         self.profile(response.text)

這里先構造一個表單,復制各個字段,其中email和password是以變量的形式傳遞。然后再用Session對象的post()方法模擬登錄即可。由於 requests 自動處理了重定向信息,登錄成功后就可直接跳轉到首頁,首頁有顯示所關注人的動態信息,得到響應后調用dynamics()方法對其進行處理。接下來再用Session對象請求個人詳情頁,調用profile()方法處理個人詳情頁信息。其中,dynamics()和profile()方法的實現如下所示:

 1 def dynamics(self, html):  2     """處理登錄成功后的頁面,即主頁面內容"""
 3     # 頁面已經發生跳轉,該段代碼的輸出為空
 4     selector = etree.HTML(html)  5     print(html)  6     dynamics = selector.xpath('//div[contains(@class, "news")]//div[contains(@class, "Box")]')  7     for item in dynamics:  8         dynamic = ' '.join(item.xpath('.//div[@class="title"]//text()')).strip()  9         print(dynamic) 10 
11 def profile(self, html): 12     """處理登錄成功后的 profile 頁面"""
13     selector = etree.HTML(html) 14     # 下面獲取到的每一項數據都是列表
15     name = selector.xpath('//input[@id="user_profile_name"]/@value') 16     url = selector.xpath('//input[@id="user_profile_blog"]/@value') 17     company = selector.xpath('//input[@id="user_profile_company"]/@value') 18     location = selector.xpath('//input[@id="user_profile_location"]/@value') 19     email = selector.xpath('//select[@id="user_profile_email"]/option[@value!=""]/text()') 20     print(name, email, url, company, location) 21 
22 if __name__ == '__main__': 23     login = Login() 24     login.login(email='email or username', password='password')

這里用XPath對信息進行提取,在dynamics()方法里,提取所有的動態信息並輸出(網址已發生跳轉,輸出為空)。在profile()里,提取個人信息並將其輸出。現在完成了整個類的編寫,在最后面的if代碼塊中,先創建Login類對象,然后運行程序,通過調用login()方法傳入用戶名和密碼,成功實現了模擬登錄,並且成功輸出用戶個人信息。

利用requests的Session實現模擬登錄操作,最重要的是分析思路,只要各個參數都成功獲取,模擬登錄就沒有問題。登錄成功后,就相當於建立一個 Session會話,Session對象維護着Cookies的信息,直接請求就會得到模擬登錄成功后的頁面。

二、 Cookies池的搭建

不登錄直接爬取網站內容可能有下面的限制:
(1)、設置了登錄限制的頁面不能爬取。如某些論壇設置了登錄可查看資源,一些博客設置了登錄才可查看全文等。
(2)、有的頁面請求過於頻繁,訪問容易被限制或者IP被封,但是登錄后不會出現這些問題。因此登錄后被反爬的可能性低。

例如新浪財經官方微博的Ajax接口 https://m.weibo.cn/api/container/getIndex?uid=1804544030&type=uid&page=1&containerid=1076031804544030,這個網站用瀏覽器直接訪問返回JSON格式信息,直接解析JSON即可提取信息。這個接口在沒有登錄的情況下會有請求頻率檢測。一段時間內請求過於頻繁,請求就會被限制並提示請求過於頻繁。

重新打開瀏覽器窗口,打開 https://passport.weibo.cn/signin/login?entry=mweibo&r=https://m.weibo.cn/,登錄微博賬號后重新打開這API接口連接可以正常顯示。但是登錄后一直用同一個賬號頻繁請求,也會有可能被封號。所在在大規模抓取,就要擁有很多賬號,每次請求隨機選擇一個賬號,這樣降低單個賬號的訪問頻率,來降低被封的概率。要維護多個賬號的登錄信息,就要用到Cookies池。下面就Cookies池的搭建做一些了解。

以新浪微博為例實現一個Cookies池的搭建過程。Cookies池中保存了許多微博賬號和登錄后的Cookies信息,並且Cookies池還需要定時檢測每個Cookies的有效性,如果Cookies無效,就刪除該Cookies並模擬登錄生成的Cookies。同時Cookies池還需要一個重要的接口,即獲取隨機Cookies的接口,Cookies運行后,只要請求該接口,即可隨機獲得一個Cookies並用其爬取。由此可知,Cookies池需要自動生成Cookies、定時檢測Cookies、提供隨機Cookies等功能。

基本要求:Redis數據庫正常運行。Python的redis-py、requests、Selelnium和Flask庫。以及Chrome瀏覽器的安裝並配置 ChromeDriver。

1、Cookies池架構
Cookies池架構的基本模塊分為4塊:存儲模塊、生成模塊、檢測模塊和接口模塊。每個模塊功能如下:
(1)、存儲模塊負責存儲每個賬號的用戶名密碼以及每個賬號對應的Cookies信息,同時還需要提供一些方法來實現方便的存取操作。
(2)、生成模塊可生成新的Cookies。從存儲模塊獲取賬號的用戶名和密碼,然后模擬登錄目標頁面,判斷登錄成功,就將Cookies返回並交給存儲模塊存儲。
(3)、檢測模塊定時檢測數據庫中的Cookies。可設置一個檢測連接,不同的站點檢測連接不同,檢測模塊會逐個獲取賬號對應的Cookies去請求鏈接,如果返回的狀態是有效的,此Cookies就沒有失效,否則Cookies失效並移除。接下來等待生成模塊重新生成。
(4)、接口模塊用API對外提供服務接口。可用的Cookies有多個,可隨機返回Cookies的接口,這樣保證每個Cookies都有可能被取到。Cookies越多,每個Cookies被取到的概率越小,封號的風險也越小。

2、Cookies 池的實現
對各個模塊的實現過程做一些了解。

(1)、存儲模塊
存儲的內容有賬號信息和Cookies信息。賬號由用戶名和密碼組成,將用戶名和密碼在數據庫中存儲成映射關系。Cookies存成JSON字符串,並且要對應用戶名信息,實際也是用戶名和Cookies的映射。可以用Redis的Hash結構,需要建立兩個Hash結構,用戶名和密碼Hash,用戶名和Cookies的Hash。

Hash的Key對應賬號,Value對應密碼或者Cookies。還要注意的是,Cookies池要做到可擴展,也就是存儲的賬號和Cookies不一定只有新浪微博的,其他站點同樣可以對接此Cookies池,所以對Hash的名稱做二級分類,如存微博賬號的Hash名稱可以是 accounts:weibo,Cookies的名稱可以是 cookies:weibo。如果要擴展知乎的Cookies池,可使用 accounts:zhihu和 cookies:zhihu。

下面代碼創建一個存儲模塊類,用以提供一些Hash的基本操作,代碼如下:
首先將一些基本配置放在一個config.py文件,避免各個模塊的代碼雜亂,config.py 文件的代碼如下:

 1 # Redis 數據庫地址
 2 REDIS_HOST = '192.168.64.50'
 3 
 4 # Redis 端口
 5 REDIS_PORT = 6379
 6 
 7 # Redis密碼,無密碼就為 None
 8 REDIS_PASSWORD = None  9 
10 # 產生器使用的瀏覽器
11 BROWSER_TYPE = 'Chrome'
12 
13 # 產生器類,如要擴展其他站點,就在這里配置
14 GENERATOR_MAP = { 15     'weibo': 'WeiboCookiesGenerator', 16 } 17 
18 # 測試類,如要擴展其他站點,就在這里配置
19 TESTER_MAP = { 20     'weibo': 'WeiboValidTester', 21 } 22 
23 TEST_URL_MAP = { 24     'weibo': 'https://m.weibo.cn/api/container/getIndex?uid=1804544030&type=uid&page=1&containerid=1076031804544030', 25 } 26 
27 # 產生器和驗證器循環周期
28 CYCLE = 120
29 
30 # API地址和端口
31 API_HOST = '0.0.0.0'
32 API_PORT = 5000
33 
34 # 產生器開關,模擬登錄添加Cookies
35 GENERATOR_PROCESS = False 36 # 驗證器開關,循環檢測數據庫中Cookies是否可用,不可用刪除
37 VALID_PROCESS = False 38 # API接口服務
39 API_PROCESS = True

下面是存儲模塊的代碼,代碼如下所示:

 1 import random  2 import redis  3 from cookiespool.config import *
 4 
 5 class RedisClient():  6     def __init__(self, type, website, host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD):  7         """
 8  初始化Redis連接  9  :param type: 10  :param website: 11  :param host: 地址 12  :param port: 端口 13  :param password: 密碼 14         """
15         self.db = redis.StrictRedis(host=host, port=port, password=password, decode_responses=True) 16         self.type = type 17         self.website = website 18 
19     def name(self): 20         """
21  獲取Hash的名稱 22  :return: Hash名稱 23         """
24         return "{type}:{website}".format(type=self.type, website=self.website) 25 
26     def set(self, username, value): 27         """
28  設置鍵值對 29  :param username: 用戶名 30  :param value: 密碼或Cookies 31  :return: 32         """
33         return self.db.hset(self.name(), username, value) 34 
35     def get(self, username): 36         """
37  根據鍵名獲取鍵值 38  :param username: 用戶名 39  :return: 40         """
41         return self.db.hget(self.name(), username) 42 
43     def delete(self, username): 44         """
45  根據鍵名刪除鍵值對 46  :param username: 用戶名 47  :return: 刪除結果 48         """
49         return self.db.hdel(self.name(), username) 50 
51     def count(self): 52         """
53  獲取數目 54  :return: 數目 55         """
56         return self.db.hlen(self.name()) 57 
58     def random(self): 59         """
60  隨機得到鍵值,用於隨機Cookies獲取 61  :return: 隨機Cookies 62         """
63         return random.choice(self.db.hvals(self.name())) 64 
65     def username(self): 66         """
67  獲取所有賬戶信息 68  :return: 所有用戶名 69         """
70         return self.db.hkeys(self.name()) 71 
72     def all(self): 73         """
74  獲取所有鍵值對 75  :return: 用戶名和密碼或Cookies的映射表 76         """
77         return self.db.hgetall(self.name()) 78 
79 
80 if __name__ == '__main__': 81     conn = RedisClient('accounts', 'weibo') 82     result = conn.set('michael', 'python') 83     print(result)

首先創建RedisClient類,初始化__init__()方法的兩個關鍵參數type和website,分別代表類型和站點名稱,這是用來拼接Hash名稱的兩個字段。例如存儲賬戶的Hash,type是accounts、website是webo,如果是存儲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即可。詳細代碼如下:

 1 import time  2 from io import BytesIO  3 from PIL import Image  4 #from selenium import webdriver
 5 from selenium.common.exceptions import TimeoutException  6 from selenium.webdriver import ActionChains  7 from selenium.webdriver.common.by import By  8 from selenium.webdriver.support.ui import WebDriverWait  9 from selenium.webdriver.support import expected_conditions as EC  10 from os import listdir  11 from os.path import abspath, dirname  12 
 13 TEMPLATER_FOLDER = dirname(abspath(__file__)) + '/templates/'
 14 
 15 class WeiboCookies():  16     def __init__(self, username, password, browser):  17         self.url = 'https://passport.weibo.cn/signin/login?entry=mweibo&r=https://m.weibo.cn/'
 18         self.browser = browser  19         self.wait = WebDriverWait(self.browser, 20)  20         self.username = username  21         self.password = password  22 
 23     def open(self):  24         """
 25  打開網頁輸入用戶名密碼並點擊  26  :return: None  27         """
 28         self.browser.delete_all_cookies()       # 首先清除瀏覽器緩存的Cookies
 29  self.browser.get(self.url)  30         username = self.wait.until(EC.presence_of_element_located((By.ID, 'loginName')))  31         password = self.wait.until(EC.presence_of_element_located((By.ID, 'loginPassword')))  32         submit = self.wait.until(EC.element_to_be_clickable((By.ID, 'loginAction')))  33  username.send_keys(self.username)  34  password.send_keys(self.password)  35         time.sleep(1)  36  submit.click()  37 
 38     def password_error(self):  39         """
 40  判斷是否密碼錯誤  41  :return:  42         """
 43         try:  44             return WebDriverWait(self.browser, 5).until(  45                 EC.text_to_be_present_in_element((By.ID, 'errorMsg'), '用戶名或密碼錯誤')  46  )  47         except TimeoutException:  48             return False  49 
 50     def login_successfully(self):  51         """
 52  判斷是否登錄成功  53  :return:  54         """
 55         try:  56             return bool(  57                 WebDriverWait(self.browser, 5).until(EC.presence_of_element_located((By.CLASS_NAME, 'lite-iconf-profile'))))  58         except TimeoutException:  59             return False  60 
 61     def get_position(self):  62         """
 63  獲取驗證碼位置  64  :return: 驗證碼位置元組  65         """
 66         try:  67             img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'patt-shadow')))  68         except TimeoutException:  69             print('未出現驗證碼')  70  self.open()  71         time.sleep(2)  72         location = img.location  73         size = img.size  74         top, bottom, left, right =location['y'], location['y'] + size['height'], location['x'], location['x'] + size['width']  75         return (top, bottom, left, right)  76 
 77     def get_screenshot(self):  78         """
 79  獲取網頁截圖  80  :return: 截圖對象  81         """
 82         screenshot = self.browser.get_screenshot_as_png()  83         screenshot = Image.open(BytesIO(screenshot))  84         return screenshot  85 
 86     def get_image(self):  87         """
 88  獲取驗證碼圖片  89  :return: 圖片對象  90         """
 91         top, bottom, left, right = self.get_position()  92         print('驗證碼位置', top, bottom, left, right)  93         screenshot = self.get_screenshot()  94         captcha = screenshot.crop((left, top, right, bottom))  95         return captcha  96 
 97     def is_pixel_equal(self, image1, image2, x, y):  98         """
 99  判斷兩個像素是否相同 100  :param image1: 圖片1 101  :param image2: 圖片2 102  :param x: 位置x 103  :param y: 位置y 104  :return: 像素是否相同 105         """
106         # 取兩個圖片的像素點
107         pixel1 = image1.load()[x, y] 108         pixel2 = image2.load()[x, y] 109         threshold = 20
110         if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs( 111             pixel1[2] - pixel2[2]) < threshold: 112             return True 113         else: 114             return False 115 
116     def same_image(self, image, template): 117         """
118  識別相似驗證碼 119  :param image: 待識別的驗證碼 120  :param template: 模板 121  :return: 122         """
123         # 相似度閾值
124         threshold = 0.99
125         count = 0 126         for x in range(image.width): 127             for y in range(image.height): 128                 # 判斷像素是否相同
129                 if self.is_pixel_equal(image, template, x, y): 130                     count += 1
131         result = float(count) / (image.width * image.height) 132         if result > threshold: 133             print('成功匹配') 134             return True 135         return False 136 
137     def detect_image(self, image): 138         """
139  匹配圖片 140  :param image: 圖片 141  :return: 手動順序 142         """
143         for template_name in listdir(TEMPLATER_FOLDER): 144             print('正在匹配', template_name) 145             template = Image.open(TEMPLATER_FOLDER + template_name) 146             if self.same_image(image, template): 147                 # 返回順序
148                 numbers = [int(number) for number in list(template_name.split('.')[0])] 149                 print('拖動順序', numbers) 150                 return numbers 151 
152     def move(self, numbers): 153         """
154  根據順序拖動 155  :param numbers: 156  :return: 157         """
158         # 獲得四個按點
159         try: 160             circles = self.browser.find_elements_by_css_selector('.patt-wrap .patt-circ') 161             dx = dy = 0 162             for index in range(4): 163                 circle = circles[numbers[index] - 1] 164                 # 如果是第一次循環
165                 if index == 0: 166                     # 點擊第一個按點
167  ActionChains(self.browser) \ 168                         .move_to_element_with_offset(circle, circle.size['width'] / 2, circle.size['height'] / 2) \ 169  .click_and_hold().perform() 170                 else: 171                     # 小幅移動次數
172                     times = 30
173                     # 拖動
174                     for i in range(times): 175                         ActionChains(self.browser).move_by_offset(dx / times, dy / times).perform() 176                         time.sleep(1 / times) 177                 # 如果是最后一次循環
178                 if index == 3: 179                     # 松開鼠標
180  ActionChains(self.browser).release().perform() 181                 else: 182                     # 計算下一次偏移
183                     dx = circle[numbers[index + 1] - 1].location['x'] - circle.location['x'] 184                     dy = circle[numbers[index + 1] - 1].location['y'] - circle.location['y'] 185         except: 186             return False 187 
188     def get_cookies(self): 189         """
190  獲取Cookies 191  :return: 192         """
193         return self.browser.get_cookies() 194 
195     def main(self): 196         """
197  破解入口 198  :return: 199         """
200  self.open() 201         if self.password_error(): 202             return { 203                 'status': 2, 204                 'content': '用戶名或密碼錯誤'
205  } 206         # 如果不需驗證碼直接登錄成功
207         if self.login_successfully(): 208             cookies = self.get_cookies() 209             return { 210                 'status': 1, 211                 'content': cookies 212  } 213         # 獲取驗證碼圖片
214         image = self.get_image() 215         numbers = self.detect_image(image) 216  self.move(numbers) 217         if self.login_successfully(): 218             cookies = self.get_cookies()    # content鍵對應的值是列表,列表內是字典
219             return { 220                 'status': 1, 221                 'content': cookies 222  } 223         else: 224             return { 225                 'status': 3, 226                 'content': '登錄失敗'
227  } 228 
229 
230 if __name__ == '__main__': 231     browser = webdriver.Chrome() 232     result = WeiboCookies('qq_number@qq.com', 'password', browser).main() 233     print(result)

在 WeiboCookies 類中,首先對接了新浪微博的四宮格驗證碼。在main() 方法中,調用cookies的獲取方法,並針對不同的情況返回不同的結果。返回結果類型是字典,並且附有狀態碼status,在生成模塊中可以根據不同的狀態碼做不同的處理。例如狀態碼為1時,表示成功獲取Cookies,只需將Cookies保存到數據庫即可。狀態碼為2表示用戶名和密碼錯誤,這時就應該把當前數據庫中存儲的賬號信息刪除。如果狀態碼為3時,則表示登錄失敗,此時不能判斷是否用戶名或密碼錯誤,也不能成功獲取Cookies,這時可做一些提示,進行下一個處理即可,完整的實現代碼如下所示:

 1 import json  2 from selenium import webdriver  3 from selenium.webdriver import DesiredCapabilities  4 from cookiespool.config import *
 5 from redisdb import RedisClient  6 from login.weibo.cookies import WeiboCookies  7 
 8 
 9 class CookiesGenerator():  10     def __init__(self, website='default'):  11         """
 12  父類,初始化一些對象  13  :param website: 名稱  14         """
 15         self.website = website  16         self.cookies_db = RedisClient('cookies', self.website)      # 創建Redis數據庫連接,參數是Redis的Hash鍵要用到的
 17         self.accounts_db = RedisClient('accounts', self.website)  18  self.init_browser()  19 
 20     def __del__(self):  21  self.close()  22 
 23     def init_browser(self):  24         """
 25  通過browser參數初始化全局瀏覽器供模擬登錄使用  26  :return:  27         """
 28         if BROWSER_TYPE == 'PhantomJS':  29             caps = DesiredCapabilities.PHANTOMJS  30             caps["phantomjs.page.settings.userAgent"] = \  31                 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'
 32             self.browser = webdriver.PhantomJS(desired_capabilities=caps)  33             self.browser.set_window_size(1300, 500)  34         elif BROWSER_TYPE == 'Chrome':  35             self.browser = webdriver.Chrome()  36 
 37     def new_cookies(self, username, password):  38         """
 39  新生成Cookies,子類需要重寫  40  :param username: 用戶名  41  :param password: 密碼  42  :return:  43         """
 44         raise NotImplementedError  45 
 46     def process_cookies(self, cookies):  47         """
 48  處理Cookies  49  :param cookies:  50  :return:  51         """
 52         dict = {}  53         for cookie in cookies:  54             dict[cookie['name']] = cookie['value']  55         return dict  56 
 57     def run(self):  58         """
 59  運行,得到所有賬戶名,然后順序模擬登錄  60  :return:  61         """
 62         accounts_usernames = self.accounts_db.usernames()  63         cookies_usernames = self.cookies_db.usernames()  64 
 65         for username in accounts_usernames:  66             if not username in cookies_usernames:  67                 password = self.accounts_db.get(username)  68                 print('正在生成Cookies', '賬號', username, '密碼', password)  69                 result = self.new_cookies(username, password)  70                 # 獲取成功
 71                 if result.get('status') == 1:  72                     cookies = self.process_cookies(result.get('content'))  73                     print('成功獲取到Cookies', cookies)  74                     if self.cookies_db.set(username, json.dumps(cookies)):  75                         print('成功保存Cookies')  76                 # 密碼錯誤,移除賬號
 77                 elif result.get('status') == 2:  78                     print(result.get('content'))  79                     if self.accounts_db.delete(username):  80                         print('成功刪除賬號')  81                 else:  82                     print(result.get('content'))  83         else:  84             print('所有賬號都已經成功獲取Cookies')  85 
 86     def close(self):  87         """
 88  關閉  89  :return:  90         """
 91         try:  92             print('Closing Browser')  93  self.browser.close()  94             del self.browser  95         except TypeError:  96             print('Browser not opened')  97 
 98 
 99 class WeiboCookiesGenerator(CookiesGenerator): 100     def __init__(self, website='weibo'): 101         """
102  初始化操作 103  :param website: 104         """
105         CookiesGenerator.__init__(self, website) 106         self.website = website 107 
108     def new_cookies(self, username, password): 109         """
110  生成Cookies 111  :param username: 用戶名 112  :param password: 密碼 113  :return: 用戶名和Cookies 114         """
115         # 調用了 login模塊下的cookies.py文件中的 WeiboCookies,self.browser由父類提供
116         return WeiboCookies(username, password, self.browser).main() 117 
118 
119 if __name__ == '__main__': 120     generator = WeiboCookiesGenerator(website='https://passport.weibo.cn/signin/login?entry=mweibo&r=https://m.weibo.cn/') 121     generator.run()

要擴展其他站點,只要實現new_cookies() 方法即可,然后按此規則返回對應的模擬登錄結果,如1代表獲取成功,2代表用戶名或密碼錯誤。

3、 檢測模塊
Cookies時間太長導致失效,或者Cookies使用太頻繁造成無法正常請求網頁。有這樣的Cookies需要及時清理或者替換。所以需要一個定時檢測模塊來遍歷Cookies池中的所有Cookies,同時設置好對應的檢測鏈接,用每個Cookies去請求這個鏈接。請求成功或者狀態碼合法,則該Cookies有效;請求失敗,或者無法獲取正常數據,如跳轉到登錄頁面或者驗證頁面,則此Cookies無效,需要將該Cookies從數據庫中移除。

移除Cookies后,前面的生成模塊就會檢測到Cookies的Hash和賬號的Hash相比少了此賬號的Cookies,生成模塊就會認為這個賬號還沒有生成Cookies,就用此賬號重新登錄,此賬號的Cookies又被重新更新。

檢測模塊主要作用是檢測Cookies失效,將其從數據庫中移除。要考慮通用可擴展性,首先定義一個檢測器的父類,聲明一些通用組件,代碼如下所示:

 1 import json  2 import requests  3 from requests.exceptions import ConnectionError  4 from redisdb import *
 5 
 6 class ValidTester():  7     def __init__(self, website='default'):  8         self.website = website  9         self.cookies_db = RedisClient('cookies', self.website) 10         self.accouts_db = RedisClient('account', self.website) 11 
12     def test(self, username, cookies): 13         """為了便於擴展,該方法由子類來實現"""
14         raise NotImplementedError 15 
16     def run(self): 17         cookies_groups = self.cookies_db.all() 18         for username, cookies in cookies_groups.items(): 19             self.test(username, cookies)        # 調用 test 方法測試,子類提供 test 方法
20 
21 class WeiboValidTester(ValidTester): 22     """測試微博,如果要測試其他網站,可創建相應的測試類,並且繼承ValidTester類"""
23     def __init__(self, website='weibo'): 24         ValidTester.__init__(self, website) 25 
26     def test(self, username, cookies): 27         print('正在測試Cookies', '用戶名', username) 28         try: 29             cookies = json.loads(cookies) 30         except TypeError: 31             print('Cookies不合法', username) 32  self.cookies_db.delete(username) 33             print('刪除Cookies', username) 34             return
35         # 如果上面的try代碼塊沒有引發異常,就執行下面的try代碼塊
36         try: 37             test_url = TEST_URL_MAP[self.website] 38             response = requests.get(test_url, cookies=cookies, timeout=5, allow_redirects=False) 39             if response.status_code == 200: 40                 print('Cookies有效', username) 41             else: 42                 print(response.status_code, response.headers) 43                 print('Cookies失效', username) 44  self.cookies_db.delete(username) 45                 print('刪除Cookies', username) 46         except ConnectionError as e: 47             print('發生異常', e.args) 48 
49 if __name__ == '__main__': 50     WeiboValidTester().run()

這段代碼中定義了一個父類ValidTester,在其__init__()方法中指定了站點名稱website,另外建立兩個存儲模塊連接對象cookies_db 和 accounts_db,分別負責操作Cookies 和賬號的hash,run()方法是入口,這里遍歷了所有的Cookies,然后調用test()方法進行測試,test()方法由子類來實現,每個子類負責各自不同的網站的檢測。如檢測微博的可定義為WeiboValidTester,實現其獨有的 test() 方法來檢測微博的Cookies是否合法,然后做相應的處理。WeiboValidTester類就是繼承了ValidTester類的子類。

子類的test()方法首先將Cookies轉化為字典,檢測Cookies的格式,如果格式不正確,直接將其刪除,如果沒有格式問題,就拿此 Cookies請求被檢測的URL。test()方法在這里檢測的是微博,檢測的URL可以是某個Ajax接口,為了實現可配置化,將測試URL也定義成字典,如下所示:
TEST_URL_MAP = {'weibo': 'https://m.weibo.cn/'}
要擴展(檢測)其他站點,可統一在字典里添加。對微博來說,用Cookies去請求目標站點,同時禁止重定向和設置超時時間,得到響應后檢測其返回狀態碼。返回的是200,則Cookies有效,如果遇到302跳轉等情況,一般會跳轉到登錄頁面,則 Cookies已失效,此時將失效的Cookies從Cookies的Hash里移除即可。

4、接口模塊
生成模塊和檢測模塊定時運行可完成Cookies實時檢測和更新。但Cookies最終是給爬蟲用的,同時一個Cookies池可供多個爬蟲使用,所以需要定義一個Web接口,爬蟲訪問該接口就可獲取隨機的Cookies。這個接口用Flask來搭建,代碼如下所示:

 1 import json  2 from flask import Flask, g  3 from cookiespool.config import *
 4 from redisdb import *
 5 
 6 __all__ = ['app']  7 
 8 app = Flask(__name__)  9 
10 @app.route('/') 11 def index(): 12     return '<h2>Welcome to Cookie Pool System</h2>'
13 
14 
15 def get_conn(): 16     """
17  獲取 18  :return: 19     """
20     for website in GENERATOR_MAP: 21         print(website) 22         if not hasattr(g, website): 23             setattr(g, website + '_cookies', eval('RedisClient' + '("cookies","' + website + '")')) 24             setattr(g, website + '_accounts', eval('RedisClient' + '("accounts", "' + website + '")')) 25     return g 26 
27 
28 @app.route('/<website>/random') 29 def random(website): 30     """
31  獲取隨機的Cookie,訪問地址如 /weibo/random 32  :param website: 33  :return: 隨機Cookie 34     """
35     g = get_conn() 36     cookies = getattr(g, website + '_cookies').random() 37     return cookies 38 
39 
40 @app.route('/<website>/add/<username>/<password>') 41 def add(website, username, password): 42     """
43  添加用戶,訪問地址如 /weibo/add/user/password 44  :param website: 站點 45  :param username: 用戶名 46  :param password: 密碼 47  :return: 48     """
49     g = get_conn() 50     print(username, password) 51     getattr(g, website + '_accounts').set(username, password) 52     return json.dumps({'status': '1'}) 53 
54 
55 @app.route('/<website>/count') 56 def count(website): 57     """
58  獲取Cookies總數 59     """
60     g = get_conn() 61     count = getattr(g, website + '_cookies').count() 62     return json.dumps({'status': '1', 'count': count}) 63 
64 if __name__ == '__main__': 65     app.run(host='127.0.0.1')

這里random方法實現通用的配置來對接不同的站點,所以接口鏈接的第一個字段定義為站點名稱,第二個字段定義為獲取方法,例如 /weibo/random是獲取微博的隨機Cookies,/zhihu/random是獲取知乎的隨機Cookies。

5、調度模塊
最后再加一個調度模塊,讓這幾個模塊配合起來運行,主要工作就是驅動幾個模塊定時運行,同時各個模塊需要在不同的進程上運行,代碼實現如下所示:

 1 import time  2 from multiprocessing import Process  3 
 4 from cookiesapi import app  5 from cookiespool.config import *
 6 from cookiespool.generator import *
 7 from cookiespool.tester import *
 8 
 9 class Scheduler(object): 10 
11  @staticmethod 12     def valid_cookie(cycle=CYCLE): 13         while True: 14             print('Cookies 檢測進程開始運行') 15             try: 16                 for website, cls in TESTER_MAP.items(): 17                     tester = eval(cls + '(website="' + website + '"")') 18  tester.run() 19                     print('Cookies 檢測完成') 20                     del tester 21  time.sleep(cycle) 22             except Exception as e: 23                 print(e.args) 24 
25  @ staticmethod 26     def generate_cookie(cycle=CYCLE): 27         while True: 28             print("Cookies生成進程開始運行") 29             try: 30                 for website, cls in GENERATOR_MAP.items(): 31                     generator = eval(cls + '(website="' + website + '")') 32  generator.run() 33                     print('Cookies 生成完成') 34  generator.close() 35  time.sleep(cycle) 36             except Exception as e: 37                 print(e.args) 38 
39  @staticmethod 40     def api(): 41         print('API接口開始運行') 42         app.run(host=API_HOST, port=API_PORT) 43 
44     def run(self): 45         if API_PROCESS: 46             api_process = Process(target=Scheduler.api) 47  api_process.start() 48 
49         if GENERATOR_PROCESS: 50             generate_process = Process(target=Scheduler.generate_cookie) 51  generate_process.start() 52 
53         if VALID_PROCESS: 54             valid_process = Process(target=Scheduler.valid_cookie) 55             valid_process.start()

代碼中用到的兩個重要配置是,產生模塊類和測試模塊類的字典配置,該配置信息在 config 模塊中,配置信息如下所示:

1 # 產生器類,如要擴展其他站點,就在這里配置
2 GENERATOR_MAP = { 3     'weibo': 'WeiboCookiesGenerator', 4 } 5 
6 # 測試類,如要擴展其他站點,就在這里配置
7 TESTER_MAP = { 8     'weibo': 'WeiboValidTester', 9 }

這樣配置可方便動態擴展使用,鍵名是站點名稱,鍵值是類名。如有需要配置其它站點,可在字典中添加,例如要擴展知乎站點的產生模塊,可以這樣配置:

1 GENERATOR_MAP = { 2     'weibo': 'WeiboCookiesGenerator', 3     'zhihu': 'ZhihuCookiesGenerator', 4 }


Scheduler類里對字典遍歷,並利用 eval() 方法創建各個類的對象,調用其入口 run() 方法運行各個模塊。同時,各個模塊的多進程使用了 multiprocessing 中的 Process 類,調用其 start()方法即可啟動各個進程。

最后,還需要為各個模塊設置一個開關,可以在配置文件中設置開關的開啟和關閉狀態,如下所示:

1 # 產生器開關,模擬登錄添加Cookies
2 GENERATOR_PROCESS = False 3 # 驗證器開關,循環檢測數據庫中Cookies是否可用,不可用刪除
4 VALID_PROCESS = False 5 # API接口服務
6 API_PROCESS = True


這幾個開關的值為True則開啟,為False則為關閉。要讓代碼能夠成功運行,還需要導入賬號和密碼,為此再寫一個導入賬號和密碼的模塊,這個模塊的代碼如下所示:

 1 from redisdb import RedisClient  2 
 3 conn = RedisClient('accounts', 'weibo')  4 
 5 def set(account, sep='----'):  6     username, password = account.split(sep)  7     result = conn.set(username, password)  8     print('賬號', username, '密碼', password)  9     print('錄入成功' if result else '錄入失敗') 10 
11 
12 def scan(): 13     print('請輸入賬號密碼組,輸入exit退出讀入') 14     while True: 15         account = input() 16         if account == 'exit': 17             break
18  set(account) 19 
20 
21 if __name__ == '__main__': 22     scan()


運行這個模塊,就將錄入的賬號和密碼存儲到 Redis 數據庫中。最終,還需要寫一個總的運行程序入口模塊,這個模塊很簡單,主要是調用調度模塊的run()方法運行程序。

1 from cookiespool.scheduler import Scheduler 2 
3 def main(): 4     s = Scheduler() 5  s.run() 6 
7 if __name__ == '__main__': 8     main()


經測試,代碼運行成功,各個模塊都正常啟動,測試模塊逐個測試Cookies,生成模塊獲取還未生成Cookies的賬號的Ccookies,各個模塊並行運行,互不干擾。這里測試了一個賬號,控制台的輸出信息如下所示:

Cookies 檢測進程開始運行 API接口開始運行 * Serving Flask app "cookiesapi" (lazy loading) * Environment: production WARNING: Do not use the development server in a production environment. Use a production WSGI server instead. * Debug mode: off Cookies 檢測完成 Cookies生成進程開始運行 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit) 正在生成Cookies 賬號 1234567890 密碼 abcd1234 (這里的賬號和密碼不是真實輸出的賬號和密碼) 成功獲取到Cookies {'M_WEIBOCN_PARAMS': 'uicode%3D10000011%26fid%3D102803', 'MLOGIN': '1', ...(后面省略)} 成功保存Cookies 所有賬號都已經成功獲取Cookies Cookies 生成完成 Closing Browser


此時在瀏覽器地址欄訪問接口 http://127.0.0.1:5000/weibo/random 也能正確看到隨機生成的 cookies,如下圖1-5所示,爬蟲項目只要請求該接口就可實現隨機Cookies的獲取。
圖1-5  瀏覽器上隨機獲取cookies

                                                                              圖1-5     瀏覽器上隨機獲取cookies


免責聲明!

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



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