欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

python3下使用requests模拟用户登录 —— 中级篇(百度云俱乐部)

程序员文章站 2024-03-16 14:25:58
...

python3下使用requests模拟用户登录 —— 中级篇(百度云俱乐部)

1. 背景

2. 环境

  • 系统:win7
  • python 3.6.1
  • requests 2.14.2 (通过pip list查看)

3. 模拟登录百度云俱乐部

3.1. 分析用户登录必要的信息

  • 值得一提的是,在分析这种复杂登录请求时,需要保存登录过程中,所有的页面请求和响应情况,这样才方便找到所有的登录参数,回溯查找,寻找来源。 如果之前的信息丢失了,那重新打开的请求和上一次请求的参数是对不上的,就不方便查找数据来源了。
    python3下使用requests模拟用户登录 —— 中级篇(百度云俱乐部)

  • 点击登录,提取到的信息如下:

# General:
Request URL:http://www.51baiduyun.com/member.php?mod=logging&action=login&loginsubmit=yes&handlekey=login&loginhash=LyqBH&inajax=1
Request Method:POST
Status Code:200 OK
Remote Address:47.91.148.25:80
Referrer Policy:no-referrer-when-downgrade

# Request Headers
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding:gzip, deflate
Accept-Language:zh-CN,zh;q=0.9
Cache-Control:max-age=0
Connection:keep-alive
Content-Length:216
Content-Type:application/x-www-form-urlencoded
Cookie:L3em_2132_saltkey=geeeP9NE; L3em_2132_lastvisit=1521177080; UM_distinctid=1622d6bb6c589-07e7bc1d9d570b-454c092b-1fa400-1622d6bb6c6609; L3em_2132_lastcheckfeed=1315026%7C1521180699; L3em_2132_nofavfid=1; L3em_2132_ulastactivity=406fKQfd1L5Bpyfb%2BwxJVkSKEP%2FeA%2FE0EKi0tL5iIiN0sPTQgPK7; L3em_2132_smile=1D1; Hm_lvt_79316e5471828e6e10f2df47721ce150=1521508740,1521515152,1521516204,1521712944; Hm_lvt_eaefab1768d285abfc718a706c1164f3=1521508740,1521515152,1521516204,1521712944; L3em_2132_st_p=0%7C1521785655%7C706b59df71621dff8c64747f9df9e6a1; L3em_2132_visitedfid=38D66D68D37D44D41D45; L3em_2132_viewid=tid_480044; CNZZDATA1253365484=727141784-1521177990-null%7C1521783379; CNZZDATA1253863031=1430494274-1521176587-null%7C1521787718; Hm_lpvt_79316e5471828e6e10f2df47721ce150=1521787672; Hm_lpvt_eaefab1768d285abfc718a706c1164f3=1521787672; L3em_2132_sid=AHqj37; L3em_2132_seccode=79666.06fdbff670d881c68f; L3em_2132_lastact=1521787976%09misc.php%09seccode
Host:www.51baiduyun.com
Origin:http://www.51baiduyun.com
Referer:http://www.51baiduyun.com/
Upgrade-Insecure-Requests:1
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36

# Query String Parameters
mod:logging
action:login
loginsubmit:yes
handlekey:login
loginhash:LyqBH
inajax:1

# Form Data
formhash:7736cc00
referer:http://www.51baiduyun.com/
loginfield:username
username:aaaaaaaaaa
password:bbb12345678
questionid:0
answer:
seccodehash:cSAAHqj37
seccodemodid:member::logging
seccodeverify:eB3X
  • 我们尝试用不同的用户名登陆几次,还记得之前说过,使用错误的密码登录吧,防止登陆成功,造成页面跳转,干扰分析。结果发现有几个主要的登录参数需要特别留意,每次都会不同,如下所示:
    • formhash
    • username:用户名
    • password:密码
    • seccodehash
    • seccodeverify:验证码
  • 其中 formhash 和 seccodehash的来源目前还不明朗,seccodeverify的获取也需要技巧,接着看往下…

3.2. 寻找 formhash

  • 像这种类型的参数,一般来说有几个来源:
    • 第一,网页源代码中携带。
    • 第二,藏在服务器返回过来的cookie中。
    • 第三,网页js脚本计算生成。
    • 第四,登录时,请求js脚本生成,或者同步产生的其他网页中。
  • 比较幸运的是,直接在登录页的源代码中找到了(同时找到的还有referer):
    python3下使用requests模拟用户登录 —— 中级篇(百度云俱乐部)
