Python模拟二维码登录百度

模拟二维码登录百度

    • 写在前面
    • 准备工作
      • 二维码地址
      • 登录状态
      • 获取gid
      • 登录参数
    • 代码部分
      • 二维码展示
      • 获取cookie
    • 完整代码
    • 写在后面

写在前面

前段时间写了利用BDUSS到达百度首页,这一次尝试使用二维码模拟登录,目前网上能搜到的相关内容基本失效了,但是思路基本不变,无非是百度改了些参数。本文较为复杂,要求对python的requests模块以及Chrome审查元素有一定了解,我不确定自己是否能完全讲明白,讲多少是多少吧,各位看官请坐。

准备工作

二维码地址

打开Chrome浏览器,清理掉baidu.com下的所有cookie,如果没有则忽略。打开百度首页,F12审查元素,在右上角点击登录,切换到二维码登录,可以看到NetWork里面出现了一些相关内容:


Python模拟二维码登录百度_第1张图片


查看getqrcode?lp=pc:


Python模拟二维码登录百度_第2张图片


这里的imgurl代表二维码地址,在浏览器访问一下就能看到二维码,为了获得该地址,我们需要向该条目的Request URL发起请求:

https://passport.baidu.com/v2/api/getqrcode?lp=pc&qrloginfrom=pc

有些小伙伴可能看到Request URL包含有gid、callback等参数,这里并不需要,访问上面的地址就能获得imgurl。

登录状态

让页面停滞一会儿,可以看到出现了好多unicast开头的请求,就是用来验证二维码扫描状态的。


Python模拟二维码登录百度_第3张图片


未扫描时response如下:

tangram_guid_1607686170040({"errno":1})

先分析一下需要的参数:


Python模拟二维码登录百度_第4张图片


channel_id是不是看起来很熟悉?就是imgurl里面的sign,这个参数就搞定了。刷新几次页面,可以看到gid、callback、tt、_这四个参数发生了变化,仔细观察可以发现后面两个是毫秒级时间戳,callback变化的只有后面那串数字,也是毫秒级时间戳,gid等下再说。观察多个unicast请求,发现gid、callback是不变的,可以确定callback是一个默认字符串加上第一次unicast请求的时间戳,另外两项则是新的unicast请求发生时的时间戳。对比相邻的unicast的tt参数,相减得到的数值接近30000,即30s,再细心观察,新的unicast出现时是一个unsafe-url:


Python模拟二维码登录百度_第5张图片


过30s左右会变成正常的Get请求,在浏览器直接访问Request URL也是在30s后得到Response(真的不是你断网了…),也就是说,二维码未被扫描的时候,每30s验证一次状态(二维码被扫描,未点击确定的时候会重新设定等待时间,也许是5s),这样的话,在使用requests发送请求时要将timeout设置为30s以上,不然无法正确验证状态。


获取gid

这个参数在getqrcode?lp=pc请求中就已经出现了,首先查看该请求调用的js文件:


Python模拟二维码登录百度_第6张图片


打开setChannel对应的js,单击左下角的花括号可以让格式化输出,按Ctrl+F搜索“gid”,可以看到指向了guideRandom,再搜索guideRandom,得到如下信息:


Python模拟二维码登录百度_第7张图片


需要将其转换为python代码:

#获取gid
def guideRandom():
    string = 'xxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
    gid = ''
    #下面是根据JavaScript代码写的
    for z in string:
        #获取0到1的随机数,并乘以16
        t = 16 * random.random()
        #如果是z是x或者y
        if z in "xy":
            #hex将数字转换为十六进制,后面的你肯定能读懂
            s = hex(int(t)) if z == "x" else hex(3 & int(t) | 8)
            #将十六进制数转换为字符串,删除前面0x
            gid += str(s).replace('0x','')
        else:
            #不是x或y则不做处理
            gid += z
    #将字符串转换为大写,返回
    return gid.upper()

另外关于callback的函数,可以在‘uni_loginv4_tangram_b55ba1e.js’中搜索‘callback’:


Python模拟二维码登录百度_第8张图片


baidu.id.key是“tangram_guid”,m是时间戳加1,与上面的猜测差不多。
检测二维码扫描状态的参数分析完毕。

登录参数

用百度的app扫描二维码,但不要点确定,可以发现unicast的Response变了:


Python模拟二维码登录百度_第9张图片


