知乎的登陸頁面已經改版屢次,增強了身份驗證,網絡上大部分模擬登陸均已失效,因此我重寫了一份完整的,並實現了提交驗證碼 (包括中文驗證碼),本文我對分析過程和代碼進行步驟分解,完整的代碼請見末尾 Github 倉庫,不過仍是建議看一遍正文,由於代碼遲早會失效,解析思路纔是永恆。python
首先打開控制檯正常登陸一次,能夠很快找到登陸的 API 接口,這個就是模擬登陸 POST 的連接。程序員
咱們的最終目標是構建 POST 請求所需的 Headers 和 Form-Data 這兩個對象便可。算法
繼續看Requests Headers
信息,和登陸頁面的 GET 請求對比發現,這個 POST 的頭部多了三個身份驗證字段,經測試x-xsrftoken
是必需的。
x-xsrftoken
則是防 Xsrf 跨站的 Token 認證,訪問首頁時從Response Headers
的Set-Cookie
字段中能夠找到。編程
Form部分目前已是加密的,沒法再直觀看到,能夠經過在 JS 裏打斷點的方式(具體這裏再也不贅述,如不會打斷點請自行搜索)。json
而後咱們逐個構建上圖這些參數:
timestamp
時間戳,這個很好解決,區別是這裏是13位整數,Python 生成的整數部分只有10位,須要額外乘以1000api
timestamp = str(int(time.time()*1000))
signature
經過 Ctrl+Shift+F 搜索找到是在一個 JS 裏生成的,是經過 Hmac 算法對幾個固定值和時間戳進行加密,那麼只須要在 Python 裏也模擬一次這個加密便可。微信
def _get_signature(self, timestamp): ha = hmac.new(b'd1b964811afb40118a12068ff74a12f4', digestmod=hashlib.sha1) grant_type = self.login_data['grant_type'] client_id = self.login_data['client_id'] source = self.login_data['source'] ha.update(bytes((grant_type + client_id + source + timestamp), 'utf-8')) return ha.hexdigest()
captcha
驗證碼,是經過 GET 請求單獨的 API 接口返回是否須要驗證碼(不管是否須要,都要請求一次),若是是 True 則須要再次 PUT 請求獲取圖片的 base64 編碼。cookie
resp = self.session.get(api, headers=headers) show_captcha = re.search(r'true', resp.text) if show_captcha: put_resp = self.session.put(api, headers=headers) json_data = json.loads(put_resp.text) img_base64 = json_data['img_base64'].replace(r'\n', '') with open('./captcha.jpg', 'wb') as f: f.write(base64.b64decode(img_base64)) img = Image.open('./captcha.jpg')
實際上有兩個 API,一個是識別倒立漢字,一個是常見的英文驗證碼,任選其一便可,代碼中我將兩個都實現了,漢字是經過 plt 點擊座標,而後轉爲 JSON 格式。(另外,這裏其實能夠經過從新請求登陸頁面避開驗證碼,若是你須要自動登陸的話能夠改造試試)
最後還有一點要注意,若是有驗證碼,須要將驗證碼的參數先 POST 到驗證碼 API,再隨其餘參數一塊兒 POST 到登陸 API。網絡
if lang == 'cn': import matplotlib.pyplot as plt plt.imshow(img) print('點擊全部倒立的漢字,按回車提交') points = plt.ginput(7) capt = json.dumps({'img_size': [200, 44], 'input_points': [[i[0]/2, i[1]/2] for i in points]}) else: img.show() capt = input('請輸入圖片裏的驗證碼:') # 這裏必須先把參數 POST 驗證碼接口 self.session.post(api, data={'input_text': capt}, headers=headers) return capt
而後把 username 和 password 兩個值更新進去,其餘字段都保持固定值便可。session
self.login_data.update({ 'username': self.username, 'password': self.password, 'lang': captcha_lang }) timestamp = int(time.time()*1000) self.login_data.update({ 'captcha': self._get_captcha(self.login_data['lang']), 'timestamp': timestamp, 'signature': self._get_signature(timestamp) })
可是如今知乎必須先將 Form-Data 加密才能進行 POST 傳遞,因此咱們還要解決加密問題,可因爲咱們看到的 JS 是混淆後的代碼,想窺視其中的加密實現方式是一件很費精力的事情。
因此這裏我採用了 sergiojune 這位知友經過 pyexecjs
調用 JS 進行加密的方式,只須要把混淆代碼完整複製過來,稍做修改便可。
具體可看他的原文:https://zhuanlan.zhihu.com/p/57375111
with open('./encrypt.js') as f: js = execjs.compile(f.read()) return js.call('Q', urlencode(form_data))
這裏也感謝他分享了一些坑,否則確實很差解決。
最後實現一個檢查登陸狀態的方法,若是訪問登陸頁面出現跳轉,說明已經登陸成功,這時將 Cookies 保存起來(這裏 session.cookies 初始化爲 LWPCookieJar 對象,因此有 save 方法),這樣下次登陸能夠直接讀取 Cookies 文件。
def check_login(self): resp = self.session.get(self.login_url, allow_redirects=False) if resp.status_code == 302: self.session.cookies.save() return True return False
請關注微信公衆號:面向人生編程
回覆關鍵詞 「知乎」 獲取代碼
編程思惟不該只存留在代碼之中,更應伴隨於整我的生旅途,因此公衆號裏不僅聊技術,還會聊產品/互聯網/經濟學等普遍話題,因此也歡迎非程序員關注。