2019年最新 Python 模擬登入知乎 支援驗證碼

戴德滿發表於2019-02-28

2019年最新 Python 模擬登入知乎 支援驗證碼

2019年最新 Python 模擬登入知乎 支援驗證碼和儲存 Cookies

知乎的登入頁面已經改版多次,加強了身份驗證,網路上大部分模擬登入均已失效,所以我重寫了一份完整的,並實現了提交驗證碼 (包括中文驗證碼),本文我對分析過程和程式碼進行步驟分解,完整的程式碼請見末尾 Github 倉庫,不過還是建議看一遍正文,因為程式碼早晚會失效,解析思路才是永恆。

分析 POST 請求

首先開啟控制檯正常登入一次,可以很快找到登入的 API 介面,這個就是模擬登入 POST 的連結。

2019年最新 Python 模擬登入知乎 支援驗證碼

我們的最終目標是構建 POST 請求所需的 Headers 和 Form-Data 這兩個物件即可。

構建 Headers

繼續看Requests Headers資訊,和登入頁面的 GET 請求對比發現,這個 POST 的頭部多了三個身份驗證欄位,經測試x-xsrftoken是必需的。 x-xsrftoken則是防 Xsrf 跨站的 Token 認證,訪問首頁時從Response HeadersSet-Cookie欄位中可以找到。

2019年最新 Python 模擬登入知乎 支援驗證碼

構建 Form-Data

Form部分目前已經是加密的,無法再直觀看到,可以通過在 JS 裡打斷點的方式(具體這裡不再贅述,如不會打斷點請自行搜尋)。

2019年最新 Python 模擬登入知乎 支援驗證碼

2019年最新 Python 模擬登入知乎 支援驗證碼

然後我們逐個構建上圖這些引數:
timestamp 時間戳,這個很好解決,區別是這裡是13位整數,Python 生成的整數部分只有10位,需要額外乘以1000

timestamp = str(int(time.time()*1000))
複製程式碼

signature 通過 Ctrl+Shift+F 搜尋找到是在一個 JS 裡生成的,是通過 Hmac 演算法對幾個固定值和時間戳進行加密,那麼只需要在 Python 裡也模擬一次這個加密即可。

2019年最新 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 編碼。

2019年最新 Python 模擬登入知乎 支援驗證碼

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 兩個值更新進去,其他欄位都保持固定值即可。

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

但是現在知乎必須先將 Form-Data 加密才能進行 POST 傳遞,所以我們還要解決加密問題,可由於我們看到的 JS 是混淆後的程式碼,想窺視其中的加密實現方式是一件很費精力的事情。
所以這裡我採用了 sergiojune 這位知友通過 pyexecjs 呼叫 JS 進行加密的方式,只需要把混淆程式碼完整複製過來,稍作修改即可。
具體可看他的原文:zhuanlan.zhihu.com/p/57375111

with open('./encrypt.js') as f:
    js = execjs.compile(f.read())
    return js.call('Q', urlencode(form_data))
複製程式碼

這裡也感謝他分享了一些坑,不然確實不好解決。

儲存 Cookies

最後實現一個檢查登入狀態的方法,如果訪問登入頁面出現跳轉,說明已經登入成功,這時將 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
複製程式碼

完整程式碼

請關注微信公眾號:面向人生程式設計
回覆關鍵詞 “知乎” 獲取程式碼

2019年最新 Python 模擬登入知乎 支援驗證碼
程式設計思維不應只存留在程式碼之中,更應伴隨於整個人生旅途,所以公眾號裡不只聊技術,還會聊產品/網際網路/經濟學等廣泛話題,所以也歡迎非程式設計師關注。

相關文章