errno变为0,而且获取到channel_id,跟imgurl中的sign一致(我这里跟上面不一样是因为刷新了页面),还有一个status,不出意外的话,这个是用来判断用户是否点击确定的,app点击确定,再查看unicast的Response:


Python模拟二维码登录百度_第10张图片


可以看到status变为0,而且多了个参数"v",这个有什么用处呢?先放着不管,去看下面这个包:


Python模拟二维码登录百度_第11张图片


Python模拟二维码登录百度_第12张图片


这里设置了BDUSS、PTOKEN、STOKEN三个cookie,这些就是登录成功的标志,带上这三个cookie可以访问百度的大多数产品。再看看需要的参数:


Python模拟二维码登录百度_第13张图片


bduss是不是有点眼熟?没错,就是刚才unicast返回的v,而且只有这个参数比较重要,别的要么是时间戳或固定值,就算有几个是变化的(sig、shaOne、callback等),用固定值也没有什么影响。OK,参数解析完毕。


如果跳转太快抓不到包的话,在开发者工具里面:


Python模拟二维码登录百度_第14张图片


将网络模式改为Slow 3G(手速比网速快的可以不改),调整完毕再点击客户端上的确定,Response发生变化时快速点击左上角的小红点停止继续抓包,就不会因为刷新页面丢失抓到的包。

代码部分

代码分为两部分,第一部分是二维码的获取及展示,第二部分是获取cookie。

二维码展示

#将关于二维码的部分封装成类
class Show(object):
    def __init__(self,imgurl):
		#创建一个子线程,用于展示二维码
		#如果不创建子线程,在二维码关闭之前程序停滞
        self.t = threading.Thread(target = self.get_img)
		#将线程设置为后台运行
        self.t.setDaemon(True)
		#二维码的网页地址
        self.imgurl = imgurl
        
    def get_img(self):
		#访问imgurl,将二维码保存到本地
        res = requests.get(url = self.imgurl,headers = headers)
        f = open('baidu.jpg','wb')
        f.write(res.content)
        f.close()
		#暂停1秒,不然加载的二维码可能比较模糊
        time.sleep(1)
		#Pillow读取二维码
        img = Image.open('baidu.jpg')
		#在窗口中显示(此时并没有真的显示,因为子线程还没有启动)
        img.show()
		#调用此函数的是python子线程,展示图片的是一个windows进程
		#使用psutil获取进程句柄,我们可以在合适的时候将该进程关掉
        for proc in psutil.process_iter():
			#前者是win10的照片查看器,后者是win7的
			#如果你的图片默认程序不是照片查看器,那么此处需要修改
			#获取方式:打开任意一张图片,任务管理器-性能-资源监视器
            if proc.name() in ['Microsoft.Photos.exe','dllhost.exe']:
                self.proc = proc
                break
    #启动子线程
    def show(self):
        self.t.start()
    #关闭二维码进程
    def close(self):
        self.proc.kill()

获取cookie

class Login(object):
	#初始化变量
    def __init__(self,login_params,check_params,show):
		#Show对象,控制二维码展示和关闭
        self.show = show
		#检查二维码扫描状态的URL
        self.check_url = 'https://passport.baidu.com/channel/unicast'
		#获取BDUSS等cookie的URL
        self.login_url = 'https://passport.baidu.com/v3/login/main/qrbdusslogin'
		#login_url的参数
        self.login_params = login_params
		#check_url的参数
        self.check_params = check_params

    #检查二维码扫描状态
    def check_status(self):
		#启动子线程在窗口展示二维码
        self.show.show()
		#将errno和status设定为1
        errno = 1
        status = 1
		#用于在while循环中关闭windows照片查看器
        flag = True
		#unicast的response不是标准json格式,需要进行正则匹配
        pattern = re.compile('({.*})')
		#开始循环
        while True:
			#访问目标地址,将timeout设置为35s
            req = requests.get(url = self.check_url,headers = passport_headers,cookies = cookie,params = self.check_params,timeout = 35)
			#替换返回值中的不合法字符,以便转换为json格式
			#菜如我只能用三个replace......
            response = req.text.replace('\\','').replace('"{',"{").replace('}"',"}")
            try:
				#如果errno或者status其中一项为1
                if errno or status:
					#将response转换为json格式
                    message = json.loads(re.search(pattern,response).group())
					#更新errno
                    errno = message['errno']
					#如果errno变为0(用户扫描二维码),并且照片查看器未关闭
                    if errno == 0 and flag:
						#关闭照片查看器,将flag设置为False
                        self.show.close()
                        flag = False
					#如果errno为0,则更新status值
                    elif errno == 0:
                        status = message['channel_v']['status']
				#如果errno和status都为0,则补充login_params的bduss字段,结束循环
                if not errno and not status:
                    message = json.loads(re.search(pattern,response).group())
                    self.login_params['bduss'] = message['channel_v']['v']
                    break
				#更新check_paramas中两个关于时间的参数
                self.check_params['tt'] = get_time()
                self.check_params['_'] = self.check_params['tt'] + 2
            except Exception as e:
                print(e)
                break
    #获取cookie
    def login(self):
		#带上完整参数请求登录地址
        login_r = requests.get(self.login_url, headers=headers, params=self.login_params,cookies = cookie)
		#将BDUSS、PTOKEN、STOKEN写入cookie
        for key in login_r.cookies.keys():
            cookie[key] = login_r.cookies[key]
        print('登陆成功,正在保存cookie')
		#将cookie保存到本地
        f = open('auth_cookie.txt','wt')
        f.write(str(cookie))
        f.close()
        print('保存完毕')

