1. 程式人生 > >Python 爬蟲案例-web微信登陸與消息發送

Python 爬蟲案例-web微信登陸與消息發送

recv 遺憾 內部 set rmq view ons ats ascii

首先回顧下網頁微信登陸的一般流程

  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="width: 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。。。。。。

對,然後被拉黑了。至於接收消息,發送圖片什麽的其實都可以自己通過網絡監控做到的。這裏就不再多提了。

收工!

Python 爬蟲案例-web微信登陸與消息發送