Python爬蟲實戰之(五)| 模擬登入wechat

Python資料科學發表於2018-04-10

作者:xiaoyu

微信公眾號:Python資料科學

知乎:Python資料分析師


不知何時,微信已經成為我們不可缺少的一部分了,我們的社交圈、關注的新聞或是公眾號、還有個人資訊或是隱私都被繫結在了一起。既然它這麼重要,如果我們可以利用爬蟲模擬登入,是不是就意味著我們可以獲取這些資訊,甚至可以根據需要來對它們進行有效的檢視和管理。是的,沒錯,這完全可以。本篇博主將會給大家分享一下如何模擬登入網頁版的微信,並展示模擬登入後獲取的好友列表資訊

微信模擬登入的過程比較複雜,當然不管怎麼樣方法都是萬變不離其宗,我們還是使用fiddler抓包工具來模擬登入的過程。 好了,下面讓我們一步一步的詳細講解一下如何實現的這個複雜的過程。

用fiddler模擬登入的請求

首先,我們在瀏覽器上開啟微信網頁版(fiddler已經在這之前開啟了),然後我們會看到一個二維碼的介面。

Python爬蟲實戰之(五)| 模擬登入wechat

然後我們使用手機微信掃描並確認,這時候網頁版的微信就登陸了。

好,我們去看看fiddler都給我們抓取了什麼資訊包。由於過程中發出的請求有點多,這裡把抓包按操作進行分解並逐一分析。

1.開啟微信網頁

這一步驟的抓包是這樣的,發現其中login.wx.qq.com的兩個連結是我們需要的。

Python爬蟲實戰之(五)| 模擬登入wechat

於是點開詳細分析一下。

第一個連結如下,是一個get請求,可以看到uri中攜帶了一些引數appid、redirect_uri、fun、lang、_

GET /jslogin?appid=wx782c26e4  c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=zh_CN&_=1520350213674 HTTP/1.1
複製程式碼

Python爬蟲實戰之(五)| 模擬登入wechat

經過多次抓取發現appid、redirect_uri、fun、lang引數都是固定的,而_是一串變化的數字,我們在之前模擬京東商城的文章提過,它其實是一個時間戳,如果不清楚可以回顧一下[Python爬蟲實戰之(四)| 模擬登入京東商城][1]

知道這些引數,模擬get傳送出去就可以了。那麼我們為什麼要模擬這一步呢?

是因為訪問這個連結會有如下的響應,而其中有我們後續需要的重要資訊uuid(後面步驟會提到)。

window.QRLogin.code = 200; window.QRLogin.uuid = "Idf_QdW1OQ==";
複製程式碼

2.模擬獲取二維碼

微信網頁提供的登入方式是掃碼,我們模擬也無法避開,因此也要進行掃碼驗證。回到瀏覽器,使用開發者工具可以輕鬆找到二維碼的連結。

Python爬蟲實戰之(五)| 模擬登入wechat

https://login.weixin.qq.com/qrcode/AdgAWNry-w==
複製程式碼

我們發現最後的字串是變化的。等等,它和uuid一模一樣的。沒錯,它就是uuid,用來保證二維碼的唯一性。

因此,我們將上面提取的uuid拼接到後面就可以得到二維碼圖片了,然後進行掃碼確認操作。

3.識別登入狀態

為了識別掃碼是否成功,這個步驟我們需要用到上面提到的第二個連結。

GET /cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=Idf_QdW8OQ==&tip=1&r=68288473&_=1520050213675 HTTP/1.1
複製程式碼

這個連結也是個get請求,同樣攜帶了一些引數。

Python爬蟲實戰之(五)| 模擬登入wechat

實際上在抓包過程發現只要我們不掃描二維碼,這個連結就會一直重複傳送直到二維碼被掃描或者超時。

那麼我們如何判斷二維碼是否被掃描或者已經登陸了呢?

還是通過響應的資料來進行判斷的。經分析發現如果二維碼一直沒被掃,那麼響應是這樣的:

window.code=408;
複製程式碼

但是如果二維碼被掃描了,響應是這樣的:

window.code=201;window.userAvatar = .....
window.code=200;
window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=AaL_Xd5muLPKNVY_Hzt_uoBs@qrticket_0&uuid=gbJqPdkNSQ==&lang=zh_CN&scan=1520353803";
複製程式碼

code=201說明二維碼被掃描成功了。 code=200說明是登入成功了。

4.登入

掃描了二維碼之後,fiddler上會多出幾個新的請求。

Python爬蟲實戰之(五)| 模擬登入wechat

你可能發現了,上一步驟中code=200後面有個重定向的uri,這個uri就是此步驟中跳轉的登入連結。

