美文网首页
PRO-012:知乎登录并下载主页主题

PRO-012:知乎登录并下载主页主题

作者: 杨强AT南京 | 来源:发表于2019-03-03 02:43 被阅读43次

    本主题主要通过知乎的登录,来讲述反爬虫的破解技术,在反爬虫技术中最难点的还是是签名与登录加密,因为这两个都牵涉加密相关技术,更加重要的还需要从网站的茫茫Javascript脚本中找到加密的代码,这些代码千丝万缕,要剥离出来也是难事。万事都有规律,只要从一个网站的登录破解开始,很多网站的破解道理都一样,从浏览器的角度说,只要浏览器能访问的,爬虫就能访问,就看技术成本是多少。要掌握本文的技术,需要掌握如下技术:

    1. 浏览器的Javascript调试技术(本主题使用的Safari浏览器,其他浏览器大同小异);
    2. 简单的加密解密技术:BASE64,SHA1;
    3. 掌握Python调用Javascript(PyExecJS\ [ 支持Node\ ]或者js2py [ 支持 纯Javascript Core - ES6 ] )
    4. 掌握HTTP请求Python模块:requests;
    5. HTTP 协议与Web App与Web服务器的常识;
    6. 本例子使用Qt实现了一个界面;

    说明:本例子只实现校验,登录与主页主题爬取;其他的功能可以根据需要开发。截图如下:


    例子效果

    一、知乎的加密脚本调试过程:

    • 这个脚本来自:浏览器调试的结果,从HXR的url开始定位调试,可以跟踪到签名的生成,提交表单数据的加密。下面是调试的几个截图

    1.1. 从登录的提交之前的行为开始:

    从登录按钮开始

    1.2. 找到按钮事件调用的函数名:

    注意那个name后面的o,就是事件函数

    1.3. 从按钮的click事件绑定的o函数跟踪

    • 在这儿可以设置断点。
    调试的第一个断点,慢慢执行,总有惊喜

    1.4. 跟踪到签名代码


    签名生成函数

    1.5. 跟踪到加密函数


    加密函数调用

    1.6. 加密函数的生成

    Q函数就是加密函数的封装,并exorts为default

    1.7. 加密过程


    image.png

    1.8. 其他

    • 过程跟踪很痛苦,大家自己调试,才是最好的理解之路。

    二、脚本

    • 脚本文件名:p05_custom_atob.js
    window = {
        'encodeURIComponent':encodeURIComponent
    };
    navigator = {
        'userAgent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36'
    }
    function atob(e){
        return new Buffer(e,'base64').toString('binary');
    }
    
    function s(e) {
        return (s = "function" == typeof Symbol && "symbol" == typeof Symbol.t ? function(e) {
            return typeof e
        } : function(e) {
            return e && "function" == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype ? "symbol" : typeof e
        })(e)
    }
    
    // ...... 这里省略若干代码,自己可以拷贝保存。
    
    var Q = function(e) {
        return __g._encrypt(e)
    };
    
    
    • 上面代码中:window,navigator,atob是自己加的,不是来自知乎脚本。加这window,navigator是因为脚本需要BOM对象,atob是浏览器中window对象中的base64加密与解密函数(我们利用Node的Buffer来实现)。

    三、加密函数的实现

      # encoding = utf-8
      import execjs
    
      with open('p05_custom_atob.js') as fd:
          js_Q = fd.read()
    
      # 编译没有问题
      ctx = execjs.compile(js_Q)
    
      # 调用
      re = ctx.call('Q','abcsdefgh')
      print(re)
    

    四、签名函数的实现过程

    # encoding = utf-8
    import execjs
    import hmac
    from hashlib import sha1
    import time
    
    
    def get_signature(now_):
        # 签名由clientId,grantType,source,timestamp四个参数生成
        h = hmac.new(
            key='d1b964811afb40118a12068ff74a12f4'.encode('utf-8'),
            digestmod=sha1)
        grant_type = 'password'
        client_id = 'c3cef7c66a1843f8b3a9e6a1e3160e20'
        source = 'com.zhihu.web'
        now = now_
        h.update((grant_type + client_id + source + now).encode('utf-8'))
        return h.hexdigest()
    
    
    timestamp = str(int(time.time()*1000))
    signature = get_signature(timestamp)
    print(signature)
    
    

    五、最终知乎登录的实现

    • 利用上面两个函数
    # coding = utf-8
    import requests
    import json
    import base64
    from PyQt5.QtWidgets import *
    from PyQt5.QtCore import *
    from PyQt5.QtGui import *
    import itchat
    import sys
    from login import Ui_dlg_login
    import hmac
    from hashlib import sha1
    import time
    import execjs
    import bs4
    
    class ZhihuCaptcha(QThread):
        # 校验码信号(发送校验码图像)
        sign_need_captcha = pyqtSignal(str, bytes)
    
        # 校验码服务url,后面的lang表示用户是英文用户,校验码会返回英文数字校验
        url_captcha='https://www.zhihu.com/api/v3/oauth/captcha?lang=en'
        headers = {
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15'
        }
    
        def __init__(self):
            super(ZhihuCaptcha,self).__init__()
            # 发起校验码查询请求- 第一次请求(是否需要校验码)
            self.session = requests.Session()
            # 设置HTTP请求头:User-Agent
            self.session.headers = self.headers
    
        def run(self):
            response_1st = self.session.request(
                method='get',
                url=self.url_captcha)
            # 返回的数据大致这个格式,可以使用抓包工具与浏览器分析处这个数据格式
            # {
            #     "show_captcha": false
            # }
            is_captcha = json.loads(response_1st.content.decode())['show_captcha']
            # 如果需要检验码,则需要下载校验码,并校验
            if is_captcha:
                print('这次登录需要校验码!')
                # 下载校验码(使用的是put请求方法,可以从浏览器或者抓包工具分析出来)
                # 第二次请求:下载校验码(png格式的图像,数据样例见下面注释:图像内容是压缩的字节序列)
                # {
                #     "img_base64": "......"
                # }
                response_2nd = self.session.put(self.url_captcha)
                # 得到Base_64加密的图像
                img_base64 = json.loads(response_2nd.content.decode())['img_base64']
                # 解密图像
                img_bytes = base64.b64decode(img_base64)
                # 保存或者显示图像
                self.sign_need_captcha.emit('captcha', img_bytes)
                # 校验校验码
    
            else:
                # 不需要校验码,不需要任何,处理,下面直接使用用户信息登录
                print('这次登录,不需要校验码校验!')
                self.sign_need_captcha.emit('no captcha', b'')
            print('校验码线程结束')
    
        # 第三次请求:校验校验码
        def login_rquest(self, captcha_code, username, password):
            #
            data = {
                'input_text': captcha_code
            }
            print(data)
            res_captcha = self.session.post(self.url_captcha, data)
            print(res_captcha.content.decode())
            # 201 响应码表示已经接收请求,还需要继续处理
            # 请求已经被实现,而且有一个新的资源已经依据请求的需要而建立,
            # 且其 URI 已经随Location 头信息返回。
            # 假如需要的资源无法及时建立的话,应当返回 '202 Accepted'。
            if res_captcha.status_code == 201:
                captcha_ok = json.loads(res_captcha.content.decode())['success']
                if not captcha_ok:
                    print("校验码失败")
                    return 'captcha_err'
                else:
                    print("校验码通过")
                    # 开始登录
                    timestamp = str(int(time.time() * 1000))
                    signature = self.get_signature(timestamp)
                    encrypt = self.get_encrypt(username, password, timestamp, signature, captcha_code)
                    print(encrypt)
                    data = encrypt
                    login_headers = {
                        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.1 Safari/605.1.15',
                        'Content-Type': 'application/x-www-form-urlencoded',
                        'x-zse-83': '3_1.1'}
                    url_login = 'https://www.zhihu.com/api/v3/oauth/sign_in'
                    res_login = self.session.post(url_login, data=data, headers=login_headers)
                    print(res_login.status_code, res_login.content.decode())
                    if 200 <= res_login.status_code < 300:
                        print('登录成功!')
                        # 爬取主页内容
                        url_home = 'https://www.zhihu.com'
                        res_home = self.session.post(url_home)
                        content_home = res_home.content.decode('utf-8')
                        # 解析
                        bs_content = bs4.BeautifulSoup(content_home, 'html.parser')
                        list_topics = bs_content.find_all('div',attrs={
                            "class": "ContentItem AnswerItem",
                        })
                        print()
                        print(len(list_topics))
                        if len(list_topics)>0:
                            # 第一条
                            topic = list_topics[0].get('data-zop')
                            json_topic = json.loads(topic)
                            print(json_topic['title'])
                        return json_topic['title']
    
        # 签名函数
        def get_signature(self, now_):
            # 签名由clientId,grantType,source,timestamp四个参数生成
            h = hmac.new(
                key='d1b964811afb40118a12068ff74a12f4'.encode('utf-8'),
                digestmod=sha1)
            grant_type = 'password'
            client_id = 'c3cef7c66a1843f8b3a9e6a1e3160e20'
            source = 'com.zhihu.web'
            now = now_
            h.update((grant_type + client_id + source + now).encode('utf-8'))
            return h.hexdigest()
    
        # 提交信息加密函数
        def get_encrypt(self, username, password, timestamp, signature, captcha):
            str_login = ""
            str_login += "client_id=c3cef7c66a1843f8b3a9e6a1e3160e20&"
            str_login += "grant_type=password&"
            str_login += F"timestamp={timestamp}&"
            str_login += "source=com.zhihu.web"
            str_login += F"&signature={signature}&"
            str_login += F"username={username}&"
            str_login += F"password={password}&"
            str_login += F"captcha={captcha}&"
            str_login += "lang=en&"
            str_login += "ref_source=homepage&"
            str_login += "utm_source="
    
            with open('p05_custom_atob.js', 'r') as fd:
                js_zhihu = fd.read()
    
            ctx = execjs.compile(js_zhihu)
            encrypt_ = ctx.call('Q', str_login)
            return encrypt_
    
    
    # 校验码窗体
    class LoginDialog(QDialog):
    
        # 初始化窗体
        def __init__(self, parent=None):
            super().__init__()
            self.th = ZhihuCaptcha()
    
            self.ui = Ui_dlg_login()
            self.setGeometry(100, 100, 400, 300)
            self.setWindowTitle('用户登录')
            self.ui.setupUi(self)
    
            self.ui.edt_username.setText('你的电话')
            self.ui.edt_password.setText('密码')
    
            # 处理信号
            self.th.sign_need_captcha.connect(self.query_captcha)
    
            self.th.start()
            self.show()
    
        def query_captcha(self, status, img_bytes):
            if status == 'no captcha':
                # 显示校验码提示
                self.ui.lbl_captcha.setText('不需要验证码')
                self.ui.lbl_captcha.setVisible(True)
            if status == 'captcha':
                # 显示校验码
                img_captcha = QImage.fromData(img_bytes)
                pix_captcha = QPixmap.fromImage(img_captcha)
                self.ui.lbl_captcha.setPixmap(pix_captcha)
                self.ui.lbl_captcha.setScaledContents(True)
                self.ui.lbl_captcha.setVisible(True)
                self.ui.edt_captcha.setVisible(True)
    
        def login(self):
            # 调用登录函数(登录失败会返回信号)
            re_login = self.th.login_rquest(
                self.ui.edt_captcha.text(),
                self.ui.edt_username.text(),
                self.ui.edt_password.text())
            self.setWindowTitle(re_login)
    
    
    app = QApplication(sys.argv)
    
    ui_login = LoginDialog()
    
    sys.exit(app.exec())
    
    

    附录:

    相关文章

      网友评论

          本文标题:PRO-012:知乎登录并下载主页主题

          本文链接:https://www.haomeiwen.com/subject/hmvuuqtx.html