第26讲:模拟登录爬取实战案例
在上一課時我們了解了網站登錄驗證和模擬登錄的基本原理。網站登錄驗證主要有兩種實現,一種是基于 Session + Cookies 的登錄驗證,另一種是基于 JWT 的登錄驗證,那么本課時我們就通過兩個實例來分別講解這兩種登錄驗證的分析和模擬登錄流程。
準備工作
在本課時開始之前,請你確保已經做好了如下準備工作:
- 安裝好了 Python (最好 3.6 及以上版本)并能成功運行 Python 程序;
- 安裝好了 requests 請求庫并學會了其基本用法;
- 安裝好了 Selenium 庫并學會了其基本用法。
下面我們就以兩個案例為例來分別講解模擬登錄的實現。
案例介紹
這里有兩個需要登錄才能抓取的網站,鏈接為 https://login2.scrape.cuiqingcai.com/ 和 https://login3.scrape.cuiqingcai.com/,前者是基于 Session + Cookies 認證的網站,后者是基于 JWT 認證的網站。
首先看下第一個網站,打開后會看到如圖所示的頁面。
它直接跳轉到了登錄頁面,這里用戶名和密碼都是 admin,我們輸入之后登錄。
登錄成功之后,我們便看到了熟悉的電影網站的展示頁面,如圖所示。
這個網站是基于傳統的 MVC 模式開發的,因此也比較適合 Session + Cookies 的認證。
第二個網站打開后同樣會跳到登錄頁面,如圖所示。
用戶名和密碼是一樣的,都輸入 admin 即可登錄。
登錄之后會跳轉到首頁,展示了一些書籍信息,如圖所示。
這個頁面是前后端分離式的頁面,數據的加載都是通過 Ajax 請求后端 API 接口獲取,登錄的校驗是基于 JWT 的,同時后端每個 API 都會校驗 JWT 是否是有效的,如果無效則不會返回數據。
案例一
接下來我們就分析這兩個案例并實現模擬登錄吧。
對于案例一,我們如果要模擬登錄,就需要先分析下登錄過程究竟發生了什么,首先我們打開 https://login2.scrape.cuiqingcai.com/,然后執行登錄操作,查看其登錄過程中發生的請求,如圖所示。
這里我們可以看到其登錄的瞬間是發起了一個 POST 請求,目標 URL 為 https://login2.scrape.cuiqingcai.com/login,通過表單提交的方式提交了登錄數據,包括 username 和 password 兩個字段,返回的狀態碼是 302,Response Headers 的 location 字段是根頁面,同時 Response Headers 還包含了 set-cookie 信息,設置了 Session ID。
由此我們可以發現,要實現模擬登錄,我們只需要模擬這個請求就好了,登錄完成之后獲取 Response 設置的 Cookies,將 Cookies 保存好,以后后續的請求帶上 Cookies 就可以正常訪問了。
好,那么我們接下來用代碼實現一下吧。
requests 默認情況下每次請求都是獨立互不干擾的,比如我們第一次先調用了 post 方法模擬登錄,然后緊接著再調用 get 方法請求下主頁面,其實這是兩個完全獨立的請求,第一次請求獲取的 Cookies 并不能傳給第二次請求,因此說,常規的順序調用是不能起到模擬登錄的效果的。
我們先來看一個無效的代碼:
import requests from urllib.parse import urljoinBASE_URL = 'https://login2.scrape.cuiqingcai.com/' LOGIN_URL = urljoin(BASE_URL, '/login') INDEX_URL = urljoin(BASE_URL, '/page/1') USERNAME = 'admin' PASSWORD = 'admin'response_login = requests.post(LOGIN_URL, data={'username': USERNAME,'password': PASSWORD })response_index = requests.get(INDEX_URL) print('Response Status', response_index.status_code) print('Response URL', response_index.url)這里我們先定義了幾個基本的 URL 和用戶名、密碼,接下來分別用 requests 請求了登錄的 URL 進行模擬登錄,然后緊接著請求了首頁來獲取頁面內容,但是能正常獲取數據嗎?
由于 requests 可以自動處理重定向,我們最后把 Response 的 URL 打印出來,如果它的結果是 INDEX_URL,那么就證明模擬登錄成功并成功爬取到了首頁的內容。如果它跳回到了登錄頁面,那就說明模擬登錄失敗。
我們通過結果來驗證一下,運行結果如下:
Response Status 200 Response URL https://login2.scrape.cuiqingcai.com/login?next=/page/1這里可以看到,其最終的頁面 URL 是登錄頁面的 URL,另外這里也可以通過 response 的 text 屬性來驗證頁面源碼,其源碼內容就是登錄頁面的源碼內容,由于內容較多,這里就不再輸出比對了。
總之,這個現象說明我們并沒有成功完成模擬登錄,這是因為 requests 直接調用 post、get 等方法,每次請求都是一個獨立的請求,都相當于是新開了一個瀏覽器打開這些鏈接,這兩次請求對應的 Session 并不是同一個,因此這里我們模擬了第一個 Session 登錄,而這并不能影響第二個 Session 的狀態,因此模擬登錄也就無效了。
那么怎樣才能實現正確的模擬登錄呢?
我們知道 Cookies 里面是保存了 Session ID 信息的,剛才也觀察到了登錄成功后 Response Headers 里面是有 set-cookie 字段,實際上這就是讓瀏覽器生成了 Cookies。
Cookies 里面包含了 Session ID 的信息,所以只要后續的請求攜帶這些 Cookies,服務器便能通過 Cookies 里的 Session ID 信息找到對應的 Session,因此服務端對于這兩次請求就會使用同一個 Session 了。而因為第一次我們已經完成了模擬登錄,所以第一次模擬登錄成功后,Session 里面就記錄了用戶的登錄信息,第二次訪問的時候,由于是同一個 Session,服務器就能知道用戶當前是登錄狀態,就可以返回正確的結果而不再是跳轉到登錄頁面了。
所以,這里的關鍵就在于兩次請求的 Cookies 的傳遞。所以這里我們可以把第一次模擬登錄后的 Cookies 保存下來,在第二次請求的時候加上這個 Cookies 就好了,所以代碼可以改寫如下:
import requests from urllib.parse import urljoinBASE_URL = 'https://login2.scrape.cuiqingcai.com/' LOGIN_URL = urljoin(BASE_URL, '/login') INDEX_URL = urljoin(BASE_URL, '/page/1') USERNAME = 'admin' PASSWORD = 'admin'response_login = requests.post(LOGIN_URL, data={'username': USERNAME,'password': PASSWORD }, allow_redirects=False)cookies = response_login.cookies print('Cookies', cookies)response_index = requests.get(INDEX_URL, cookies=cookies) print('Response Status', response_index.status_code) print('Response URL', response_index.url)由于 requests 可以自動處理重定向,所以模擬登錄的過程我們要加上 allow_redirects 參數并設置為 False,使其不自動處理重定向,這里登錄之后返回的 Response 我們賦值為 response_login,這樣通過調用 response_login 的 cookies 就可以獲取到網站的 Cookies 信息了,這里 requests 自動幫我們解析了 Response Headers 的 set-cookie 字段并設置了 Cookies,所以我們不需要手動解析 Response Headers 的內容了,直接使用 response_login 對象的 cookies 屬性即可獲取 Cookies。
好,接下來我們再次用 requests 的 get 方法來請求網站的 INDEX_URL,不過這里和之前不同,get 方法多加了一個參數 cookies,這就是第一次模擬登錄完之后獲取的 Cookies,這樣第二次請求就能攜帶第一次模擬登錄獲取的 Cookies 信息了,此時網站會根據 Cookies 里面的 Session ID 信息查找到同一個 Session,校驗其已經是登錄狀態,然后返回正確的結果。
這里我們還是輸出了最終的 URL,如果其是 INDEX_URL,那就代表模擬登錄成功并獲取到了有效數據,否則就代表模擬登錄失敗。
我們看下運行結果:
Cookies <RequestsCookieJar[<Cookie sessionid=psnu8ij69f0ltecd5wasccyzc6ud41tc for login2.scrape.cuiqingcai.com/>]> Response Status 200 Response URL https://login2.scrape.cuiqingcai.com/page/1這下就沒有問題了,這次我們發現其 URL 就是 INDEX_URL,模擬登錄成功了!同時還可以進一步輸出 response_index 的 text 屬性看下是否獲取成功。
接下來后續的爬取用同樣的方式爬取即可。
但是我們發現其實這種實現方式比較煩瑣,每次還需要處理 Cookies 并進行一次傳遞,有沒有更簡便的方法呢?
有的,我們可以直接借助于 requests 內置的 Session 對象來幫我們自動處理 Cookies,使用了 Session 對象之后,requests 會將每次請求后需要設置的 Cookies 自動保存好,并在下次請求時自動攜帶上去,就相當于幫我們維持了一個 Session 對象,這樣就更方便了。
所以,剛才的代碼可以簡化如下:
import requests from urllib.parse import urljoinBASE_URL = 'https://login2.scrape.cuiqingcai.com/' LOGIN_URL = urljoin(BASE_URL, '/login') INDEX_URL = urljoin(BASE_URL, '/page/1') USERNAME = 'admin' PASSWORD = 'admin'session = requests.Session()response_login = session.post(LOGIN_URL, data={'username': USERNAME,'password': PASSWORD })cookies = session.cookies print('Cookies', cookies)response_index = session.get(INDEX_URL) print('Response Status', response_index.status_code) print('Response URL', response_index.url)可以看到,這里我們無需再關心 Cookies 的處理和傳遞問題,我們聲明了一個 Session 對象,然后每次調用請求的時候都直接使用 Session 對象的 post 或 get 方法就好了。
運行效果是完全一樣的,結果如下:
Cookies <RequestsCookieJar[<Cookie sessionid=ssngkl4i7en9vm73bb36hxif05k10k13 for login2.scrape.cuiqingcai.com/>]>Response Status 200Response URL https://login2.scrape.cuiqingcai.com/page/1因此,為了簡化寫法,這里建議直接使用 Session 對象來進行請求,這樣我們就無需關心 Cookies 的操作了,實現起來會更加方便。
這個案例整體來說比較簡單,但是如果碰上復雜一點的網站,如帶有驗證碼,帶有加密參數等等,直接用 requests 并不好處理模擬登錄,如果登錄不了,那豈不是整個頁面都沒法爬了嗎?那么有沒有其他的方式來解決這個問題呢?當然是有的,比如說,我們可以使用 Selenium 來通過模擬瀏覽器的方式實現模擬登錄,然后獲取模擬登錄成功后的 Cookies,再把獲取的 Cookies 交由 requests 等來爬取就好了。
這里我們還是以剛才的頁面為例,我們可以把模擬登錄這塊交由 Selenium 來實現,后續的爬取交由
requests 來實現,代碼實現如下:
這里我們使用 Selenium 先打開了 Chrome 瀏覽器,然后跳轉到了登錄頁面,隨后模擬輸入了用戶名和密碼,接著點擊了登錄按鈕,這時候我們可以發現瀏覽器里面就提示登錄成功,然后成功跳轉到了主頁面。
這時候,我們通過調用 get_cookies 方法便能獲取到當前瀏覽器所有的 Cookies,這就是模擬登錄成功之后的 Cookies,用這些 Cookies 我們就能訪問其他的數據了。
接下來,我們聲明了 requests 的 Session 對象,然后遍歷了剛才的 Cookies 并設置到 Session 對象的 cookies 上面去,接著再拿著這個 Session 對象去請求 INDEX_URL,也就能夠獲取到對應的信息而不會跳轉到登錄頁面了。
運行結果如下:
Cookies [{'domain': 'login2.scrape.cuiqingcai.com', 'expiry': 1589043753.553155, 'httpOnly': True, 'name': 'sessionid', 'path': '/', 'sameSite': 'Lax', 'secure': False, 'value': 'rdag7ttjqhvazavpxjz31y0tmze81zur'}]Response Status 200Response URL https://login2.scrape.cuiqingcai.com/page/1可以看到這里的模擬登錄和后續的爬取也成功了。所以說,如果碰到難以模擬登錄的過程,我們也可以使用 Selenium 或 Pyppeteer 等模擬瀏覽器操作的方式來實現,其目的就是取到登錄后的 Cookies,有了 Cookies 之后,我們再用這些 Cookies 爬取其他頁面就好了。
所以這里我們也可以發現,對于基于 Session + Cookies 驗證的網站,模擬登錄的核心要點就是獲取 Cookies,這個 Cookies 可以被保存下來或傳遞給其他的程序繼續使用。甚至說可以將 Cookies 持久化存儲或傳輸給其他終端來使用。另外,為了提高 Cookies 利用率或降低封號幾率,可以搭建一個 Cookies 池實現 Cookies 的隨機取用。
案例二
對于案例二這種基于 JWT 的網站,其通常都是采用前后端分離式的,前后端的數據傳輸依賴于 Ajax,登錄驗證依賴于 JWT 本身這個 token 的值,如果 JWT 這個 token 是有效的,那么服務器就能返回想要的數據。
下面我們先來在瀏覽器里面操作登錄,觀察下其網絡請求過程,如圖所示。
這里我們發現登錄時其請求的 URL 為 https://login3.scrape.cuiqingcai.com/api/login,是通過 Ajax 請求的,同時其 Request Body 是 JSON 格式的數據,而不是 Form Data,返回狀態碼為 200。
然后再看下返回結果,如圖所示。
可以看到返回結果是一個 JSON 格式的數據,包含一個 token 字段,其結果為:
這就是我們上一課時所講的 JWT 的內容,格式是三段式的,通過“.”來分隔。
那么有了這個 JWT 之后,后續的數據怎么獲取呢?下面我們再來觀察下后續的請求內容,如圖所示。
這里我們可以發現,后續獲取數據的 Ajax 請求中的 Request Headers 里面就多了一個 Authorization 字段,其結果為 jwt 然后加上剛才的 JWT 的內容,返回結果就是 JSON 格式的數據。
沒有問題,那模擬登錄的整個思路就簡單了:
模擬請求登錄結果,帶上必要的登錄信息,獲取 JWT 的結果。
后續的請求在 Request Headers 里面加上 Authorization 字段,值就是 JWT 對應的內容。
好,接下來我們用代碼實現如下:
這里我們同樣是定義了登錄接口和獲取數據的接口,分別為 LOGIN_URL 和 INDEX_URL,接著通過 post 請求進行了模擬登錄,這里提交的數據由于是 JSON 格式,所以這里使用 json 參數來傳遞。接著獲取了返回結果中包含的 JWT 的結果。第二步就可以構造 Request Headers,然后設置 Authorization 字段并傳入 JWT 即可,這樣就能成功獲取數據了。
運行結果如下:
Response JSON {'token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTg3ODc4NzkxLCJlbWFpbCI6ImFkbWluQGFkbWluLmNvbSIsIm9yaWdfaWF0IjoxNTg3ODM1NTkxfQ.iUnu3Yhdi_a-Bupb2BLgCTUd5yHL6jgPhkBPorCPvm4'}JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNTg3ODc4NzkxLCJlbWFpbCI6ImFkbWluQGFkbWluLmNvbSIsIm9yaWdfaWF0IjoxNTg3ODM1NTkxfQ.iUnu3Yhdi_a-Bupb2BLgCTUd5yHL6jgPhkBPorCPvm4Response Status 200 Response URL https://login3.scrape.cuiqingcai.com/api/book/?limit=18&offset=0 Response Data {'count': 9200, 'results': [{'id': '27135877', 'name': '校園市場:布局未來消費群,決戰年輕人市場', 'authors': ['單興華', '李燁'], 'cover': 'https://img9.doubanio.com/view/subject/l/public/s29539805.jpg', 'score': '5.5'}, ... {'id': '30289316', 'name': '就算這樣,還是喜歡你,笠原先生', 'authors': ['おまる'], 'cover': 'https://img3.doubanio.com/view/subject/l/public/s29875002.jpg', 'score': '7.5'}]}可以看到,這里成功輸出了 JWT 的內容,同時最終也獲取到了對應的數據,模擬登錄成功!
類似的思路,如果我們遇到 JWT 認證的網站,也可以通過類似的方式來實現模擬登錄。當然可能某些頁面比較復雜,需要具體情況具體分析。
總結
以上我們就通過兩個示例來演示了模擬登錄爬取的過程,以后遇到這種情形的時候就可以用類似的思路解決了。
代碼:https://github.com/Python3WebSpider/ScrapeLogin2、https://github.com/Python3WebSpider/ScrapeLogin3。
總結
以上是生活随笔為你收集整理的第26讲:模拟登录爬取实战案例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第48讲:分布式利器 Scrapy-Re
- 下一篇: 第31讲:抓包利器 Charles 的使