GET https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=AaL_Xd5muLPKNVY_Hzt_udBs@qrticket_0&uuid=gbJqPdfNSQ==&lang=zh_CN&scan=1520353803&fun=new&version=v2 HTTP/1.1
複製程式碼

通過上一步驟識別登入成功的響應我們可以得到響應裡面的所有引數。沒錯,這些引數正好可以用在正式登入(即跳轉連結)的請求中。於是我們利用這些引數再進行一次get請求。攜帶引數如下:

Python爬蟲實戰之(五)| 模擬登入wechat

當然,這個登入請求同樣也會返回一些響應程式碼,響應程式碼如下:

<error>
     <ret>0</ret>
     <message>OK</message>
     <skey>xxx</skey>
     <wxsid>xxx</wxsid>
     <wxuin>xxx</wxuin>
     <pass_ticket>xxx</pass_ticket>
     <isgrayscale>1</isgrayscale>
</error>
複製程式碼

又是一堆引數,簡直沒完沒了啊。彆著急,我們已經接近成功了。獲取這個響應我們一樣需要將其中的引數全部提取出來供下一請求使用。

5.初始化同步

好了,終於到了最後一步了,就是微信的初始化和同步的請求了,初始化資訊連結如下:

POST https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxinit?r=64629109&pass_ticket=4dU5IS9EqtXt5cIV2Gni1tKG7m2V56PXk5XI%252BdjdrIk%253D HTTP/1.1
複製程式碼

contact聯絡連結如下:

GET https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetcontact?pass_ticket=4dU5IS9EqtXt5cIV2Gni1tKG7m2V56PXk5XI%252BdjdrIk%253D&r=1520353806102&seq=0&skey=@crypt_a82dd73a_3885c878ae2f4590f7b2b5ee949dd1bd HTTP/1.1
複製程式碼

uri中引數pass_ticket,skey在上一步的響應中已獲取,直接傳送請求即可完成。從這兩個連結的響應中,我們就可以得到一些真實有用的資訊了。

還有一個同步的請求連結,所需引數可以從上面兩個連結響應中提取。但是至此我們通過上面兩個連結已經可以獲取我們想要的資訊,因此可以不必請求這個同步連結。

GEThttps://webpush.wx.qq.com/cgi-bin/mmwebwx-bin/synccheck?r=1520353806125&skey=%40crypt_a82dd73a_3885c878ae2f4590f7b2b5ee949dd1bd&sid=O2Se5s2LJzPebME2&uin=254891255&deviceid=e289448639092966&synckey=1_694936977%7C2_694936979%7C3_694936982%7C1000_1520324882&_=1520353793581 HTTP/1.1
複製程式碼

基本的登入過程就是這樣,有點複雜,博主總結了個流程圖供參考。

Python爬蟲實戰之(五)| 模擬登入wechat

程式碼實現

請求模擬使用requests模組完成,解析使用re。這裡需要注意一下,如果執行一直報ssl的錯,可以在request請求裡面加上了verify=False跳過證照認證來解決。

Python爬蟲實戰之(五)| 模擬登入wechat

1.初始化引數

def __init__(self):
    self.session = requests.session()
    self.headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 5.1; rv:33.0) Gecko/20100101 Firefox/33.0'}
    self.QRImgPath = os.path.split(os.path.realpath(__file__))[0] + os.sep + 'webWeixinQr.jpg'
    self.uuid = ''
    self.tip = 0
    self.base_uri = ''
    self.redirect_uri = ''
    self.skey = ''
    self.wxsid = ''
    self.wxuin = ''
    self.pass_ticket = ''
    self.deviceId = 'e000000000000000'
    self.BaseRequest = {}
    self.ContactList = []
    self.My = []
    self.SyncKey = ''
複製程式碼

定義一個類,初始化例項的所有請求引數,定義二維碼的路徑。

2.請求uuid

def getUUID(self):
    url = 'https://login.weixin.qq.com/jslogin'
    params = {
        'appid': 'wx782c26e4c19acffb',
        'redirect_uri': 'https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage',
        'fun': 'new',
        'lang': 'zh_CN',
        '_': int(time.time() * 1000),  # 時間戳
    }
    response = self.session.get(url, params=params)
    target = response.content.decode('utf-8')
    pattern = r'window.QRLogin.code = (\d+); window.QRLogin.uuid = "(\S+?)"'
    ob = re.search(pattern, target)  # 正則提取uuid
    code = ob.group(1)
    self.uuid = ob.group(2)
    if code == '200':  # 判斷請求是否成功
        return True
    return False
複製程式碼

