2019 年最新 Python 模拟登录知乎 支持验证码和保存 Cookies
知乎的登录页面已经改版多次, 加强了身份验证, 网络上大部分模拟登录均已失效, 所以我重写了一份完整的, 并实现了提交验证码 (包括中文验证码), 本文我对分析过程和代码进行步骤分解, 完整的代码请见末尾 GitHub 仓库, 不过还是建议看一遍正文, 因为代码早晚会失效, 解析思路才是永恒.
分析 POST 请求
首先打开控制台正常登录一次, 可以很快找到登录的 API 接口, 这个就是模拟登录 POST 的链接.
我们的最终目标是构建 POST 请求所需的 Headers 和 Form-Data 这两个对象即可.
构建 Headers
继续看 Requests Headers 信息, 和登录页面的 GET 请求对比发现, 这个 POST 的头部多了三个身份验证字段, 经测试 x-xsrftoken 是必需的. x-xsrftoken 则是防 Xsrf 跨站的 Token 认证, 访问首页时从 Response Headers 的 Set-Cookie 字段中可以找到.
构建 Form-Data
Form 部分目前已经是加密的, 无法再直观看到, 可以通过在 JS 里打断点的方式 (具体这里不再赘述, 如不会打断点请自行搜索).
然后我们逐个构建上图这些参数:
timestamp 时间戳, 这个很好解决, 区别是这里是 13 位整数, Python 生成的整数部分只有 10 位, 需要额外乘以 1000
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 编码.
- 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 进行加密的方式, 只需要把混淆代码完整复制过来, 稍作修改即可.
- with open('./encrypt.js') as f:
- JS = ExecJS.compile(f.read())
- return JS.call('Q', urlencode(form_data))
- 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
来源: https://juejin.im/post/5c77b673e51d4554ff57fc05