用 Python 实现中科大健康打卡脚本
新冠疫情期间,学校规定假期必须每天进行健康打卡,汇报自身各项情况,在开学前未中断且打满 14 天才可申请返校,而开学后虽然不管,但原则上仍需每天打卡、每周报备。
打卡?这辈子不可能手动打卡的,我决定写一个爬虫脚本来自动打卡。
登录
首先来分析一下打卡的登录逻辑:
- 打卡平台的网址是
https://weixine.ustc.edu.cn/2020/home
。 - 点进去发现其跳转到了
https://weixine.ustc.edu.cn/2020/login
,其中有一条 “统一身份认证登录”。 - 点击 “统一身份认证登录”,页面跳转到
https://passport.ustc.edu.cn/login?service=https%3A%2F%2Fweixine.ustc.edu.cn%2F2020%2Fcaslogin
,这是打卡平台在科大统一身份认证平台注册的 CAS 身份认证服务链接,我们在此需要输入科大 Passport 的账号密码,即可登录。
因此,从这个逻辑可以得到,我们可以向上面第 3 点中的 CAS 身份认证 URL 发送包含登录信息的 POST 数据包,来实现登录。不过,事实上只要我们先在会话中登录了 https://passport.ustc.edu.cn
,即中科大身份认证系统,再对 CAS 认证 URL 直接发送 GET 请求,可以达到相同的效果,为了降低耦合,我选择了后面一种登录方法。
因此,最终的登录逻辑化为以下两步:
- 向
https://passport.ustc.edu.cn/login
发送学号、密码等字段信息,使会话登录上中科大身份认证系统。 - 直接 GET 请求 CAS 认证链接:
https://passport.ustc.edu.cn/login?service=https%3A%2F%2Fweixine.ustc.edu.cn%2F2020%2Fcaslogin
进行打卡平台的 CAS 认证。
登录界面如下:
接下来,在浏览器的 F12 界面中,对中科大身份认证系统的登录过程进行抓包:
发现登录过程向登录链接 POST 了不少内容,多试几次容易知道,其中的 model、service、warn、button 等参数都是固定的,showCode 参数表示是否需要验证码,然而,直接把 showCode 取为空串就可以绕过验证码。username 和 password 即学号、密码,用于校内身份认证(这里需要吐槽一下学校的身份认证系统居然还在使用明文传输密码,造成了很大的安全隐患)。
另外,还有一个貌似临时凭证的 CAS_LT 参数,初看不容易摸索出它的规律,但实际上,CAS_LT 正藏在 passport.ustc.edu.cn/login
这个网页中,如下图:
可以使用 BeautifulSoup 通过 id 把它找出来。
打卡
登录成功以后,我们对打卡系统的 CAS 链接:https://passport.ustc.edu.cn/login?service=https%3A%2F%2Fweixine.ustc.edu.cn%2F2020%2Fcaslogin
发起一个 GET 请求,即可跳转到打卡网页:https://weixine.ustc.edu.cn/2020/home
,我们先手动打一下卡,看看打卡系统是如何在请求中标识用户身份的。
在 Network 选项卡下的众多内容中,有一条名为 daliy_report 的(真不是我不会拼 daily 这个单词),其提交表单部分内容如下:
上面省略了一部分表单的内容,但容易发现,有一条内容明显与其他内容不同,就是这个_token。短期内多打几次卡,可以发现表单的_token 不会发生变化,但重新登录以后,_token 则会发生变化,很显然,它用于用户身份的标识,即告诉打卡平台的服务端这条打卡内容是来自哪个同学。既然_token 出现在表单内容里,那大概率它就藏在网页的表单当中,找了一下,发现还真有:
那么身份标识的问题就解决了,顺便我们也把打卡的过程研究了一遍,其实就是提交这么一个表单到 https://weixine.ustc.edu.cn/2020/daliy_report
。
代码
接下来开始着手写代码,首先实现登录过程。下面先定义 passport 登录链接。
self.passport = "https://passport.ustc.edu.cn/login"
然后,由于前面这些请求之间并非独立的,而是依赖于共同的 cookie,因此必须在同一个会话中发起,不能直接用 requests 自带的 GET、POST 方法来完成请求,所以我们需要先建立一个会话(requests.Session 对象),使用会话的 GET、POST 方法。会话会主动维护一个 cookie 字典。
self.sess = requests.session()
然后定义 login 的主函数:
def login(self, username, password):
"""
登录,需要提供用户名、密码
"""
self.sess.cookies.clear()
CAS_LT = self._get_cas_lt()
login_data = {
'username': username,
'password': password,
'warn': '',
'CAS_LT': CAS_LT,
'showCode': '',
'button': '',
'model': 'uplogin.jsp',
'service': ''
}
self.sess.post(self.passport, login_data, allow_redirects=False)
return self.sess.cookies.get("uc") == username
逻辑非常简单,首先把会话的 cookie 清空,然后通过一个函数获取前文提到的 CAS_LT 参数,并构造 POST 表单,调用 Session 的 POST 方法,把它提交给 passport 登录链接,如果登录成功,会话的 cookie 中会多出一条键为”uc”、值为登录 username 的键值对,可以通过它来判断是否登录成功。
接下来,来完善前面前面提到的函数:
def _get_cas_lt(self):
"""
获取登录时需要提供的验证字段
"""
response = self.sess.get(self.passport)
CAS_LT = BeautifulSoup(response.text, 'html.parser').find(attrs={'id': 'CAS_LT'}).get('value')
return CAS_LT
上述流程用 Session 去 GET 请求 passport 登录链接,在返回的 html 中即可获取到 CAS_LT。
以上即是登录过程的代码,接下来给出打卡的代码:
self.login_bot = USTCPassportLogin()
self.sess = self.login_bot.sess
# CAS身份认证url
self.cas_url = 'https://passport.ustc.edu.cn/login?service=https%3A%2F%2Fweixine.ustc.edu.cn%2F2020%2Fcaslogin'
# 打卡url
self.clock_in_url = 'https://weixine.ustc.edu.cn/2020/daliy_report'
self.token = ''
健康打卡需要两个 URL,第一个是打卡平台 CAS 身份认证的 URL,在登录成功以后,对此 URL 进行请求,以完成 CAS 认证;第二个是打卡链接。同时,初始化一个 login_bot,即为前面定义的登录类。最后初始化一个空字符串作为未登录状态下的 token。
打卡系统的登录过程如下:
def login(self, username, password):
"""
登录,需要提供用户名、密码
"""
self.token = ''
is_success = self.login_bot.login(username, password)
if is_success:
self.token = self._get_token()
return is_success
若登录成功了,则通过下面的_get_token 方法获取到 token。
def _get_token(self):
"""
获取打卡时需要提供的验证字段
"""
response = self.sess.get(self.cas_url)
s = BeautifulSoup(response.text, 'html.parser')
token = s.find(attrs={'name': '_token'}).get('value')
return token
在获取了 token 以后,我们终于可以进行打卡了:
def daily_clock_in(self, post_data_file):
"""
打卡函数,需要提供包含表单内容的json文件
打卡成功返回True,打卡失败返回False
"""
with open(post_data_file, 'r') as f:
post_data = json.loads(f.read())
post_data['_token'] = self.token
response = self.sess.post(self.clock_in_url, data=post_data)
return self._check_success(response)
这里我们从一个 JSON 文件读取打卡需要的表单,然后在字典中加入_token,对打卡的 URL 发起 POST 请求即可,最后通过下面的_check_success 方法,来检查是否成功打卡。
def _check_success(self, response):
"""
简单check一下有没有成功打卡、报备
"""
s = BeautifulSoup(response.text, 'html.parser')
msg = s.select('.alert')[0].text
return '成功' in msg
这是由于:
综上,我们已经完成了一个健康打卡的脚本,同理也可以实现每周的出校报备,完整的代码见我的 GitHub 项目:
最后,此脚本仅供学习,希望大家为自己和他人的健康负责,在自身健康状态良好的情况下合理使用脚本,切勿上报不实信息!