使用正則對相應進行提取獲取uuid,通過code判斷請求是否成功,響應如下:

window.QRLogin.code = 200; window.QRLogin.uuid = "Idf_QdW1OQ==";
複製程式碼

3.模擬獲取二維碼

def showQRImage(self):
    url = 'https://login.weixin.qq.com/qrcode/' + self.uuid
    response = self.session.get(url)
    self.tip = 1
    with open(self.QRImgPath, 'wb') as f:
        f.write(response.content)
        f.close()
    # 開啟二維碼
    if sys.platform.find('darwin') >= 0:
        subprocess.call(['open', self.QRImgPath])  # 蘋果系統
    elif sys.platform.find('linux') >= 0:
        subprocess.call(['xdg-open', self.QRImgPath])  # linux系統
    else:
        os.startfile(self.QRImgPath)  # windows系統
    print('請使用微信掃描二維碼登入')
複製程式碼

使用uuid請求二維碼圖片,並根據作業系統自動開啟。

4.識別登入狀態

def checkLogin(self):
    url = 'https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login?tip=%s&uuid=%s&_=%s' % (
        self.tip, self.uuid, int(time.time() * 1000))
    response = self.session.get(url)
    target = response.content.decode('utf-8')
    pattern = r'window.code=(\d+);'
    ob = re.search(pattern, target)
    code = ob.group(1)
    if code == '201':  # 已掃描
        print('成功掃描,請在手機上點選確認登入')
        self.tip = 0
    elif code == '200':  # 已登入
        print('正在登入中...')
        regx = r'window.redirect_uri="(\S+?)";'
        ob = re.search(regx, target)
        self.redirect_uri = ob.group(1) + '&fun=new'
        self.base_uri = self.redirect_uri[:self.redirect_uri.rfind('/')]
    elif code == '408':  # 超時
        pass
    return code
複製程式碼

響應如下:

window.code=200;
window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=AaL_Xd5muLPKNVY_Hzt_uoBs@qrticket_0&
複製程式碼

根據響應中的code程式碼識別登入狀態。 408:超時 201:已掃描 200:已登入

5.登入

def login(self):
    response = self.session.get(self.redirect_uri, verify=False)
    data = response.content.decode('utf-8')
    doc = xml.dom.minidom.parseString(data)
    root = doc.documentElement
    # 提取響應中的引數
    for node in root.childNodes:
        if node.nodeName == 'skey':
            self.skey = node.childNodes[0].data
        elif node.nodeName == 'wxsid':
            self.wxsid = node.childNodes[0].data
        elif node.nodeName == 'wxuin':
            self.wxuin = node.childNodes[0].data
        elif node.nodeName == 'pass_ticket':
            self.pass_ticket = node.childNodes[0].data
    if not all((self.skey, self.wxsid, self.wxuin, self.pass_ticket)):
        return False
    self.BaseRequest = {
        'Uin': int(self.wxuin),
        'Sid': self.wxsid,
        'Skey': self.skey,
        'DeviceID': self.deviceId,
    }
    return True
複製程式碼

請求跳轉的登入連結,提取響應程式碼引數,響應如下:

<error>
    <ret>0</ret>
    <message>OK</message>
    <skey>xxx</skey>
    <wxsid>xxx</wxsid>
    <wxuin>xxx</wxuin>
    <pass_ticket>xxx</pass_ticket>
    <isgrayscale>1</isgrayscale>
</error>
複製程式碼

6.初始化獲取資訊

def webwxinit(self):
    url = self.base_uri + \
          '/webwxinit?pass_ticket=%s&skey=%s&r=%s' % (
              self.pass_ticket, self.skey, int(time.time() * 1000))
    params = {
        'BaseRequest': self.BaseRequest
    }
    h = self.headers
    h['ContentType'] = 'application/json; charset=UTF-8'
    response = self.session.post(url, data=json.dumps(params), headers=h, verify=False)
    data = response.content.decode('utf-8')
    print(data)
    dic = json.loads(data)
    self.ContactList = dic['ContactList']
    self.My = dic['User']
    SyncKeyList = []
    for item in dic['SyncKey']['List']:
        SyncKeyList.append('%s_%s' % (item['Key'], item['Val']))
    self.SyncKey = '|'.join(SyncKeyList)
    ErrMsg = dic['BaseResponse']['ErrMsg']
    Ret = dic['BaseResponse']['Ret']
    if Ret != 0:
        return False
    return True
複製程式碼

請求初始化的連結,獲取初始化響應資料。

def webwxgetcontact(self):
    url = self.base_uri + \
          '/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s' % (
              self.pass_ticket, self.skey, int(time.time()))
    h = self.headers
    h['ContentType'] = 'application/json; charset=UTF-8'
    response = self.session.get(url, headers=h, verify=False)
    data = response.content.decode('utf-8')
    # print(data)
    dic = json.loads(data)
    MemberList = dic['MemberList']
    # 倒序遍歷,不然刪除的時候出問題..
    SpecialUsers = ["newsapp", "fmessage", "filehelper", "weibo", "qqmail", "tmessage", "qmessage", "qqsync",
                    "floatbottle", "lbsapp", "shakeapp", "medianote", "qqfriend", "readerapp", "blogapp",
                    "facebookapp", "masssendapp",
                    "meishiapp", "feedsapp", "voip", "blogappweixin", "weixin", "brandsessionholder",
                    "weixinreminder", "wxid_novlwrv3lqwv11", "gh_22b87fa7cb3c", "officialaccounts",
                    "notification_messages", "wxitil", "userexperience_alarm"]
    for i in range(len(MemberList) - 1, -1, -1):
        Member = MemberList[i]
        if Member['VerifyFlag'] & 8 != 0:  # 公眾號/服務號
            MemberList.remove(Member)
        elif Member['UserName'] in SpecialUsers:  # 特殊賬號
            MemberList.remove(Member)
        elif Member['UserName'].find('@@') != -1:  # 群聊
            MemberList.remove(Member)
        elif Member['UserName'] == self.My['UserName']:  # 自己
            MemberList.remove(Member)
    return MemberList
複製程式碼

請求contact的連結,獲取聯絡人、公眾號、群聊以及個人資訊。響應程式碼為json格式,如下:

{
"BaseResponse": {
"Ret": 0,
"ErrMsg": ""
}
,
"Count": 11,
"ContactList": [{
"Uin": 0,
"UserName": "filehelper",
"NickName": "檔案傳輸助手",
"HeadImgUrl": "/cgi-bin/mmwebwx-bin/webwxgeticon?seq=621637626&username=filehelper&skey=@crypt_a82dd73a_7e8e1054c011e8d71d0b542f39c7db85",
"ContactFlag": 3,
"MemberCount": 0,
"MemberList": [],
"RemarkName": "",
"HideInputBarFlag": 0,
"Sex": 0,
"Signature": "",
"VerifyFlag": 0,
"OwnerUin": 0,
"PYInitial": "WJCSZS",
"PYQuanPin": "wenjianchuanshuzhushou",
"RemarkPYInitial": "",
"RemarkPYQuanPin": "",
"StarFriend": 0,
"AppAccountFlag": 0,
"Statues": 0,
"AttrStatus": 0,
"Province": "",
"City": "",
"Alias": "",
"SnsFlag": 0,
"UniFriend": 0,
"DisplayName": "",
"ChatRoomId": 0,
"KeyWord": "fil",
"EncryChatRoomId": "",
"IsOwner": 0
}
,{...}
...
複製程式碼

根據響應中欄位資訊做資訊操作,這裡是獲取好友列表,所以將其它欄位如公眾號、群聊、自己都去掉了,只保留好友資訊。

7.主函式執行

def main(self):
    if not self.getUUID():
        print('獲取uuid失敗')
        return
    self.showQRImage()
    time.sleep(1)
    while self.checkLogin() != '200':
        pass
    os.remove(self.QRImgPath)
    if not self.login():
        print('登入失敗')
        return
    # 登入完成, 下面查詢好友
    if not self.webwxinit():
        print('初始化失敗')
        return
    MemberList = self.webwxgetcontact()
    print('通訊錄共%s位好友' % len(MemberList))
    for x in MemberList:
        sex = '未知' if x['Sex'] == 0 else '男' if x['Sex'] == 1 else '女'
        print('暱稱:%s, 性別:%s, 備註:%s, 簽名:%s' % (x['NickName'], sex, x['RemarkName'], x['Signature']))
複製程式碼

模擬登入結果

好友列表如下:

Python爬蟲實戰之(五)| 模擬登入wechat

Python爬蟲實戰之(五)| 模擬登入wechat

當然,好友列表只是個例子,我們也可以對其它資訊進行檢視和管理或者資料分析。

總結

本篇與大家分享了網頁版微信的模擬登入過程。儘管過程中請求多有點複雜,但是隻要我們仔細分析還是可以一步一步實現的,希望對大家有幫助,程式碼已上傳到github:連結

完畢。


關注微信公眾號Python資料科學,獲取 120G 人工智慧 學習資料。

Python爬蟲實戰之(五)| 模擬登入wechat

Python爬蟲實戰之(五)| 模擬登入wechat

相關文章