• 2019年最新 Python 模拟登录知乎 支持验证码


    知乎的登录页面已经改版多次,加强了身份验证,网络上大部分模拟登录均已失效,所以我重写了一份完整的,并实现了提交验证码 (包括中文验证码),本文我对分析过程和代码进行步骤分解,完整的代码请见末尾 Github 仓库,不过还是建议看一遍正文,因为代码早晚会失效,解析思路才是永恒。

    分析 POST 请求

    首先打开控制台正常登录一次,可以很快找到登录的 API 接口,这个就是模拟登录 POST 的链接。

    我们的最终目标是构建 POST 请求所需的 Headers 和 Form-Data 这两个对象即可。

    构建 Headers

    继续看Requests Headers信息,和登录页面的 GET 请求对比发现,这个 POST 的头部多了三个身份验证字段,经测试x-xsrftoken是必需的。
    x-xsrftoken则是防 Xsrf 跨站的 Token 认证,访问首页时从Response HeadersSet-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'
    ', '')
        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 进行加密的方式,只需要把混淆代码完整复制过来,稍作修改即可。
    具体可看他的原文: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

    最后实现一个检查登录状态的方法,如果访问登录页面出现跳转,说明已经登录成功,这时将 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
    

    完整代码

    请关注微信公众号:面向人生编程
    回复【知乎】获取本文代码
    回复【资料】获取本人精选的学习资料

  • 相关阅读:
    多维梯度下降
    梯度下降
    三种评价函数
    Gluon sgd
    Gluon.vision的几类数据集
    Gluon Data API
    Gluon 实现 dropout 丢弃法
    AlexNet 分类 FashionMNIST
    LeNet 分类 FashionMNIST
    LeNet
  • 原文地址:https://www.cnblogs.com/zkqiang/p/10515072.html
Copyright © 2020-2023  润新知