很多情況下,頁面的某些信息需要登錄才可以查看。
這里的核心是獲取登陸之后的 Cookies 。話不多說,操練起來。
1. 模擬登錄並爬取GitHub
1.1 環境准備
- requests庫
- lxml庫
1.2 分析登錄過程
打開Github的登錄頁面,https://github.com/login.輸入用戶名和密碼,打開開發者工具,勾選preserve log,這表示顯示持續日志。
點擊登錄按鈕,可以看到各個請求過程。
點擊第一個請求,進入詳細界面:
Headers 里面包含了:
From Data包含了:
- commit 是固定的字符串Sign in
- utf-8 是一個勾選字符,
- authenticity_token較長,初步判斷是一個Base64 加密的字符串
- login是登陸的用戶名
- password 是密碼
綜上,我們無法直接構造的內容包括Cookies和authenticity_token ,下面看一下這兩部分怎么獲取。
在登錄之前我們會訪問一個登錄頁面,此頁面是通過Get形式訪問的。輸入用戶名和密碼,點擊登錄按鈕,瀏覽器發送這兩部分內容,也就是說Cookies和authenticity_token一定是在訪問登錄頁面時設置的。
我們退出登錄,回到登錄頁,同時清空Cookies ,重新訪問登錄頁,截獲發生的請求:
Response Headers有一個Set-Cookie 字段。這就是設置Cookie的過程。
我們發現Response Headers 沒有authenticity_token相關的信息,所以authenticity_token可能隱藏在其他的地方或者是計算出來的。從網頁的源碼查看,搜索相關字段,發現源代碼里面隱藏着此信息,他是一個隱藏式表單元素。
現在我們已經獲取到所有信息,接下來實現模擬。
3. 代碼實戰
首先定義一個Login 類,初始化一些變量:
import requests
class Login(object): def __init__(self): self.headers = { 'Referer': 'https://github.com/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133 Safari/537.36', 'Host': 'github.com' } self.login_url = 'https://github.com/login' self.post_url = 'https://github.com/session' self.logined_url = 'https://github.com/settings/profile' self.session = requests.Session() //維持會話,自動處理cookies
接下來,訪問登錄頁面需要完成兩件事:
-
- 通過此頁面獲取初始的 cookies;
- 提取 authenticit_token;
from lxml import etree def token(self): response = self.session.get(self.login_url, headers=self.headers) #用Session對象的get()方法訪問GitHub的登錄頁面
selector = etree.HTML(response.text) token = selector.xpath('//div//input[2]/@value')[0] #用Xpath解析出登錄所需的authenticity_token 信息並返回
return token
開始模擬登錄,實現一個login()方法:
def login(self, email, password): #首先構造一個表單,其中email和password是以變量的形式傳遞。
post_data = { 'commit': 'Sign in', 'utf8': '√', 'authenticity_token': self.token(), 'login': email, 'password': password } response = self.session.post(self.post_url, data=post_data, headers=self.headers) #用Session對象的post()方法模擬登錄
if response.status_code == 200: self.dynamics(response.text) #得到響應之后用dynamics()對其進行處理
response = self.session.get(self.logined_url, headers=self.headers) #請求個人詳情頁
if response.status_code == 200: self.profile(response.text) #用profile處理個人詳情頁信息
def dynamics(self, html): selector = etree.HTML(html) dynamics = selector.xpath('//div[contains(@class, "news")]//div[contains(@class, "alert")]') for item in dynamics: dynamic = ' '.join(item.xpath('.//div[@class="title"]//text()')).strip() print(dynamic) def profile(self, html): selector = etree.HTML(html) name = selector.xpath('//input[@id="user_profile_name"]/@value')[0] email = selector.xpath('//select[@id="user_profile_email"]/option[@value!=""]/text()') print(name, email)
這樣,整個類就編寫完成了。
4. 運行
if __name__ == "__main__": login = Login() login.login(email='jujudeduxingzhe@163.com', password='password')
大家可參照他人文章:http://www.pianshen.com/article/226443350/
2.Cookies 池的搭建
參考來源:https://blog.csdn.net/xmxt668/article/details/92368537
大多數情況下,即使我們沒有登錄頁面,我們也能訪問網站的部分頁面或者一些請求,因為網站本身要做SEO,不會對所有頁面設置登錄權限。
百度百科:SEO(Search Engine Optimization):搜索引擎優化,是一種方式:利用搜索引擎的規則提高網站在有關搜索引擎內的自然排名。目的是讓其在行業內占據領先地位,獲得品牌收益。很大程度上是網站經營者的一種商業行為,將自己或自己公司的排名前移。
但是不登錄網站直接爬取有兩個弊端:
- 只能獲取部分內容;
- 訪問容易被限制或者IP被封。
如果需要做大規模抓取,我們就需要擁有很多賬號,每次請求隨機選取一個賬號,這樣就降低了單個賬號的訪問頻率,被封的概率又會大大降低。
維護多個賬號的登錄信息,這時就需要用到Cookies池了。
2.1 本節內容:
實現一個Cookies池的搭建過程:
- Cookies池中保存了許多新浪微博賬號和登錄后的Cookies信息,並且Cookies池還需要定時檢測每個Cookies的有效性,如果某Cookies無效,就刪除該Cookies並模擬登錄生成新的Cookies。
- Cookies池還需要一個非常重要的接口,即獲取隨機Cookies的接口,Cookies運行后,我們只需請求該接口,即可隨機獲得一個Cookies並用其爬取。
2.2 工作准備
- 需要一些微博賬號;
- Redis數據庫
- RedisPy 庫、requests、Selelnium、Flask庫;
- 安裝Chrome瀏覽器並配置好ChromeDriver
2.3 Cookies 池架構
cookies池架構和代理池類似,也包括四個模塊:
- 存儲模塊負責存儲每個賬號的用戶名密碼以及每個賬號對應的Cookies信息,同時還需要提供一些方法來實現方便的存取操作。
- 生成模塊負責生成新的Cookies。此模塊會從存儲模塊逐個拿取賬號的用戶名和密碼,然后模擬登錄目標頁面,判斷登錄成功,就將Cookies返回並交給存儲模塊存儲。
- 檢測模塊需要定時檢測數據庫中的Cookies。在這里我們需要設置一個檢測鏈接,不同的站點檢測鏈接不同,檢測模塊會逐個拿取賬號對應的Cookies去請求鏈接,如果返回的 狀態是有效的,那么此Cookies沒有失效,否則Cookies失效並移除。接下來等待生成模塊重新生成即可。
- 接口模塊需要用API來提供對外服務的接口。由於可用的Cookies可能有多個,我們可以隨機返回Cookies的接口,這樣保證每個Cookies都有可能被取到。Cookies越多,每個Cookies被取到的概率就會越小,從而減少被封號的風險。
2.4 Cookies 的實現
- 存儲模塊
存儲的內容無非就是賬號信息和Cookies信息。賬號由用戶名和密碼兩部分組成,我們可以存成用戶名和密碼的映射。Cookies可以存成JSON字符串,但是我們后面得需要根據賬號來生成Cookies。生成的時候我們需要知道哪些賬號已經生成了Cookies,哪些沒有生成,所以需要同時保存該Cookies對應的用戶名信息,其實也是用戶名和Cookies的映射。這里就是兩組映射,我們自然而然想到Redis的Hash,於是就建立兩個Hash。
前幾節,已經實現了Redis的安裝以及可視化管理工具的安裝,見:https://www.cnblogs.com/bltstop/p/11686568.html
接下來我們用Redis 可視化管理工具來實現Hash的創建,可參考百度經驗(https://jingyan.baidu.com/article/ae97a646ff37f3bbfd461d01.html)
打開RedisDesktopManager,並連接到redis服務器。選擇其中一個db數據庫,選擇"Add new key"新建一個hash數據:
新建對話創建,注意選擇"hash"類型,value分成兩部分,上面是hash的key,下面填的是hash的value;
添加完成之后,如果沒有立即顯示出來,點擊刷新重新加載數據,選擇新建的key,右側頁面則可以展示詳細的信息;
點擊Add row,可以添加一個元素,填寫key和value:
同理,創建Hash:cookies:weibo:
接下來創建一個存儲模塊類,用以提供一些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__()方法有兩個關鍵參數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。
- 生成模塊
生成模塊負責獲取各個賬號信息並模擬登錄,隨后生成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失效的問題,例如時間太長導致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里移除即可。
- 接口模塊
生成模塊和檢測模塊如果定時運行就可以完成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。
- 調度模塊
最后,我們再加一個調度模塊讓這幾個模塊配合運行起來,主要的工作就是驅動幾個模塊定時運行,同時各個模塊需要在不同進程上運行,實現如下所示:
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 用戶名 14747219309Cookies有效 14747219309正在測試Cookies 用戶名 14740626332Cookies有效 14740626332正在測試Cookies 用戶名 14740691419Cookies有效 14740691419正在測試Cookies 用戶名 14740618009Cookies有效 14740618009正在測試Cookies 用戶名 14740636046Cookies有效 14740636046正在測試Cookies 用戶名 14747222472Cookies有效 14747222472Cookies檢測完成驗證碼位置 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
申明:本文內容部分轉自:https://blog.csdn.net/xmxt668/article/details/92368537