# 作为一名大学生,每次最烦恼的就是选课的时候选课系统巨卡无比,而且有些选课轮次还是先到先得,这让抢课开放时还在认真学习导致网页打不开,登陆不上的我如何是好?但是作为一名爱好者,我们不谈外挂,只是尽量去模拟这个浏览器和人的行为,去尝试自动化的替我们选课。
一、如何实现选课
选课无非就是在浏览器中打开网页,输入账号和密码,有时还有验证码,点击登录后,在一些课程里去选择我心仪的那一门课,点击选择提交。那么这一些列的行为又是如何传递到后端服务器的呢?其实只要你打开开发者工具,就会看到很多的请求在你进行一些列操作的时候进行数据的收发,所以只要能模拟出这些请求,然后将其中的一些信息进行替换,在加上一些定时辅助手段,在规定的时间运行我们的脚本。
二、找出请求
# 本次示例使用的选课系统由http://xk.ynu.edu.cn/xsxkapp/sys/xsxkapp/*default/index.do
独家赞助。
(一)登录
打开网页,打开开发者工具,随便输入一个账号密码,捕获带有login关键字的那条请求。查看标头你会发现这是一个get请求,再看载荷,timestrap,loginName,loginPwd,verifyCode,vtoken,一共就这几个。不难发现timestrap就是时间戳,loginName和loginPwd对应你的账号密码,verifyCode则是你刚刚输入的的验证码,那么这个vtoken又是什么呢?其实我们换个方式就很容易推测出来了,后端如何校验我输入的验证码是不是正确的呢?所以当我们点击验证码更换验证码时会再次多出一条请求,在这条请求的响应中就有token这样一条,形式一致。之后还有一条图片请求其中就有这个token。再次输入账号密码验证码登录,捕获新的请求,再看载荷,vtoken就是刚刚获得新的token。至此,登录模拟分析完成!
(二)选课
在成功后我们进入选课页面,随便选一个可以选的课,捕获该选课请求,发现这是一个post的请求,载荷是
addParam:
{"data":{"operationType":"1","studentCode":"********","electiveBatchCode":"a0d5fd9c11b64029879ba9d65db507ea","teachingClassId":"*********","isMajor":"1","campus":"05","teachingClassType":"FANKC"}}
studentCode 就是学生账号即学生ID,teachingClassId就是课程标识号,刷新可选课程的界面抓一条数据就可以得到,FANKC是课程类别,a0d5fd9c11b64029879ba9d65db507ea是选课轮次有关的。FANKC和a0d5fd9c11b64029879ba9d65db507ea如果是只是在特定轮次和类别才使用的话可以写死。这条请求的数据是{"data":null,"code":"1","msg":"添加选课志愿成功","timestamp":"1719886397774"}格式的,可以对msg进行校验,判断选课是否成功和查看选课信息。
三、python实现
(一)模拟登录
构造登录请求不难,可以直接将该请求以cmd格式复制,然后找一个提供转换的站点,直接转为python的代码。
这里注意,每次的验证码都是动态更新的,所以我们需要三个请求,一个请求得到一个新的验证码token,一个是请求得到验证码,另外一个进行登录请求。为了追求代码的美观和简化的话可以对header进行一定简化和合并。在这里我用正则表达式获取了登录请求的的cookie的一部分值,是为了为后续的选课请求做准备。登录获得的_WEU和JSESSIONID才能让后端正确识别和处理我的请求。这里的验证码识别使用ddddocr。
def get_token():
"""
获取一个token
用于请求验证码和登录时载荷,校验验证码
:return:
"""
headers = {
# 这里的cookie是为了登良获取一个token,这里为了方便,直接写死也行
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cookie": "_WEU=zGqZ8jwIRhXYuPUV62BK9h4m7CPP0bU6qKccdYd6R4L.; JSESSIONID=LXkqgcNSE_Lh8jB2SDbPkWNFwA7K8sRSRsmUKmN1P74NPK2pr3W1!-1276073466",
"Proxy-Connection": "keep-alive",
"Referer": "http://xk.ynu.edu.cn/",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"X-Requested-With": "XMLHttpRequest"
}
url = "http://xk.ynu.edu.cn/xsxkapp/sys/xsxkapp/student/4/vcode.do"
params = {
"timestamp": str(int(time.time() * 1000))
}
response = requests.get(url, headers=headers, params=params, verify=False)
return json.loads(response.text)['data']['token']
def get_sign(vt):
"""
获取验证码
利用ddddocr识别验证码
:return:
"""
headers = {
"Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cookie": "_WEU=Cxnbl8nIYhBmODczGWL0dP_hpKp5BiQV00j0UEJXnzDUEhXGUgB_WPD4RZ3fNKvGdCUuGc4s6dP.; JSESSIONID=LXkqgcNSE_Lh8jB2SDbPkWNFwA7K8sRSRsmUKmN1P74NPK2pr3W1!-1276073466",
"Proxy-Connection": "keep-alive",
"Referer": "http://xk.ynu.edu.cn/",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
}
url = "http://xk.ynu.edu.cn/xsxkapp/sys/xsxkapp/student/vcode/image.do"
params = {
"vtoken": vt
}
response = requests.get(url, headers=headers, params=params, verify=False)
Docr = ddddocr.DdddOcr()
return Docr.classification(response.content)
def login(tk, sign):
"""
登录获得token
:param tk:
:param sign:
:return:
"""
headers = {
"Accept": "*/*",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cookie": "_WEU=Cxnbl8nIYhBmODczGWL0dP_hpKp5BiQV00j0UEJXnzDUEhXGUgB_WPD4RZ3fNKvGdCUuGc4s6dP.; JSESSIONID=LXkqgcNSE_Lh8jB2SDbPkWNFwA7K8sRSRsmUKmN1P74NPK2pr3W1!-1276073466",
"Proxy-Connection": "keep-alive",
"Referer": "http://xk.ynu.edu.cn/",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"X-Requested-With": "XMLHttpRequest"
}
url = "http://xk.ynu.edu.cn/xsxkapp/sys/xsxkapp/student/check/login.do"
params = {
"timestrap": str(int(time.time() * 1000)),
"loginName": "loginName",
"loginPwd": "loginPwd",
"verifyCode": sign,
"vtoken": tk
}
response = requests.get(url, headers=headers, params=params, verify=False)
# 获取登录返回的cookie
_WEU = re.findall(r'_WEU=(.*?);', response.headers['Set-Cookie'])[0]
JSESSIONID = re.findall(r'JSESSIONID=(.*?);', response.headers['Set-Cookie'])[0]
return json.loads(response.text)['data']['token'], [_WEU, JSESSIONID]
(二)模拟选课
提前准备好需要选的课程的课程号,然后同理构造请求。cookie的值需要根据上述请求的值进行更新,注意替换studentCode。
def get_course(tk, teachingClassId, ck, teachingClassType):
"""
:param teachingClassType:
:param ck:
:param tk:
:param teachingClassId:
:return:
"""
headers = {
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Language": "zh-CN,zh;q=0.9",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Cookie": f"_WEU={ck[0]}; JSESSIONID={ck[1]}",
"Origin": "http://xk.ynu.edu.cn",
"Proxy-Connection": "keep-alive",
"Referer": "http://xk.ynu.edu.cn/xsxkapp/sys/xsxkapp/*default/curriculavariable.do?token=1db3fe41-9f8b-45c3-b80b-8ad96f998952",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"X-Requested-With": "XMLHttpRequest",
"token": tk
}
url = "http://xk.ynu.edu.cn/xsxkapp/sys/xsxkapp/elective/volunteer.do"
data = {
"addParam": f'{{"data":{{"operationType":"1","studentCode":"your_studentCode","electiveBatchCode":"a0d5fd9c11b64029879ba9d65db507ea","teachingClassId":"{teachingClassId}","isMajor":"1","campus":"05","teachingClassType":{teachingClassType},"chooseVolunteer":"1"}}}}'
}
response = requests.post(url, headers=headers, data=data, verify=False)
msg = json.loads(response.text)['msg']
print(msg)
(三)模拟设计
可以事先获取需要抢的的课程号以及对应的信息,可以设置列表,利用循环对每个课程号进行请求,请求次数和判断成功与否可以对msg的内容进行判定。建议一轮或者是多次失败可以直接退出,在外套的循环中重新从登录开始。如果是需要定时的话可以利用下面的代码,几点开始的话我们提前几秒就开始模拟。
while True:
now = datetime.datetime.now()
if now.hour == 8 and now.minute == 59:
start()
break
else:
time.sleep(20)