# Request URL:http://www.51baiduyun.com/member.php?mod=logging&action=login&infloat=yes&handlekey=login&inajax=1&ajaxtarget=fwin_content_login

<form method="post" autocomplete="off" name="login" id="loginform_LyqBH" class="cl" onsubmit="pwdclear = 1;ajaxpost('loginform_LyqBH', 'returnmessage_LyqBH', 'returnmessage_LyqBH', 'onerror');return false;" action="member.php?mod=logging&amp;action=login&amp;loginsubmit=yes&amp;handlekey=login&amp;loginhash=LyqBH">
<div class="c cl">
<input type="hidden" name="formhash" value="7736cc00" />
<input type="hidden" name="referer" value="http://www.51baiduyun.com/" />
<div class="rfm">

3.3. 寻找 seccodehash

  • 这个我是在cookie中找到的。
    python3下使用requests模拟用户登录 —— 中级篇(百度云俱乐部)
# 很明显能看到,是按如下方式构造的:
# seccodehash:cSAAHqj37
cSA + cookie中的 AHqj37
  • 经过分析,formhash 和 seccodehash的提取方案如下:
# 从网页源码和cookie中拿到登录所需的参数
def getLoginArgs():
    # 首先第一步,从网页源码中获得 formhash
    response = baiduyunSession.get("http://www.51baiduyun.com/member.php?mod=logging&action=login&infloat=yes&handlekey=login&inajax=1&ajaxtarget=fwin_content_login", headers=header)
    # print(f"statusCode = {response.status_code}, text = {response.text}")
    print(f"statusCode = {response.status_code}")
    '''
        <div class="c cl">
        <input type="hidden" name="formhash" value="7736cc00" />
        <input type="hidden" name="referer" value="http://www.51baiduyun.com/" />
    '''
    # formhashRe = re.search('name="formhash" value="(.*?)"', response.text, re.DOTALL)
    formhashRe = re.search('name="formhash" value="(\w+?)"', response.text, re.DOTALL)
    refererRe = re.search('name="referer" value="(.*?)"', response.text, re.DOTALL)
    print(f"formhashRe = {formhashRe}, refererRe = {refererRe}")
    if formhashRe:
        formhash = formhashRe.group(1)
    else:
        formhash = ""
    if refererRe:
        referer = refererRe.group(1)
    else:
        referer = ""

    # 第二步,从cookie中获得 seccodehash
    cookieGet = baiduyunSession.cookies
    # print(f"cookieGet = {cookieGet}")
    seccodehash = ""
    for item in cookieGet:
        print(f"itemName = {item.name}, itemValue = {item.value}")
        if item.name.find('_sid') != -1:
            seccodehash = 'cSA' + item.value
    return {'formhash':formhash, 'referer':referer, 'seccodehash':seccodehash}

3.4. 探索验证码seccodeverify

  • 关于如何打码,请参考文章:
  • 验证码的获取是需要一点小技巧的,因为验证码可以刷新,每次都可以重新获取到。所以我们点击” 刷新验证码 “,看看新的验证码是如何获取到的:
    python3下使用requests模拟用户登录 —— 中级篇(百度云俱乐部)
  • 可以看到产生了两个请求。我们找到返回验证码的这个请求,看看是什么样的请求。
    python3下使用requests模拟用户登录 —— 中级篇(百度云俱乐部)
# General:
Request URL:http://www.51baiduyun.com/misc.php?mod=seccode&update=66158&idhash=cSAAHqj37
Request Method:GET
Status Code:200 OK
Remote Address:47.91.148.25:80
Referrer Policy:no-referrer-when-downgrade

# Query String Parameters
mod:seccode
update:66158
idhash:cSAAHqj37
  • 多了一个未知的参数:update:66158。按照3.2节中的思路,发现这个参数来自于 跟它一起发出的另一个请求。看一下这个请求的信息:
    python3下使用requests模拟用户登录 —— 中级篇(百度云俱乐部)
    • 这个请求的信息就比较简单了:
# General:
Request URL:http://www.51baiduyun.com/misc.php?mod=seccode&action=update&idhash=cSAAHqj37&0.8088204604265181&modid=undefined
Request Method:GET
Status Code:200 OK
Remote Address:47.91.148.25:80
Referrer Policy:no-referrer-when-downgrade

