新冠疫情期间,学校规定假期必须每天进行健康打卡,汇报自身各项情况,在开学前未中断且打满 14 天才可申请返校,而开学后虽然不管,但原则上仍需每天打卡、每周报备。

打卡?这辈子不可能手动打卡的,我决定写一个爬虫脚本来自动打卡。

登录

首先来分析一下打卡的登录逻辑:

  1. 打卡平台的网址是 https://weixine.ustc.edu.cn/2020/home
  2. 点进去发现其跳转到了 https://weixine.ustc.edu.cn/2020/login,其中有一条 “统一身份认证登录”。
  3. 点击 “统一身份认证登录”,页面跳转到 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 请求,可以达到相同的效果,为了降低耦合,我选择了后面一种登录方法。

因此,最终的登录逻辑化为以下两步:

  1. https://passport.ustc.edu.cn/login 发送学号、密码等字段信息,使会话登录上中科大身份认证系统。
  2. 直接 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 登录链接。

python
self.passport = "https://passport.ustc.edu.cn/login"

然后,由于前面这些请求之间并非独立的,而是依赖于共同的 cookie,因此必须在同一个会话中发起,不能直接用 requests 自带的 GET、POST 方法来完成请求,所以我们需要先建立一个会话(requests.Session 对象),使用会话的 GET、POST 方法。会话会主动维护一个 cookie 字典。

python
self.sess = requests.session()

然后定义 login 的主函数:

python
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 的键值对,可以通过它来判断是否登录成功。

接下来,来完善前面前面提到的函数:

python
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。

以上即是登录过程的代码,接下来给出打卡的代码:

python
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。

打卡系统的登录过程如下:

python
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。

python
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 以后,我们终于可以进行打卡了:

python
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 方法,来检查是否成功打卡。

python
def _check_success(self, response):
    """
    简单check一下有没有成功打卡、报备
    """
    s = BeautifulSoup(response.text, 'html.parser')
    msg = s.select('.alert')[0].text
    return '成功' in msg

这是由于:


综上,我们已经完成了一个健康打卡的脚本,同理也可以实现每周的出校报备,完整的代码见我的 GitHub 项目:

最后,此脚本仅供学习,希望大家为自己和他人的健康负责,在自身健康状态良好的情况下合理使用脚本,切勿上报不实信息!