厦大自动健康打卡脚本

受疫情影响,学校每天需要登陆教务系统进行健康打卡,作为一名沉迷学习无法自拔的科研人(纯粗懒得忘记了),每日被辅导员@提醒打开,所以萌发了写个自动打卡脚本的想法。
研三时针对师大打卡系统开发过一版,这次大同小异,细节处借鉴了git上这个项目auto-daily-health-report在此致谢。
功能上要简单很多。但增加了每日邮箱推送来避免打卡失败
学校居然出了反自动打卡系统,所以对代码进行了一些优化,通过随机时间进行打卡,并且打卡失败后 会间隔1~2分钟随机时间再进行打卡,最大打卡次数不超过20次,来降低打卡的请求次数避免被抓。
然后苹果自带的邮箱系统不能实时抓取邮件提醒,所以改用了微信推送打卡信息,和apple watch联动,简直绝了。

环境:centos

正文

项目的邮件推送功能需要两个邮箱,一个发送一个接收,这里我用QQ邮箱发送,学校邮箱接收的。
项目挂在私人服务器上,设定好脚本,每天8.30准时打卡(辅导员再也不用担心我的打卡了)

依赖包引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
import json
import sys
import time
import SetEmail
import base64
import random
from bs4 import BeautifulSoup
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad

http_header = {
'User-Agent': 'Mozilla/5.png.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/81.0.4044.138 Safari/537.36',
'Referer': 'https://xmuxg.xmu.edu.cn/xmu/login?app=214'
}

工具类与请求头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def randstr(num):
H = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
salt = ''
for i in range(num):
salt += random.choice(H)
return salt

def encryptAES(data: str, salt: str):
salt = salt.encode('utf-8')
iv = randstr(16).encode('utf-8')
cipher = AES.new(salt, AES.MODE_CBC, iv)
data = randstr(64) + data
data = data.encode('utf-8')
data = pad(data, 16, 'pkcs7')
cipher_text = cipher.encrypt(data)
encoded64 = str(base64.encodebytes(cipher_text), encoding='utf-8').replace("\n", "")
return encoded64

程序入口

程序分三部分执行:

  1. 模拟登陆
  2. 健康打卡
  3. 邮件推送
    打卡后会判断是否打卡成功,如果没有则会间隔随机半分钟后再去尝试,如果20小时后还没打卡成功则将失败信息通过邮件方式推送,打卡成功也会把成功信息推送到邮箱。

首先给个执行的shell脚本

1
/home/hjz/.conda/envs/python3.7/bin/python main.py --EW Wechat --to_user ****** --username ******* --password ******

主程序

新建一个main.py文件
获取参数

1
2
3
4
5
parser = argparse.ArgumentParser()
parser.add_argument('--to_user', type=str, default='******', help='email or Wechat token')
parser.add_argument('--username', type=str, default='******', help='username')
parser.add_argument('--password', type=str, default='******', help='password')
parser.add_argument('--EW', type=str, default='Email', help='Email or Wechat')

主程序入口main函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if __name__ == '__main__':
time.sleep(int(random.random()*300)) # 程序随机暂停10分钟内再执行
args = parser.parse_args()
session = requests.Session() # 获取session
log = login(session,args.username,args.password) # 登陆
t = 1
while log['status']!='True' and t<20: # 登陆不成功 尝试20次
time.sleep(int(random.random()*30)) # 半分钟内随机时常访问一次
t += 1
log = login(session, args.username, args.password)
if log['status']!='True': # 尝试20次依旧没有登陆成功 发送邮件登陆失败
setMessege(args,'登陆失败',str(json['reason']))
else:
res = check_health(session) # 打卡
t = 1
while (res== None or res[0]['status']=='failed') and t < 30: # 打卡失败再次尝试 尝试30次
time.sleep(int(random.random()*30)) # 间隔半分钟内随机时常再试
t += 1
res = check_health(session) # 打卡
if res== None or res[0]['status']=='failed': # 打卡失败
setMessege(args, '打卡失败', str(res[0]['reason']))
else:
setMessege(args, '打卡成功', time.strftime("%Y-%m-%d %H:%M:%S ", time.localtime())+'打卡成功')

主程序中消息发送函数
1
2
3
4
5
def setMessege(args,content,log):
if args.EW == 'Email':
SetEmail.mail(time.strftime("%Y-%m-%d %H:%M:%S ", time.localtime())+ content+'\n' +log+'\n',args.to_user)
else:
SetMessegeToWechat.send_notice(content,log, args.to_user)

模拟登陆

想要打卡首先需要登陆教务系统,代码如下

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 login(session,username,password):
try:
loginUrl = 'https://ids.xmu.edu.cn/authserver/login?service=https://xmuxg.xmu.edu.cn/login/cas/xmu' # 登陆URL
resp = session.get(loginUrl,headers=http_header)
soup = BeautifulSoup(resp.text, 'html.parser')
lt = soup.select('input[name="lt"]')[0]["value"]
dllt = soup.select('input[name="dllt"]')[0]['value']
execution = soup.select('input[name="execution"]')[0]['value']
salt = soup.select('input#pwdDefaultEncryptSalt')[0]['value']

login_data = {
"username": username,
"password": encryptAES(password, salt),
"lt": lt,
"dllt": dllt,
"execution": execution,
"_eventId": "submit",
"rmShown": 1
}

session.post(loginUrl, login_data,
headers=http_header,
allow_redirects=True) # will redirect to https://xmuxg.xmu.edu.cn
return {
"status": "True",
"reason": "Login success"
}