# Query String Parameters
mod:seccode
action:update
idhash:cSAAHqj37
0.8088204604265181:
modid:undefined
  • 这里面同样有一个未知的参数0.8088204604265181,不过直觉告诉我,这只是一个随机数,用于生成验证码的随机数,不需要什么特别的算法,可以由程序直接生成…后面的登录结果也证实了我的这个想法。

  • 这个刷新验证码的流程就比较清晰了

    • 第一步:发送第一个请求,去获得update这个参数。
    • 第二步:使用这个update参数,去获取真实的验证码图片。
# 刷新验证码
def get_captcha(seccodehash):
    # 第一步:发送第一个请求,获取“update” 的参数值
    import random
    randomFloat = random.uniform(0, 1)
    url = f"http://www.51baiduyun.com/misc.php?mod=seccode&action=update&idhash={seccodehash}&{randomFloat}&modid=undefined"
    response = baiduyunSession.get(url = url, headers = header)
    print(f"statusCode = {response.status_code}, text = {response.text}")
    # src="misc.php?mod=seccode&update=22285&idhash=cSAD3vAf3"
    if response.status_code == 200:
        updateRe = re.search('update=(\d+?)&', response.text, re.DOTALL)
        print(f"updateRe = {updateRe}")
        if updateRe:
            update = int(updateRe.group(1))
        else:
            update = 0
        print(f"update = {update}")

    # 第二步:拿到update参数之后,用这些参数去请求验证码,然后保存成一张图片
    # http://www.51baiduyun.com/misc.php?mod=seccode&update=88800&idhash=cSAY3fpK6
    captchaUrl = f"http://www.51baiduyun.com/misc.php?mod=seccode&update={update}&idhash={seccodehash}"
    t = baiduyunSession.get(captchaUrl, headers=header)
    # print(f"t = {t.text}")    # 打印结果可以看出是一张图片
    with open("captcha51baiduyun.jpg", "wb") as f:
        f.write(t.content)
        f.close()

    # 在这里,为了让逻辑简单,暂时采用手动输入验证码的方式。
    # 如果想让程序自动打码,可以参考文章:https://blog.csdn.net/zwq912318834/article/details/78616462
    from PIL import Image
    try:
        imObj = Image.open('captcha51baiduyun.jpg')
        imObj.show()
        imObj.close()
    except:
        pass
    captcha = input("输入验证码\n>")
    return captcha

3.5. 最终,带着所有的数据信息,进行模拟登录。

  • 在这里尤其需要注意到一点,因为这些参数都有着千丝万缕的联系,也就是属于同一个会话中,所以我们需要全程使用session进行请求,不能使用requests。
  • 整个代码如下:
# -*- coding: utf-8 -*-

import requests
import re

# python2 和 python3的兼容代码
try:
    # python2 中
    import cookielib
    print(f"user cookielib in python2.")
except:
    # python3 中
    import http.cookiejar as cookielib
    print(f"user cookielib in python3.")

# session代表某一次连接
baiduyunSession = requests.session()
# 因为原始的session.cookies 没有save()方法,所以需要用到cookielib中的方法LWPCookieJar,这个类实例化的cookie对象,就可以直接调用save方法。
baiduyunSession.cookies = cookielib.LWPCookieJar(filename = "baiduyunCookies.txt")

userAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
header = {
    "Referer": "http://www.51baiduyun.com/",
    'User-Agent': userAgent,
}

