前兩天學習了Python的requests模塊的相關內容,對於用GET和PSOT請求訪問網頁以抓取需要的內容有了初步的了解,想要再從一些復雜的網站積累些經驗。最開始我采用最簡單的get(url)方法想要抓取知乎熱搜的標題,想着是個很簡單的任務。但是耗費了我五天的時間才堪堪解決模擬登錄知乎的問題,期間還查閱了十幾個相關網站,解決了一堆問題,還沒有實現抓取熱搜的目的,不過最難的一步解決了,相信之后的提取網頁內容的問題也會解決。
至於為什么學習內容會從“抓取知乎熱搜”變成“模擬登錄知乎”,是因為知乎它比較坑,不登錄的話不顯示熱搜的內容,哪怕你訪問了熱搜的URL——www.zhihu.com/hot,它也給你跳到登錄頁面——www.zhihu.com/signin,所以要想抓取內容,就繞不開登錄知乎。
也許還有其他簡單的模塊selenium,但本次並沒有涉及到,等到以后有機會再去學習了解^_^。
接下來我就按照步驟說明我是如何一步一步的在前人文檔的幫助下模擬登錄知乎的,也許以后寫下的代碼會隨着知乎的更新而失效,但是在此過程中的思想和思考方式卻對類似問題的解決有所幫助。
一、查看請求的Headers
我們首先要清楚登錄請求是發給誰的
①打開知乎登錄頁面,輸入錯誤的賬號和密碼;同時F12打開開發者模式,監視在此過程中發送的各項請求。
②看到一個POST請求sign_in,看一下它的Response
說明沒錯,就是這個Post請求;一般Post請求都會帶一個FormData,檢查后邊,發現這個請求也不例外
但是這個參數不是常見的Key:Value格式,說明做了加密處理,那么我們現在就需要找到它的加密邏輯,然后模擬出這個參數。
二、加密方法
①加密函數
目前已知的就只有這個sign_in,在開發者模式的Source模塊中,通過全局搜索CTRL+SHIFT+F搜索一下sign_in
只有這么一個js文件里邊有sign_in,點進去之后,通過CTRL+F看看sign_in在這個文件中具體在哪。這里需要注意的是,網頁為了節省資源,通常會省去語法格式,而將大量代碼擠到一段中去,比如這樣:
這樣我們的開發就顯得十分不便,好在我們可以通過左下方的一個按鈕來還原它們的格式:
接着正文講,我們在這個js文件中搜索到的sign_in有三個。可以通過調試+設置斷點的方法判斷哪個是我們需要的。這里就不貼調試的過程了。
運行到其中一個斷點處,通過右邊的Scope監視局部變量Local,可以發現這部分的局部變量包含了很多信息,可能以后會用到它。
信息有了相當於加密函數的部分參數就有了,那么這些參數是通過哪個函數被加密從而形成了FormData了呢?這里的加密方法的尋找,我參考了別人的方法:加密一般都是用encrypt之類的名字,所以可以直接搜索encrypt:
把鼠標停留在這個return后邊的值,會自動顯示這個值;將它與FormData相比較,發現一模一樣,說明調用return后邊的代碼,起到了把信息加密的作用。
②加密函數的輸入參數
接上文,如果我們把鼠標放在參數e上,
可以得到加密函數的輸入參數,即下面這些參數用&連接構成的字符串:
client_id=c3cef7c66a1843f8b3a9e6a1e3160e20 grant_type=password timestamp=1603276798679 source=com.zhihu.web signature=d570b4b3cd3b7e473933ed5e9a10f714c383aa81 username%2B8615947657687 password=1111111 captcha=5e9x lang=en utm_source= ref_source=other_https%3A%2F%2Fwww.zhihu.com%2Fsignin%3Fnext%3D%252F
其中只有timestamp、signature、captcha是會變化的,經過分析,發現:
timestamp:13位時間戳——Python自帶的時間戳函數生成的是10位時間戳,乘以1000即可
captcha:動態輸入的驗證碼——驗證碼的獲取會在第三大部分的第②模塊請求驗證碼中介紹,你也可以直接跳過去看。
signature:則是一個經過加密的屬性,它和captcha都要通過額外編寫函數來獲取,不是可以直接得到的。但是獲取方法並不難,下面介紹如何獲取signature:
step1、在網頁源碼中,全局搜索signature,尋找看上去像進行加密的位置
step2、再調試一次,分別在該處和之前的加密函數encrypt處設置斷點,觀察encrypt的輸入參數e的signature的值是否和該處signature的值相同,比較結果如下:
一模一樣,看來這里就是給signature進行加密的地方了。這就有兩個問題了——如何加密,對誰加密。
觀察這部分代碼,可以得到上邊兩個問題的答案:
a、如何加密——HMac加密模式,Hash函數:SHA-1
b、對誰加密——就是面代碼中的a
a的內容呢?
可以看到,一共5項,分別是①“d1b964811afb40118a12068ff74a12f4”;②"password";③clientID;④"com.zhihu.web"⑤timestamp
所以我們在Python中編寫對Signature的加密函數時,就是利用上面提到的五項參數和HMac函數進行的。這部分的代碼如下:
def get_signature(self): #獲取Signature clientID=b'c3cef7c66a1843f8b3a9e6a1e3160e20' SK=b'd1b964811afb40118a12068ff74a12f4' h=hmac.new(SK,digestmod=hashlib.sha1) h.update(clientID) h.update(b'password') h.update(b'com.zhihu.web') h.update(self.timestamp.encode()) return h.hexdigest()
知道了加密函數的位置,我們就可以把參與加密的所有js方法都提取出來,放在一個html文件內執行就可以了。
向上尋找這個return所在函數的頭,把這個函數的內容全部復制到一個JS文件中, 總共400多行。
為了看看這個函數正確與否,我們可以把函數中的內容直接拿出來,就是去掉最外層的function(module,exports,webpack_require),並把exports相關代碼去掉(不去掉exports的話,是無法輸出到html文件中的)。然后調用下面的函數b,把我們的FormData傳進去。
將上邊的JS文件嵌入一個html文件中,放在script標簽內即可。
這里貼出檢驗時的HTML文件和JS文件的寫法:
HTML
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body style="word-break:break-all;"> <h1>獲得參數</h1> <script type="text/javascript" src="T.js"> </script> </body> </html>
T.js
//中間代碼和之前400多行代碼相同,我就不寫了,只是注釋掉所有和exports相關的代碼,注意是 "所有",你可以通過CTRL+F查找與exports相關的代碼 var b = function (e) { return __g._encrypt(encodeURIComponent(e)); };
document.write(b("client_id=c3cef7c66a1843f8b3a9e6a1e3160e20&grant_type=password×tamp=1603252105039&source=com.zhihu.web&signature=1f47545c2389e3356a47837daadef46d63c8530d&username=%2B8615947657687&password=111111&captcha=&lang=en&utm_source=&ref_source=other_https%3A%2F%2Fwww.zhihu.com%2Fsignin%3Fnext%3D%252F"))
用瀏覽器打開,就可以看到加密的字符串也就是發送的FormData了。它與我們之前Request Header中的FormData的值相同(這里由於我是第二天做的,所以和前一天的FormData不同)。
搞完這個后,我們就可以繼續使用Python來操作了,因為加密方法格式化后有400多行,實在太多,也全都是混淆,不太可能用Python一個一個實現,所以這里選擇用Python的execjs來直接執行JavaScript代碼從而獲得FormData,簡單且方便。
不過要注意的是,前邊我們寫的JS文件是用來嵌入HTML並在網頁中打開的,所以給這個JS文件提供的是Web環境。但是想在Python中通過execjs執行的JS是需要Node環境,所以需要對之前的JS代碼稍加修改。
修改一、開頭添加抬頭
const jsdom = require("jsdom"); const { JSDOM } = jsdom; const dom = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`); window = dom.window; document = window.document;
修改二、atob => window.atob
之后運行一下JS檢查一下結果(這里我是在Pycharm中執行的JS文件,環境是NodeJS。在這個JS文件最后一行用console.log(b("client=..."))代替我們之前寫的document.write將FormData輸出到命令行檢查)
可見Node環境下配置正確,運行結果正確。
這就是提取的加密函數並且進行加密的部分。
另外還有個問題我們一直忽略了,那就是加密函數的參數問題,是的,光有加密函數還不行,你得知道它是對哪些參數進行了加密才得到了最后的FormData;換言之,我們現在只是知道了輸入-處理-輸出中的處理和輸出部分,接下來,我們要看看輸入的參數是哪些。
為什么我把輸入放在最后說呢?是因為我們有個取巧的方法幫助我們獲得這些參數,使我可以用較短的篇幅來說明有哪些參數。
①還是回到我們之前的encrypt部分的代碼
三、模擬登錄
①請求頭headers信息
必須要有三個要素:User-Agent、Content-Type、x-zse-83
headers.update({ 'content-type': 'application/x-www-form-urlencoded', 'x-zse-83': '3_2.0', 'x-xsrftoken': self._get_xsrf(), 'User-Agent':'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36' })
不帶Content-Type,會出現錯誤:
Missing argument grant_type
不帶x-zse-83,會出現錯誤:
請求參數異常,請升級客戶端后重試
User-Agent就不用說了
②請求驗證碼
登錄時的請求順序是:
- 請求驗證碼地址,看需不需要填寫驗證碼,如果需要就再請求一次,而且還需要再再請求一次查看驗證碼輸入的正確與否;不正確就重復上述步驟。不需要填寫驗證碼的時候就可以直接請求登錄網址了。
此外,上述三次請求的驗證碼地址的請求方法都是不同的,從前到后分別是GET、PUT和POST,且這三次請求的內容都是通過json編碼,可以在請求之后通過json()方法解碼為dict類型的對象。
cptcha驗證碼,是通過GET請求單獨的API接口返回是否需要驗證碼(無論是否需要,都要請求一次),如果是True則需要再次PUT請求獲取圖片的base64編碼。
知乎驗證碼有兩種形式,一種是點擊倒立文字,另一種是輸入英文字符串。區別在於傳入的參數lang是cn還是en。這里只實現第二種驗證方式。
def getcapture(self): api='https://www.zhihu.com/api/v3/oauth/captcha?lang=en' message=self.session.get(api).json() #第一次請求 if message['show_captcha']=='False': #此時不需要驗證碼 self.picture='' else: print('需要驗證碼:') while True: self.picture_url=self.session.put()api.json()#第二次請求,獲得圖片的base64編碼 with open('captchar.jpg','wb') as f: f.write(base64.b64decode(self.picture_url['img_base64'])) image=Image.open('captcha.jpg') image.show() self.picture=input('請輸入驗證碼:') time.sleep(2) message1=self.session.post(api,data={'input_text':self.picture}) #第三次請求,POST提交驗證碼 if message1.status_code==201: break else: print(f'{message1.status_code} 請提交正確的驗證碼')
四、補充內容
a、驗證驗證碼
驗證驗證碼時的請求頭只需要有User-Agent字段就可以了。
b、請求的所有階段都要帶上Cookie
知乎的Cookie值是驗證碼票據,來源於第一次GET請求驗證碼地址,就是第一次GET請求的Response頭部中的set-cookie要素中的那個。
如果不帶Cookie請求或者請求順序不一樣就有可能返回錯誤:
{"error":{"message":"缺少驗證碼票據","code":120002,"name":"ERR_CAPSION_TICKET_NOT_FOUND"}}
參考(按對我幫助由大到小排列):
代碼來自Github: