Python爬虫实战之(五)| 模拟登陆wechat

做者:xiaoyunode

微信公众号:Python数据科学linux

知乎:Python数据分析师git


不知什么时候,微信已经成为咱们不可缺乏的一部分了,咱们的社交圈、关注的新闻或是公众号、还有我的信息或是隐私都被绑定在了一块儿。既然它这么重要,若是咱们能够利用爬虫模拟登陆,是否是就意味着咱们能够获取这些信息,甚至能够根据须要来对它们进行有效的查看和管理。是的,没错,这彻底能够。本篇博主将会给你们分享一下如何模拟登陆网页版的微信,并展现模拟登陆后获取的好友列表信息github

微信模拟登陆的过程比较复杂,固然无论怎么样方法都是万变不离其宗,咱们仍是使用fiddler抓包工具来模拟登陆的过程。 好了,下面让咱们一步一步的详细讲解一下如何实现的这个复杂的过程。web

用fiddler模拟登陆的请求

首先,咱们在浏览器上打开微信网页版(fiddler已经在这以前打开了),而后咱们会看到一个二维码的界面。json

而后咱们使用手机微信扫描并确认,这时候网页版的微信就登录了。windows

好,咱们去看看fiddler都给咱们抓取了什么信息包。因为过程当中发出的请求有点多,这里把抓包按操做进行分解并逐一分析。浏览器

1.打开微信网页

这一步骤的抓包是这样的,发现其中login.wx.qq.com的两个连接是咱们须要的。bash

因而点开详细分析一下。微信

第一个连接以下,是一个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
复制代码

通过屡次抓取发现appid、redirect_uri、fun、lang参数都是固定的,而_是一串变化的数字,咱们在以前模拟京东商城的文章提过,它实际上是一个时间戳,若是不清楚能够回顾一下[Python爬虫实战之(四)| 模拟登陆京东商城][1]

知道这些参数,模拟get发送出去就能够了。那么咱们为何要模拟这一步呢?

是由于访问这个连接会有以下的响应,而其中有咱们后续须要的重要信息uuid(后面步骤会提到)。

window.QRLogin.code = 200; window.QRLogin.uuid = "Idf_QdW1OQ==";
复制代码

2.模拟获取二维码

微信网页提供的登陆方式是扫码,咱们模拟也没法避开,所以也要进行扫码验证。回到浏览器,使用开发者工具能够轻松找到二维码的连接。

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请求,一样携带了一些参数。

实际上在抓包过程发现只要咱们不扫描二维码,这个连接就会一直重复发送直到二维码被扫描或者超时。

那么咱们如何判断二维码是否被扫描或者已经登录了呢?

仍是经过响应的数据来进行判断的。经分析发现若是二维码一直没被扫,那么响应是这样的:

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上会多出几个新的请求。

你可能发现了,上一步骤中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请求。携带参数以下:

固然,这个登陆请求一样也会返回一些响应代码,响应代码以下:

<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
复制代码

基本的登陆过程就是这样,有点复杂,博主总结了个流程图供参考。

代码实现

请求模拟使用requests模块完成,解析使用re。这里须要注意一下,若是运行一直报ssl的错,能够在request请求里面加上了verify=False跳过证书认证来解决。

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']))
复制代码

模拟登陆结果

好友列表以下:

固然,好友列表只是个例子,咱们也能够对其它信息进行查看和管理或者数据分析。

总结

本篇与你们分享了网页版微信的模拟登陆过程。尽管过程当中请求多有点复杂,可是只要咱们仔细分析仍是能够一步一步实现的,但愿对你们有帮助,代码已上传到github:连接

完毕。


关注微信公众号Python数据科学,获取 120G 人工智能 学习资料。

相关文章
相关标签/搜索