项目由来
创新港羽毛球场馆每天的预定需求较大。在线预定系统每天早上 8:40
左右开放,并且开放时间不定。这些因素给手动预定场馆带来了很大的麻烦,因此考虑由脚本自动化实现场馆预定过程。
项目构思
体育场馆预定系统需要通过交大身份认证系统来确认身份。考虑携带登录之后的身份信息来访问体育场馆预定系统。这里的身份认证参考了果果的图书馆订座脚本:
XJTU图书馆抢座位脚本--requests库
| 果果的博客 (gwyxjtu.github.io)
此外,体育场馆预定系统开放时间为
8:40-21:40,并非全天开放。因此需要进行连通性检测来进行预定控制。
项目流程架构如下所示:
检测连通性确认体育场馆预定系统是否开放
通过体育场馆查询api获取所有场馆信息
设定一定的条件来筛选出需要预定的场地
通过交大身份认证获得身份信息
向场馆预定系统发送预定post请求来进行预定。
项目实现
1. 连通性检测
使用requests
库里的urlopen
库来进行连通性检测:
1 2 3 4 5 6 def check_net (testserver ): try : ret = request.urlopen(url=testserver, timeout=3.0 ) except : return False return True
在主函数里的检测等待代码如下:
1 2 3 while True : if check_net('http://202.117.17.144/' ): break
2. 查询场馆信息
首先构建一个session
来进行后面的访问,通过session
访问可以保证访问的连续型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import requestsfrom urllib3.util.retry import Retryfrom requests.adapters import HTTPAdapterMAX_RETRIES = 10 SLEEP_INTERVAL=0.1 retries=Retry( total=MAX_RETRIES, backoff_factor=SLEEP_INTERVAL, status_forcelist=[403 , 500 , 502 , 503 , 504 ], ) session = requests.Session() session.mount("http://" , HTTPAdapter(max_retries=retries)) session.mount("https://" , HTTPAdapter(max_retries=retries)) headers = { "User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36 Edg/98.0.1108.62" , "Accept-Encoding" : "gzip, deflate" , }
接下来就是查询场馆信息了。以创新港乒乓球场馆为例(因为未被预定的场地较多,便于测试
^_^)。
通过观察链接以及进行测试,可以推测出每个场馆通过一个 id
来进行标识,后续的场馆信息查询和预定申请都需要用到该信息。
通过查看页面请求,找到向 /findtime.html
发送
GET
请求可以查询到需要查询的信息。
但是该请求返回的信息是不完全的,有一个场次 id
没有被返回到,而这个场次 id
在之后的预定中是要用到的,经过多番查找,最终在网球场预定页面找到如下api
:
从该 api
中可以找到三个关键信息:
id 每个场地的每个时间场次的标识符
status 场地状态,1为可预订,2为已被预订
stockid 每个场地的标识符
由此,便可以通过该 api
查询每个场馆的场地信息。实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 def get_avaliable_seats (court_id,date ): params={'s_date' :date, 'serviceid' :court_id} response=session.get('http://202.117.17.144/product/findOkArea.html' ,params=params,headers=headers).json() available_court_list=[] for item in response['object' ]: if item['status' ]==1 : available_court_list.append(item) return available_court_list
3. 筛选出需要的场地
这一步比较容易,直接在上一步得到的场次进行筛选即可。
1 2 3 4 5 6 def get_suitable_seats (seat_list,time ): result=[] for item in seat_list: if item['stock' ]['time_no' ]==time: result.append(item) return result
4. 通过交大认证获得身份信息
这里参考了果果的图书馆订座脚本XJTU图书馆抢座位脚本--requests库
| 果果的博客
(gwyxjtu.github.io) 。唯一不同之处在于,登录身份认证系统后跳转的应用不同。在浏览器中通过体育场馆预定系统跳转到身份认证页面,查询页面cookie即可得到相应的appid
将对应的 appid
填充到相应位置,便可进行登录和跳转。登录部分代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 def encrypt_pwd (raw_pwd, publicKey='0725@pwdorgopenp' ): ''' AES-ECB encrypt ''' publicKey = publicKey.encode('utf-8' ) BS = AES.block_size pad = lambda s: s + (BS - len (s) % BS) * chr (BS - len (s) % BS) pwd = pad(raw_pwd) ''' pwd = raw_pwd while len(raw_pwd.encode('utf-8')) % 16 != 0: pwd += '\0' ''' cipher = AES.new(publicKey, AES.MODE_ECB) pwd = cipher.encrypt(pwd.encode('utf-8' )) return str (base64.b64encode(pwd), encoding='utf-8' ) def login (): session.get('https://org.xjtu.edu.cn/openplatform/login.html' ) r_JcaptchaCode = session.post('https://org.xjtu.edu.cn/openplatform/g/admin/getJcaptchaCode' , headers=headers) url = 'https://org.xjtu.edu.cn/openplatform/g/admin/getIsShowJcaptchaCode' params = { 'userName' : config['username' ], '_' : str (int (time.time() * 1000 )) } r = session.get(url, params=params, headers=headers) url = 'https://org.xjtu.edu.cn/openplatform/g/admin/login' cookie = { 'cur_appId_' :'n0C/SQT28fY=' } data = { "loginType" : 1 , "username" : config['username' ], "pwd" : encrypt_pwd(config['password' ]), "jcaptchaCode" : "" } headers['Content-Type' ] = 'application/json;charset=UTF-8' r = session.post(url, data=json.dumps(data), headers=headers,cookies=cookie) print (r.text) print ('身份认证成功,正在跳转...' ) token = json.loads(r.text)['data' ]['tokenKey' ] memberId=json.loads(r.text)['data' ]['orgInfo' ]['memberId' ] cookie = { 'cur_appId_' :'n0C/SQT28fY=' , 'open_Platform_User' : token, 'memberId' : str (memberId) } r=session.get('http://org.xjtu.edu.cn/openplatform/oauth/auth/getRedirectUrl?userType=1&personNo=3121154016&_=1590998261976' ,cookies = cookie) r=session.get(json.loads(r.text)['data' ])
5. 预定场馆
最后一步就是预定场地了,手动预定场馆的流程如下:
选择场地
点击确认场地按钮
跳转到新的页面确认场地信息
点击继续预定按钮弹出验证码
输入验证码并确认
返回预定状态信息(成功或失败)
通过对这些流程中的页面请求进行查看和分析,关系到预定成功的只有两个过程:①获取并识别验证码,②带着验证码提交一个预定post
。
还是以乒乓球场地的预定页面为例,其验证码生成方式如下:
也就是访问地址http://202.117.17.144/login/yzm.html?+(任意一个float随机数)即可得到验证码图像。
post
请求一步步通过函数嵌套即可进行定位:
可以看出,最终提交的post
请求地址为/order/book
,参数有两个,一个为预定信息转换成的字符串,一个是验证码。下面分别介绍如何获取这两个参数。
预定信息中有大量的参数,但是大量参数为null
,不用进行考虑。需要构造的有以下几个参数:
1 2 3 4 5 6 7 8 param={ "activityPrice" : 0 , "address" : '102 ', "extend" : { } , "flag" : "0" , "stock" : { '169111 ': '1 '} , "stockdetail" : { '169111 ': '1711756 '} , "stockdetailids" : '1711756 ' }
验证码部分通过一些在线识别的 api
进行在线验证即可,这个验证码比较简单,成功率还挺高的,本次项目所用的
api
地址为:https://aicode.my-youth.cn
至此,便可以成功对场馆进行预定,预定代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 def order (seat_info ): param={"activityPrice" :0 , "address" :seat_info['stock' ]['serviceid' ], "extend" :{}, "flag" :"0" , "stock" :{str (seat_info['stockid' ]):'1' }, "stockdetail" :{str (seat_info['stockid' ]):str (seat_info['id' ])}, "stockdetailids" :str (seat_info['id' ]) } img=session.get('http://202.117.17.144/login/yzm.html?0.16003635332777866' ) with open ('yzm.jpg?x-oss-process=style/webp' ,'wb' ) as f: f.write(img.content) f.close() with open ('yzm.jpg?x-oss-process=style/webp' ,'rb' ) as f: img_base64=str (base64.b64encode(f.read()))[2 :-1 ] f.close() headers['Content-Type' ]='application/x-www-form-urlencoded' headers['Origin' ]='https://aicode.my-youth.cn' response=session.post('https://aicode.my-youth.cn/base64img' ,data={'data' :"image/jpeg;base64," +img_base64},headers=headers) yzm=response.json()['data' ] headers['Content-Type' ]='application/x-www-form-urlencoded; charset=UTF-8' headers['Origin' ]='http://202.117.17.144' response=session.post('http://202.117.17.144/order/book.html' ,params={'id' :seat_info['stock' ]['serviceid' ]},data={'param' :str (param),'yzm' :yzm},headers=headers).json() return response
项目小结
在开发过程中也走了不少弯路。例如在预定阶段的实现中,研究了很久页面跳转的逻辑。但实际上只有最后一个页面发送的最后一次请求是有效的。之后在进行脚本编写时要注意以结果为导向而不是过程为导向。从而避免很多不必要的开发。