完整代码

# -*- coding: utf-8 -*-
"""
Created on Fri Dec 11 08:36:50 2020

@author: ljc545w
"""

# -*- coding: utf-8 -*-
#用到的包
#注:requests和Pillow为第三方包,需要使用pip命令安装
import requests
#时间管理带师
import time
#获取随机数
import random
#正则、json字典
import re,json
#Pillow
from PIL import Image
#线程
import threading
#进程
import psutil

#请求头
headers = {
     
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36',
        }

passport_headers = {
     
        'Host': 'passport.baidu.com',
        'User-Agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36',
        'Referer': 'https://www.baidu.com/'
        }
#定义一个空字典用于保存cookie
cookie = {
     }

#获取毫秒级时间戳
def get_time():
    cur_time = int(round(time.time() * 1000))
    return cur_time

#获取gid
def guideRandom():
    string = 'xxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
    gid = ''
    #下面是根据JavaScript代码写的
    for z in string:
        #获取0到1的随机数,并乘以16
        t = 16 * random.random()
        #如果是z是x或者y
        if z in "xy":
            #hex将数字转换为十六进制,后面的你肯定能读懂
            s = hex(int(t)) if z == "x" else hex(3 & int(t) | 8)
            #将十六进制数转换为字符串,删除前面0x
            gid += str(s).replace('0x','')
        else:
            #不是x或者y则不做处理
            gid += z
    #将字符串转换为大写,返回
    return gid.upper()

#获取必要参数
def GetParams():
    #这个是百度首页
    Base_url = 'https://www.baidu.com/'
    #先获取一些固定的cookie,如BAIDUID、PSTM等
    Base_res = requests.get(url = Base_url,headers = headers)
    for key in Base_res.cookies.keys():
        #这里有一定概率引发异常,可能是因为某个cookie为空,预处理一下
        try:
            cookie[key] = Base_res.cookies[key]
        except:
            pass
    #用于登录的参数(这里还没有加入bduss)
    login_params = {
     
        #获取时间戳
    	'v': get_time(),
    	'u': 'https://www.baidu.com/',
        'loginVersion': 'v4',
    	'qrcode': '1',
    	'tpl': 'mn',
    	'apiver': 'v3',
        #获取时间戳
    	'tt': get_time(),
    	'traceid': None,
        #这里也是时间戳,不过是秒级,所以直接调用time方法,然后取整
        'time': round(time.time()),
        'alg':'v3',
        #下面四个参数虽然会变化,但是写死也没什么影响,实测不写都可以
        'sig': 'SXFidmJBL3NzS0lNaERoL1pzckVxaFdSS0lrSDQ5SUNPWFdjUDRhVE5rZkVOMEpzN2FpZExBTDRFejEwOFBvdw==',
        'elapsed': 18,
        'shaOne': '0011f17de7b0a1b99aa70a6f1ea881c14d404379',
    	'callback': 'bd__cbs__1sgowp',
    }
    #获取二维码地址的url
    pass_url = 'https://passport.baidu.com/v2/api/getqrcode?lp=pc&qrloginfrom=pc'
    #这里返回的是标准json格式,所以可以调用json方法而无需使用text方法
    response = requests.get(url = pass_url,headers = headers).json()
    #获取channel_id
    channel_id = response['sign']
    #拼接callback
    callback = "tangram_guid_" + str(get_time() + 1)
    #延迟0.5s,callback的时间戳与tt的时间戳相差在500左右
    time.sleep(0.5)
    #用于检查二维码扫描状态的参数
    check_params = {
     
        "channel_id":channel_id,
        "tpl":"mn",
        "gid":guideRandom(),
        "callback":callback,
        "apiver":"v3",
        #获取时间戳
        "tt":get_time(),
        #加不加2应该都没什么影响
        "_":get_time() + 2,
        }
    #拼接出完整的imgurl
    imgurl = "https://" + response['imgurl'].replace('\\','')
    #返回三个变量
    return imgurl,login_params,check_params

