前言:有些頁面的信息在爬蟲時需要登錄才能查看。打開網頁登錄后,在客戶端生成了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請求詳情面
從圖上可看到請求的URL是 https://github.com/session,請求方式為POST。繼續往下看,可以觀察到它的Request Headers和Form Data 這兩部分內容。如圖1-2所示。
圖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 截獲的請求
在截獲的請求中,Response Headers有一個 Set-Cookie 字段。這就是設置 Cookies 的過程。另外,在Response Headers中沒有和authenticity_token相關的信息,這個 authenticity_token 可能隱藏在其他地方或者計算出來的。不過在網頁的源代碼中,搜索 authenticity_token 相關的字段,發現了源代碼里面隱藏着此信息,是由一個隱藏式表單元素。如圖1-4所示。
圖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