# 从网页源码和cookie中拿到登录所需的参数
def getLoginArgs():
    # 首先第一步,从网页源码中获得 formhash
    response = baiduyunSession.get("http://www.51baiduyun.com/member.php?mod=logging&action=login&infloat=yes&handlekey=login&inajax=1&ajaxtarget=fwin_content_login", headers=header)
    # print(f"statusCode = {response.status_code}, text = {response.text}")
    print(f"statusCode = {response.status_code}")
    '''
        <div class="c cl">
        <input type="hidden" name="formhash" value="7736cc00" />
        <input type="hidden" name="referer" value="http://www.51baiduyun.com/" />
    '''
    # formhashRe = re.search('name="formhash" value="(.*?)"', response.text, re.DOTALL)
    formhashRe = re.search('name="formhash" value="(\w+?)"', response.text, re.DOTALL)
    refererRe = re.search('name="referer" value="(.*?)"', response.text, re.DOTALL)
    print(f"formhashRe = {formhashRe}, refererRe = {refererRe}")
    if formhashRe:
        formhash = formhashRe.group(1)
    else:
        formhash = ""
    if refererRe:
        referer = refererRe.group(1)
    else:
        referer = ""

    # 第二步,从cookie中获得 seccodehash
    cookieGet = baiduyunSession.cookies
    # print(f"cookieGet = {cookieGet}")
    seccodehash = ""
    for item in cookieGet:
        print(f"itemName = {item.name}, itemValue = {item.value}")
        if item.name.find('_sid') != -1:
            seccodehash = 'cSA' + item.value
    return {'formhash':formhash, 'referer':referer, 'seccodehash':seccodehash}


# 刷新验证码
def get_captcha(seccodehash):
    # 第一步:发送第一个请求,获取“update” 的参数值
    import random
    randomFloat = random.uniform(0, 1)
    url = f"http://www.51baiduyun.com/misc.php?mod=seccode&action=update&idhash={seccodehash}&{randomFloat}&modid=undefined"
    response = baiduyunSession.get(url = url, headers = header)
    print(f"statusCode = {response.status_code}")
    # src="misc.php?mod=seccode&update=22285&idhash=cSAD3vAf3"
    if response.status_code == 200:
        updateRe = re.search('update=(\d+?)&', response.text, re.DOTALL)
        print(f"updateRe = {updateRe}")
        if updateRe:
            update = int(updateRe.group(1))
        else:
            update = 0
        print(f"update = {update}")

    # 第二步:拿到update参数之后,用这些参数去请求验证码,然后保存成一张图片
    # http://www.51baiduyun.com/misc.php?mod=seccode&update=88800&idhash=cSAY3fpK6
    captchaUrl = f"http://www.51baiduyun.com/misc.php?mod=seccode&update={update}&idhash={seccodehash}"
    t = baiduyunSession.get(captchaUrl, headers=header)
    # print(f"t = {t.text}")    # 打印结果可以看出是一张图片
    with open("captcha51baiduyun.jpg", "wb") as f:
        f.write(t.content)
        f.close()

    # 在这里,为了让逻辑简单,暂时采用手动输入验证码的方式。
    # 如果想让程序自动打码,可以参考文章:https://blog.csdn.net/zwq912318834/article/details/78616462
    from PIL import Image
    try:
        imObj = Image.open('captcha51baiduyun.jpg')
        imObj.show()
        imObj.close()
    except:
        pass
    captcha = input("输入验证码\n>")
    return captcha


def baiduyunLogin(account, password, argsData, captcha):
    # 百度云模仿 登录
    print("开始模拟登录百度云俱乐部")
    postUrl = "http://www.51baiduyun.com/member.php?mod=logging&action=login&loginsubmit=yes&handlekey=login&loginhash=Lpd1b&inajax=1"
    '''
        formhash:eb6fc0ed
        referer:http://www.51baiduyun.com/
        loginfield:username
        username:aaaaaa
        password:abc123456
        questionid:0
        answer:
        seccodehash:cSAY3fpK6
        seccodemodid:member::logging
        seccodeverify:ejwe
    '''
    postData = {
        "formhash": argsData['formhash'],
        "referer": argsData['referer'],
        "loginfield": 'username',
        "username": account,
        "password": password,
        "questionid": '0',
        "answer": '',
        "seccodemodid": 'member::logging',
        "seccodeverify": captcha,
    }
    # 使用session直接post请求
    responseRes = baiduyunSession.post(postUrl, data = postData, headers = header)
    # 无论是否登录成功,状态码一般都是 statusCode = 200
    print(f"statusCode = {responseRes.status_code}")
    print(f"text = {responseRes.text}")
    # 登录成功之后,将cookie保存在本地文件中,好处是,以后再去获取马蜂窝首页的时候,就不需要再走baiduyunLogin的流程了,因为已经从文件中拿到cookie了
    baiduyunSession.cookies.save()

def isLoginStatus():
    # 通过访问个人中心页面的返回状态码来判断是否为登录状态
    # 参考模拟登录篇获取做法,这儿略过
    pass