except KeyError:
return {
"status": "failed",
"reason": "Login failed (server error)"
}

健康打卡

登陆成功后cookie会缓存到session里面,这时候直接用session去请求打卡页面获得表单,然后填入适当信息发送post请求即可

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def check_health(session):
# 获得健康打卡
now_url = 'https://xmuxg.xmu.edu.cn/api/app/214/business/now'
try:
resp = session.get(now_url).text
form_id = str(json.loads(resp)['data'][0]['business']['id'])
except Exception:
return ({
"status": "failed",
"reason": "Login failed (incorrect auth info or captcha required)"
}, 1)

# 获得表单信息
form_url='https://xmuxg.xmu.edu.cn/api/formEngine/business/%s/formRenderData?playerId=owner'% form_id
try:
resp = session.get(form_url, headers=http_header).text
form_components = json.loads(resp)["data"]["components"]
except Exception:
return ({
"status": "failed",
"reason": "Internal server error (logged in but cannot get form id)"
}, 1)

# get owner modification
form_instance_url = "https://xmuxg.xmu.edu.cn/api/formEngine/business/%s/myFormInstance" % form_id
resp = session.get(form_instance_url, headers=http_header).text
form_json = json.loads(resp)["data"]
instance_id = form_json["id"]

value_list = {}
for (k, v) in enumerate(form_json["formData"]):
name = v['name']
hide = v['hide']
title = v['title']
value = {}

if "学生本人是否填写" in title:
value['stringValue'] = '是'
elif "Can you hereby declare that" in title:
value['stringValue'] = '是 Yes'
elif v['value']['dataType'] == 'STRING':
value['stringValue'] = v['value']['stringValue']
elif v['value']['dataType'] == 'ADDRESS_VALUE':
value['addressValue'] = v['value']['addressValue']

value_list[name] = {
'hide': hide,
'title': title,
'value': value
}

# prepare post data
post_array = []
for item in form_components:
name = item['name']
if name in value_list:
hide = True if value_list[name]['hide'] else False
if 'select' in name and 'stringValue' in value_list[name]['value'] and value_list[name]['value'][
'stringValue'] == "":
hide = True
post_array.append({
'name': name,
'title': value_list[name]['title'],
'value': value_list[name]['value'],
'hide': hide
})
else:
post_array.append({
'name': name,
'title': item['title'],
'value': {},
'hide': True if 'label' not in name else False,
})

# post change
post_modify_url = "https://xmuxg.xmu.edu.cn/api/formEngine/formInstance/" + instance_id
post_json = {
"formData": post_array,
"playerId": "owner"
}
post_json_str = json.dumps(post_json, ensure_ascii=False)
http_header['Content-Type'] = 'application/json'
http_header['Referer'] = 'https://xmuxg.xmu.edu.cn/app/214'
resp = session.post(post_modify_url, headers=http_header, data=post_json_str.encode('utf-8'))

return ({
"status": "success",
"info": "automatically checked in successfully.",
"name": form_json["owner"]["name"]
}, 0)

邮件推送

新建一个SetEmail.py文件
需要两个邮箱

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
import smtplib
from email.mime.text import MIMEText
from email.utils import formataddr

def mail(String, to_user): # to_user 收件人邮箱账号,我这边发送给自己
my_sender = '********' # 发件人邮箱账号
my_pass = '********' # 发件人邮箱密码(当时申请smtp给的口令)
f = open('log.txt','a') # 保存下日志
try:
msg=MIMEText(String,'plain','utf-8')
msg['From']=formataddr(["hdc",my_sender]) # 括号里的对应发件人邮箱昵称、发件人邮箱账号
msg['To']=formataddr(["hdc",to_user]) # 括号里的对应收件人邮箱昵称、收件人邮箱账号
msg['Subject']="健康打卡" # 邮件的主题,也可以说是标题

server=smtplib.SMTP_SSL("smtp.qq.com", 465) # 发件人邮箱中的SMTP服务器,端口是465
server.login(my_sender, my_pass) # 括号中对应的是发件人邮箱账号、邮箱密码
server.sendmail(my_sender,[to_user,],msg.as_string()) # 括号中对应的是发件人邮箱账号、收件人邮箱账号、发送邮件
server.quit()# 关闭连接
f.write('send success '+String)
f.flush()
f.close()
except Exception:# 如果 try 中的语句没有执行,则会执行下面的 ret=False
f.write('send false '+String)
f.flush()
f.close()

微信推送

新建一个SetMessegeToWechat.py文件
用于微信推送 to_user在网站绑定微信获取

1
2
3
4
5
6
7
import requests
import argparse
import time

def send_notice(title, content, to_user):
url = f"http://www.pushplus.plus/send?token={to_user}&title={title}&content={content}&template=html"
response = requests.request("GET", url)

自动执行脚本

在服务器端设置每天8.30自动打卡
采用的crontab,具体配置如下

  1. 安装python环境
    conda create -n py36 python=3.6
  2. 安装依赖
    1
    2
    3
    pip install requests
    pip install bs4
    pip install pycryptodomex
  3. 对crontab进行编辑
    crontab -e
    输入指令,我这是设置的每天早上9点执行一次
    0 9 * * * /.../python /.../main.py #注意要用绝对路径
    启动服务
    service crond start
    重新载入crond服务
    service crond reload
  4. 开启crontab日志
    sudo vim /etc/rsyslog.d/50-default.conf
    去掉cron.* /var/log/cron.log 前面的#
    sudo service rsyslog restart
    sudo service cron restart
    cat /var/log/cron.log

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!