class Show(object):
    def __init__(self,imgurl):
        self.t = threading.Thread(target = self.get_img)
        self.t.setDaemon(True)
        self.imgurl = imgurl
        
    def get_img(self):
        res = requests.get(url = self.imgurl,headers = headers)
        f = open('baidu.jpg','wb')
        f.write(res.content)
        f.close()
        time.sleep(1)
        img = Image.open('baidu.jpg')
        img.show()
        for proc in psutil.process_iter():
            if proc.name() in ['Microsoft.Photos.exe','dllhost.exe']:
                self.proc = proc
                break
                
    def show(self):
        self.t.start()
                
    def close(self):
        self.proc.kill()
        
class Login(object):
    def __init__(self,login_params,check_params,show):
        self.show = show
        self.check_url = 'https://passport.baidu.com/channel/unicast'
        self.login_url = 'https://passport.baidu.com/v3/login/main/qrbdusslogin'
        self.login_params = login_params
        self.check_params = check_params
        
    def check_status(self):
        self.show.show()
        errno = 1
        status = 1
        flag = True
        pattern = re.compile('({.*})')
        while True:
            req = requests.get(url = self.check_url,headers = passport_headers,cookies = cookie,params = self.check_params,timeout = 35)
            response = req.text.replace('\\','').replace('"{',"{").replace('}"',"}")
            try:
                if errno or status:
                    message = json.loads(re.search(pattern,response).group())
                    errno = message['errno']
                    if errno == 0 and flag:
                        self.show.close()
                        flag = False
                    elif errno == 0:
                        status = message['channel_v']['status']
                if not errno and not status:
                    message = json.loads(re.search(pattern,response).group())
                    self.login_params['bduss'] = message['channel_v']['v']
                    break
                self.check_params['tt'] = get_time()
                self.check_params['_'] = self.check_params['tt'] + 2
            except Exception as e:
                print(e)
                break
            
    def login(self):
        login_r = requests.get(self.login_url, headers=headers, params=self.login_params,cookies = cookie)
        for key in login_r.cookies.keys():
            cookie[key] = login_r.cookies[key]
        print('登陆成功,正在保存cookie')
        f = open('auth_cookie.txt','wt')
        f.write(str(cookie))
        f.close()
        print('保存完毕')

#如果不想继续使用获取的cookie,可以使用这个函数      
def logout():
    #百度退出登录的url
    logout_url = 'https://passport.baidu.com/?logout'
    #从文本中读取cookie
    f = open('auth_cookie.txt','r',encoding='utf-8')
    logout_cookie = f.read()
    f.close()
    #转换为json格式。注意!!!如果文本中是单引号,要将其替换为双引号
    logout_cookie = json.loads(logout_cookie.replace("'","\""))
    #发起post请求,原本的cookie将失去作用
    requests.post(url = logout_url,headers = headers,cookies = logout_cookie)
    print('已退出登录')
        
def run():
    #获取三个重要变量
    imgurl,login_params,check_params = GetParams()
    #声明一个对象控制二维码的显示
    show = Show(imgurl)
    #将两个参数和show传递给Login类,获取用于控制登录的对象
    login = Login(login_params,check_params,show)
    #调用类中的方法,检查二维码扫描状态
    login.check_status()
    #调用类中方法获取cookie
    login.login()

#入口函数
if __name__ == '__main__':
    #logout()
    run()

写在后面

花了整整一天,暂时把百度的二维码登录搞明白了,这篇文章中的代码可能不久就会失效,我大概是不会再更新,不,是肯定不会。


这是爬虫小白对二维码登录的一次尝试,如有不足之处,恳请各位大佬批评指正。

你可能感兴趣的:(笔记,Python)