• Python 爬虫五 进阶案例-web微信登陆与消息发送


    首先回顾下网页微信登陆的一般流程

      1、打开浏览器输入网址

      2、使用手机微信扫码登陆

      3、进入用户界面

    1、打开浏览器输入网址

    首先打开浏览器输入web微信网址,并进行监控:

    https://wx.qq.com/

    可以发现网页中包含了一个新的url,而这个url就是二维码的来源。

    https://login.weixin.qq.com/qrcode/wbfd1Z-a0g==

    可以猜测一下获取url的一般网址就是https://login.weixin.qq.com/qrcode,而wbfd1Z-a0g==肯定就是通过请求前面的url后传入的参数,最终生成二维码图片。

    此时再监控此次请求的network,去寻找,究竟是什么时候返回给客户端wbfd1Z-a0g==字符串的。

    最终在网址:https://login.wx.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=zh_CN&_=1532592134738的response里面找到了二维码参数。

    这个网址里面_=1532592134738里面传入的看着像一个时间戳。此时我们先新建一个Django项目,在login页面向上面的网址发送请求。取得二维码参数,再传给前台的img标签,就可以做出二维码登陆页面。

    新建一个Django项目、并新建一个app wechat:

    将主url路由指向wechat app里面的url, 并写好一个login url,让用户从我们这边开始登陆微信。

    wechat urls:

    views内的login函数:

    from django.shortcuts import render, HttpResponse, redirect
    import requests, time
    import re, json
    from bs4 import BeautifulSoup
    # Create your views here.
    
    
    def login(req):
        ctime = time.time() * 1000  # 模拟一个相同的时间戳
        base_url = 'https://login.wx.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=zh_CN&_={0}'
        url = base_url.format(ctime)  # 字符串拼接,生成新的url
        response = requests.get(url)  # 向新的url发送get请求
        xcode_list = re.findall('window.QRLogin.uuid = "(.*)";', response.text)  # 通过正则表达式,提取到对于的参数列表['YZzTdz9m_A==', 'YZzTdz9m_A==']
        req.session['xcode'] = xcode_list[0]  # 获取到参数,存入session内
        return render(req, 'wechat/login.html', {'xcode': xcode_list[0]})  # 返回给login页面此参数

    在templates目录里面创建一个wechat目录,在wechat目录创建一个login.html页面,传入参数。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <img id="xcode" style=" 200px; height: 200px; margin:100px 40%" src="https://login.weixin.qq.com/qrcode/{{ xcode }}">
    </body>
    </html>

    因为加入了session功能需要先run一下

    python3 manage.py makemigrations
    python3 mange.py migrate

    运行并访问login:

    此时,就完成第一个二维码页面了。

    第二步,有了二维码,手机扫描是如何进行交互的?是不是这个网页有个后台一直在请求某个网页,一旦扫码验证就进行下一步操作?继续监控请求

    思路渐渐清晰了,在请求登陆的时候大概是如下的流程:

    打开一个失效的url:

    再查看扫描完的URL的返回:

    最后确认登陆:

    再次观察,会发现这个url其实是一个长轮询,一直在请求认证结果。pending的是在等待的当前请求。而在我们的项目内部,因为浏览器存在一个同源策略,所以无法完成在前端直接获取结果。此时改变一下思路:可以通过login内部写个url 偷偷的访问后端的视图,视图层再通过requests模块去发送请求获取结果返回给前端。

    另外关于url的三种返回:

    1、408代表没有人扫码(需要重新发送)
    2、201代表已经有用户扫码,后面的参数返回的是用户头像
    3、200代表用户已经登陆

    定义一下检查的url:

        path('check_login/', views.check_login),

    login.html页面加入javascript

    <script src="/static/jquery.min.js"></script>
        <script>
            tip = 1
            function checkLogin() {
                $.ajax({
                    url:'/wechat/check_login/',
                    data: {'tip': tip},
                    type: 'GET',
                    dataType: 'JSON',
                    success:function (arg) {
                        if (arg.code == 201){
                            // 有人扫码了
                            $('#xcode').attr('src', arg.data);
                            checkLogin();
                            tip = 0;
                        }else if (arg.code == 408){
                            checkLogin();
                        }else if (arg.code == 200){
                            window.location.href='/wechat/index/'
                        }
                    }
                })
            }
            checkLogin();
        </script>

    这边需要提醒的是当用户扫码之后的url发送的参数中有一个tip参数有变动,从1变成0.

    def check_login(req):
        tip = req.GET.get('tip')  # 标记是否扫码
        # 自定义返回的json数据格式
        ret = {
            'code': 408,  # 初始值408代表没有任何操作
            'data': None
        }
        ctime = time.time() * 1000  # 根据得到的url,伪造匹配的时间戳
        base_url = 'https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid={0}&tip={1}&r=903313058&_={2}'
        url1 = base_url.format(req.session['xcode'], tip, ctime)  # 字符串修饰
        r1 = requests.get(url1)  # 获取响应
        if 'window.code=201' in r1.text:
            # 有人扫码
            v = re.findall("window.userAvatar = '(.*)';", r1.text)
            avatar = v[0]
            ret['code'] = 201  # 状态码,代表有人扫码了
            ret['data'] = avatar  # 用户头像
        elif 'window.code=200;' in r1.text:
            # 扫码之后,点击确定登录
            req.session['login_cookie'] = r1.cookies.get_dict()  # 获取确认登陆的cookie
            uri = re.findall('window.redirect_uri="(.*)";', r1.text)  # 之前的图片,已经发现这里是一个重定向路由,所以获取重定向的路由
            # https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=AfpJ_ZmEJGcJ8iU62SiPyuTo@qrticket_0&uuid=gZHgYImvDQ==&lang=zh_CN&scan=1532410951
            redirect_url = '{0}&fun=new&version=v2'.format(uri[0])  # 字符串拼接,形成新的url
            # 获取凭证
            r2 = requests.get(redirect_url)  # 网页重定向的时候,会返回凭证用来进行之后的验证
            ticket_dict = {}
            soup = BeautifulSoup(r2.text, 'html.parser')  # 标签文本实例化bs对象
            for item in soup.find(name='error').children:  # 找到需要的凭证
                ticket_dict[item.name] = item.text  # 凭证存入字典
            req.session['ticket_dict'] = ticket_dict  # 凭证存入session
            req.session['ticket_cookie'] = r2.cookies.get_dict()  # session中存入获取重定向的cookie
            ret['code'] = 200  # 200表示确定登陆了
            req.session['is_login'] = True  # 给后面的url判断是否登陆
        return HttpResponse(json.dumps(ret))  # Json序列化返回

    最后创建一个新的路由:

        path('index/', views.index),

    创建一个新的html和视图函数index:

    def index(req):
        return render(req, 'wechat/index.html')

    此时,便可以完成web微信登陆功能。

    留一份确认登陆的凭证:

    <error>
        <ret>0</ret>
        <message></message>
        <skey>@crypt_41bad7c6_8bff0cc40ih18fba431b5a7e87l63bc7</skey>
        <wxsid>LkgBf9UVGIkHdg80</wxsid> 
        <wxuin>15749213440</wxuin>
        <pass_ticket>QvkUzyBHDWQ8HJDowkqw6rmqQ48weyrwDiNPTbn%2FnZl7tk%3D</pass_ticket>
        <isgrayscale>1</isgrayscale>
    </error>

    获取登陆信息

    根据平时登陆web wechat的常识,登陆确认之后肯定是跳往目的路由,初始化一些用户信息,获取用户的各种资料等等。这只是猜测怎么办?打开web 微信继续检测network。这一次从确认登陆完成进行截图分析。

    url: https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=696489291&lang=zh_CN&pass_ticket=QvkUzyBiMqnrFcw6rmqQ4I7E2DiNxTUDPTbn%252FnZl7tk%253D

    url里面有pass_ticket参数.

    看到凭证组成的form_data,另外此次是POST请求。由于response数据太多了,在index里面我们先写一个for循环看看都有哪些key值,看看能不能根据key值的命名规范,找到规律。

    视图函数index:

    def index(req):
        # 判断是否已经登陆
        if not req.session.get('is_login'):
            return redirect('/wechat/login/')
    
        # 发送post请求,根据ticket_dict进行构造数据
        # https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=892259194&pass_ticket=TS7TEfumVaVzKhn%252FrnLKS2zZyhixJDEYxlXqGgQVplQ%253D
        base_url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=892259194&pass_ticket={0}'
        url = base_url.format(req.session['ticket_dict']['pass_ticket'])
        # BaseRequest
        # :
        # {Uin: "184513440", Sid: "U4WojQwDRwKqdeMs", Skey: "", DeviceID: "e409571391728320"}  # 伪造的数据格式样板
    
        form_data = {  # 伪造数据
            'BaseRequest': {
                'DeviceID': "e409571391728320",
                'Sid': req.session['ticket_dict']['wxsid'],
                'Skey': req.session['ticket_dict']['skey'],
                'Uin': req.session['ticket_dict']['wxuin']
            }
        }
        r1 = requests.post(
            url=url,
            json=form_data
        )
        r1.encoding = r1.apparent_encoding  # 使用默认编码原则
        user_info = json.loads(r1.content)
        for key in user_info:
            print(key)
        return render(req, 'wechat/index.html', {"user_info": user_info})

    结果:

    BaseResponse
    Count
    ContactList
    SyncKey
    User
    ChatSet
    SKey
    ClientVersion
    SystemTime
    GrayScale
    InviteStartCount
    MPSubscribeMsgCount
    MPSubscribeMsgList
    ClickReportInterval

    可以猜测ContactList应该是最近联系人;User应该是用户本身,MPSubscribeMsgList应该是订阅号信息。

    再打印一下User里面的key看一下,视图函数改一句就好了:

        for key in user_info['User']:
            print(key)

    结果key:

    Uin
    UserName
    NickName
    HeadImgUrl
    RemarkName
    PYInitial
    PYQuanPin
    RemarkPYInitial
    RemarkPYQuanPin
    HideInputBarFlag
    StarFriend
    Sex
    Signature
    AppAccountFlag
    VerifyFlag
    ContactFlag
    WebWxPluginSwitch
    HeadImgFlag
    SnsFlag

    果然,现在这么看应该是登陆用户的信息了。

    同理测试一下订阅号:

    {
        'UserName': '@8fbbad518f4d3cf22ebd0a1b0b6d2cb5', 
        'MPArticleCount': 4, 
        'MPArticleList': [
            {
                'Title': '80%的爱情还没开始就会终结。关于错过的遗憾,20 年前几米就教给你了', 
                'Digest': '你活得不开心,因为你太把自己当大人。', 
                'Cover': 'http://mmbiz.qpic.cn/mmbiz_jpg/ib0l8DHhOSLuh527WuVHWnnIXwxR5s37pgacZriaKQPUkKEqArKEa9v6tH8buhrNHZB3IMMgf33RCKxtziazCTPrQ/640?wxtype=jpeg&wxfrom=0', 
                'Url': 'http://mp.weixin.qq.com/s?__biz=MzIzMzk1MzE0NQ==&mid=2247485983&idx=1&sn=6e268e95cdaf04c21114b53f1285bba4&chksm=e8fc8809df8b011fba6a8092400b312102e247780168d56f69c6863e31e7c4f3945253b1f259&scene=0#rd'
            }, 
            {...}
        ], 
        'Time': 1532608404,
        'NickName': '新世相读书会'
    }

    此时我们对index进行处理:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <h1>个人信息:{{ user_info.User.NickName }}</h1>
        <ul>
            {% for user in user_info.ContactList %}
                <li>{{ user.NickName }}</li>
            {% endfor %}
        </ul>
        <a href="/wechat/contact_all/">查看更多联系人</a>
        <h3>公众号信息</h3>
        {% for msg in user_info.MPSubscribeMsgList %}
        <div>
            <h3>{{ msg.NickName }}</h3>
            <ul>
                {% for article in msg.MPArticleList %}
                    <li><a href="{{ article.Url }}">{{ article.Title }}</a></li>
                {% endfor %}
            </ul>
        </div>
        {% endfor %}
    </body>
    </html>

    中间建立了一个新的url,用来显示所有联系人。先建立一个路由跟视图函数,其他暂时不管。

    稍微修改了一下视图函数,加了个User字典到session里面,后面要用到:

    def index(req):
        # 判断是否已经登陆
        if not req.session.get('is_login'):
            return redirect('/wechat/login/')
    
        # 获取最新联系人、并展示
        # 发送post请求,根据ticket_dict进行构造数据
        # https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=892259194&pass_ticket=TS7TEfumVaVzKhn%252FrnLKS2zZyhixJDEYxlXqGgQVplQ%253D
        base_url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=892259194&pass_ticket={0}'
        url = base_url.format(req.session['ticket_dict']['pass_ticket'])
        # BaseRequest
        # :
        # {Uin: "184513440", Sid: "U4WojQwDRwKqdeMs", Skey: "", DeviceID: "e409571391728320"}
    
        form_data = {
            'BaseRequest': {
                'DeviceID': "e409571391728320",
                'Sid': req.session['ticket_dict']['wxsid'],
                'Skey': req.session['ticket_dict']['skey'],
                'Uin': req.session['ticket_dict']['wxuin']
            }
        }
        r1 = requests.post(
            url=url,
            json=form_data
        )
        r1.encoding = r1.apparent_encoding
        user_info = json.loads(r1.content)
        req.session['current_user_info'] = user_info['User']
        # for k, v in user_info.items():
        #     print(k, v)
        # for user in user_info['ContactList']:
        #     print(user['NickName'])
        # for msg in user_info['MPSubscribeMsgList']:
        #     print(msg['NickName'])
        return render(req, 'wechat/index.html', {"user_info": user_info})

    结果示意图:

    此时解决查看联系人的问题,还是老方法,继续监控network。这里就不再卖关子了。getcontact这个就是获取所有联系人的url。

    不必多说,继续伪造:

        path('contact_all/', views.contact_all),

    视图函数:

    def contact_all(req):
        # https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?pass_ticket=z%252BTVxEioLl3A5arKy%252BUMHbeTME%252BmAkJEulNYIYgGcpw%253D&r=1532421128264&seq=0&skey=@crypt_41bed7c6_e213390395ab3a4ef54bfef5d004f719
        base_url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?pass_ticket={0}&r={1}&seq=0&skey={2}'
        url = base_url.format(
            req.session['ticket_dict']['pass_ticket'],
            time.time() * 1000,
            req.session['ticket_dict']['skey'],
        )  # url拼接
        all_cookies = {}
        all_cookies.update(req.session['login_cookie'])
        all_cookies.update(req.session['ticket_cookie'])  # 带入所有的cookies
    
        r1 = requests.get(url, cookies=all_cookies)
        r1.encoding = r1.apparent_encoding
        contact_dict = json.loads(r1.content)
        for item in contact_dict:
            print(item)
        #  for item in contact_dict['MemberList']:
        #     #     if item['RemarkName'] == '宇宙第一帅':
        #     #         print(item)
        return render(req, 'wechat/contact_all.html', {'contact_dict': contact_dict})

    index.html

    打印了所有的最外层的key:

    BaseResponse
    MemberCount
    MemberList
    Seq

    上面被注释的那段代码,可以用来查找你备注的某个人的信息。监控里面看到的username其实是用户在微信里面的唯一ID。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <div>
        <h1>联系人列表</h1>
        <ul>
            {% for item in contact_dict.MemberList %}
                <li>{{ item.UserName|safe }} --- {{ item.NickName|safe }}</li>
            {% endfor %}
        </ul>
    </div>
    </body>
    </html>

    截图:

    好了,现在到最紧张的最后一步了,如何发送消息?

    现在只需要伪造信息就好了,这里我们可以看见就是根据username的唯一id来发送信息的。

    在contact网页里面添加如下代码:

    <div>
        <h1>发送消息</h1>
        <p>接受者:<input type="text" id="recv" /></p>
        <p>内容:<input type="text" id="content" /></p>
        <input type="button" id="btn" value="Send" />
    </div>
    
    <script src="/static/jquery.min.js"></script>
    <script>
        $(function() {
            $('#btn').click(function() {
                var recv = $('#recv').val();
                var content = $('#content').val();
                $.ajax({
                    url: '/wechat/send_msg/',
                    type: 'GET',
                    data: {'recv': recv, 'content': content},
                    success: function(arg) {
                        console.log(arg)
                    }
                })
            })
        })
    </script>

    路由:

        path('send_msg/', views.send_msg),

    视图函数:

    def send_msg(req):
        recv = req.GET.get('recv')
        content = req.GET.get('content')
        # https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?pass_ticket=wtwzy%252F7fxQgJaTA511weqPXIkIGSJmZdCRATgZdIfYY%253D
        base_url = 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxsendmsg?pass_ticket={0}'
        url = base_url.format(req.session['ticket_dict']['pass_ticket'])
        ctime = time.time() * 1000
        form_data = {  # 伪造数据格式
            'BaseRequest': {
                'DeviceID': "e939509344931677",
                'Sid': req.session['ticket_dict']['wxsid'],
                'Skey': req.session['ticket_dict']['skey'],
                'Uin': req.session['ticket_dict']['wxuin']
            },
            'Msg': {
                'ClientMsgId': ctime,
                'Content': content,
                'FromUserName': req.session['current_user_info']['UserName'],
                'LocalID': ctime,
                'ToUserName': recv,
                'Type': 1,  # 文本
            },
            'Scene': 0
        }
        all_cookies = {}
        all_cookies.update(req.session['login_cookie'])
        all_cookies.update(req.session['ticket_cookie'])  # 带入cookie,试验过证明是需要的
        r1 = requests.post(
            url=url,
            data=bytes(json.dumps(form_data, ensure_ascii=False), encoding='utf-8'),
            cookies=all_cookies,
            headers={
                'Content-Type': 'application/json'  # 这句话用来表示,需要序列化成json数据;也可以去掉data跟headers直接用json=form_data来实现
            }
        )
        print(r1.text)
        return HttpResponse('.....')

    测试一下:

    em。。。。。。

    对,然后被拉黑了。至于接收消息,发送图片什么的其实都可以自己通过网络监控做到的。这里就不再多提了。

    收工!

  • 相关阅读:
    C++-蓝桥杯-小数第n位[除法模拟]
    C++-蓝桥杯-合成植物[并查集][模板题]
    Overleaf操作
    三维向量差积,以及应用
    C++-蓝桥杯-分考场[2017真题][搜索][无向图染色]
    C++-POJ1094-Sorting It All Out[拓扑排序][邻接矩阵]
    C++-LUOGU1059-明明的随机数[堆排序]
    C++-快速排序[STL][快速排序][并归排序][堆排序]
    C++-蓝桥杯-波动数组[2014真题][DP优化]
    C++-蓝桥杯-[T1~T3][结果填空题][2015真题][水题]
  • 原文地址:https://www.cnblogs.com/wuzdandz/p/9374821.html
Copyright © 2020-2023  润新知