if __name__ == "__main__":
    # # 从返回结果来看,有登录成功
    account = "13756567832"
    password = "000000001"
    argsData = getLoginArgs()
    print(f"argsData = {argsData}")
    captcha = get_captcha(argsData['seccodehash'])
    baiduyunLogin(account, password, argsData, captcha)
  • 登录成功的结果:
E:\Miniconda\python.exe E:/PyCharmCode/ArticleSpider/ArticleSpider/utils/51baiduyunLogin.py
user cookielib in python3.
statusCode = 200
formhashRe = <_sre.SRE_Match object; span=(650, 682), match='name="formhash" value="9da3d01e"'>, refererRe = <_sre.SRE_Match object; span=(708, 757), match='name="referer" value="http://www.51baiduyun.com/">
itemName = L3em_2132_lastact, itemValue = 1521791100%09member.php%09logging
itemName = L3em_2132_lastvisit, itemValue = 1521787500
itemName = L3em_2132_saltkey, itemValue = RbtB6p6O
itemName = L3em_2132_sid, itemValue = xJn0kL
argsData = {'formhash': '9da3d01e', 'referer': 'http://www.51baiduyun.com/', 'seccodehash': 'cSAxJn0kL'}
statusCode = 200
updateRe = <_sre.SRE_Match object; span=(1079, 1092), match='update=30204&'>
update = 30204
输入验证码
>E9CJ
开始模拟登录百度云俱乐部
statusCode = 200
text = <?xml version="1.0" encoding="utf-8"?>
<root><![CDATA[<script type="text/javascript" reload="1">if(typeof succeedhandle_login=='function') {succeedhandle_login('http://www.51baiduyun.com/', '欢迎您回来,见习会员 13756567832,现在将转入登录前页面', {'username':'13756567832','usergroup':'见习会员','uid':'1315026','groupid':'23','syn':'0'});}hideWindow('login');showDialog('欢迎您回来,见习会员 13756567832,现在将转入登录前页面', 'right', null, function () { window.location.href ='http://www.51baiduyun.com/'; }, 0, null, null, null, null, null, 3);</script>]]></root>
  • 验证码错误的结果:
updateRe = <_sre.SRE_Match object; span=(1079, 1092), match='update=90864&'>
update = 90864
输入验证码
>1111
开始模拟登录百度云俱乐部
statusCode = 200
text = <?xml version="1.0" encoding="utf-8"?>
<root><![CDATA[抱歉,验证码填写错误<script type="text/javascript" reload="1">if(typeof errorhandle_login=='function') {errorhandle_login('抱歉,验证码填写错误', {});}</script>]]></root>
  • 用户名或密码错误的结果:
update = 72482
输入验证码
>CPWK
开始模拟登录百度云俱乐部
statusCode = 200
text = <?xml version="1.0" encoding="utf-8"?>
<root><![CDATA[登录失败,您还可以尝试 4 次<script type="text/javascript" reload="1">if(typeof errorhandle_login=='function') {errorhandle_login('登录失败,您还可以尝试 4 次', {'loginperm':'4'});}</script>]]></root>

4. 重点说明

  • 在上面刷新验证码时,有个非常关键的地方,就是使用session,而不是requests,但是具体的原因是什么呢?
    • 首先我们需要知道session,一次session就是一次会话,如果一开始我通过session访问一个网站,后面再拿这个session再去请求这个网站,它实际上会把这个网站带给我们的cookie,或者说网站放到header里面的session等信息,完全的给我们带过去,这里面的cookie非常重要,因为在访问百度云俱乐部的时候,不管我们有没有登录,网站都会在我们的header里面放一些值,这个cookie里面就包含了很多值,包括我们登录,请求验证码等操作要用到的一些参数,比如说L3em_2132_sid。
    • 我们需要明白的是为什么浏览器能实现用户登录,就是在我们访问百度云俱乐部的时候,服务器给我们放了一些值,存在浏览器中(cookie中),这个时候我们再去请求验证码图片,会原模原样的把这些值带给服务器,这个图片请求url,在后台里面会跟这些信息做一个关联,那新的验证码图片就会自动和cookie中的信息关联起来。
    • 如果我们不用session去请求的话,我们拿request去请求时,实际上会单独再建立一个连接(session的会话),这两个session之间,cookie值是不一样的,这样这些参数就和新拿到的验证码图片就不匹配了。
    • 所以只有session能保证登录成功。