妈妈再也不用担心我忘记填报疫情——巧用云函数完成自动化数据填报 - Pinming's Blog

一个适用于某西北 985 高校「中国工科的第一大学」的疫情身体状况自动填报程序思路分享及其使用方法。

0x00 前言

在新冠疫情的背景之下,想必很多学校都要求学生每日申报自己的健康状况。但是日子一天天过去总有忘记的时候——轻则全班公开处刑,重则全院公开处刑,尴尬至极。后来接入了企业微信,如果不填每天中午 11:30 又准时来催你填……每天还得去对着表格整几下,实在有些麻烦,遂考虑制作一个自动化填报的程序来减轻自己的工作量。

本项目 Github 仓库:
如果 Github 访问异常,请浏览 Gitee 镜像。

请注意 / 在正文开始之前

本文「0x02 本地先行调试」中的操作建立于你能够独立在计算机配置 Python 环境的前提之上。
你也可以直接在云端调试,这样便不需要在自己的计算机上配置 Python 环境。

警告 / 请认真阅读本部分

  • 本软件设计之本意为技术学习,请在遵循法律及学校各项规定的前提下使用本软件。
  • 如您需要使用该软件,请确保您的身体状况良好,如实申报自身身体状况。
  • 若您的身体状况出现异常,应立即停止使用本软件、关闭云函数自动触发功能,并及时于学校系统更改每日申报情况。
  • 因使用该软件误报身体状况而引发的不良后果应由您自行承担。
  • 本软件原理是提取上一次的填报结果来提交,如果您的所在地发生改变,请自行手动填报一次,理论上程序会自动跟进后续的填报并与之同步。如出现异常烦请反馈!
  • 该软件并非万能,请时常检查填报结果!

另:由于程序开发之初使用了国家现行最新的行政区划代码,故地理位置数据 location.py 可能与学校系统有部分出入,这有可能导致申报异常。如您在运行时发现提示:

1
2
获取上一次填报的信息时出现错误!可能是本程序地理位置数据库中的数据有误。
请联系作者(通过 Github Issue 或邮箱:i@pm-z.tech)并附上信息填报网站「个人中心→我的打卡」页面的截图,便于定位问题!

烦请带上所要申报的地理位置进行反馈,多谢!
如发现这样的异常,程序会自动阻断申报过程,防止错误的数据被上传至学校。

0x01 代码实现

文中涉及的全局变量:

1
2
3
4
5
6
url_Form = 'http://yqtb.nwpu.edu.cn/wx/ry/jrsb.jsp' # 获取表格并进行操作
url_Submit = 'http://yqtb.nwpu.edu.cn/wx/ry/ry_util.jsp' # 用于 POST 申报的内容
url_for_id = 'https://uis.nwpu.edu.cn/cas/login' # 用于 Validate 登录状态
url_for_user_info = 'http://yqtb.nwpu.edu.cn/wx/ry/jbxx_v.jsp' # 「个人信息」一栏
url_for_list = 'http://yqtb.nwpu.edu.cn/wx/xg/yz-mobile/rzxx_list.jsp'
url_for_yqtb_logon = 'http://yqtb.nwpu.edu.cn//sso/login.jsp'

1.1 整体流程分析

要实现信息的填报,可以发现全流程大体需要经过四个步骤:CAS 登录传递 Cookie 到疫情填报系统获取上一次填报的 Form提交 Form。接下来将分步说明每个步骤的实现。

1.2 模拟 CAS 登录

首先登入填报系统的主页:https://yqtb.nwpu.edu.cn。发现在未登录时,其跳转至 https://uis.nwpu.edu.cn/cas/login?service=http%3A%2F%2Fyqtb.nwpu.edu.cn%2F%2Fsso%2Flogin.jsp%3FtargetUrl%3Dbase64aHR0cDovL3lxdGIubndwdS5lZHUuY24vL3d4L3hnL3l6LW1vYmlsZS9pbmRleC5qc3A%3D 这一页面,该页面用于填入用户名及密码,这些信息将被打包 POST 出去,由 UIS 接受后跳转到 yqtb 的 service。

为了更容易地获取登录状态,从 CAS 登录网址 url_for_id 无跳转地发起登录,直接获得登录状态,与 Form 的 submit 过程分开。

于是首先建立一个 session 并对登录页发起 get:

1
2
session = requests.Session()
session.get(url_for_id)

通过 Chrome 抓包发现,在 uis.nwpu.edu.cn登录时,CAS 首先接到第一组 POST,同时前端生成了 SESSION 作为第一个 cookie:

提取以上信息,打包第一组 headerdata,然后对 url_for_id 模拟 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
header = {
'referer': url_for_id,
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_16_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.26 Safari/537.36',
'content-Type': 'application/x-www-form-urlencoded',
'origin': 'https://uis.nwpu.edu.cn',
'cookie': 'SESSION=' + str((session.cookies.values()[0])),
# [0] 是生成的 `SESSION`
'authority': 'uis.nwpu.edu.cn',
'path': '/cas/login',
'scheme': 'https',
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'upgrade-insecure-requests': '1',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'same-origin',
'sec-fetch-user': '?1',
'cache-control': 'no-cache'
}
data = {
'username': username,
'password': password,
'_eventId': 'submit',
'currentMenu': '1',
'execution': 'e2s1',
'submit': 'One moment please...',
'geolocation': '',
}

之后 url_for_id 会如图返回登录状态。如果登录成功成功则返回文字内容:欢迎使用 统一身份认证 系统。这里直接在网页中查找这个字段来判断登录是否成功:

2020 年 8 月 6 日翱翔门户进行了微调,返回登录状态页面只返回欢迎使用四个字。因此下方代码块第三行需要对应修改为:

1
if rt.find('欢迎使用') != -1:
1
2
3
4
5
6
7
rt = session.post(url_for_id, data=data, headers=header, timeout=5).text
# rt 以 html 形式返回登录状态
if rt.find('欢迎使用 统一身份认证 系统') != -1:
print('登录成功!')
else:
print('登录失败!请检查「登录信息」一栏用户名及密码是否正确')
exit()

至此我们已经成功登入学校 CAS 系统,接下来需要到 yqtb.nwpu.edu.cn 进行健康状况的申报。

1.3 传递登录信息到疫情填报页面

在来到疫情填报页面前,我们还需要观察 url_for_id 在登录成功后返回的 cookie:它携带了此前生成的第一个 cookie SESSION,且又生成了第二个 cookie TGC

利用 session.cookies.values() 来得到已经获取的 cookie 列表,分别存入 header。

紧接着访问疫情填报页面 url_Form,发现又生成了第三个 cookie JSESSIONID。这里先将其储存到 header3,否则后续在 yqtb.nwpu.edu.cn 执行的步骤会因为缺少 cookie 传递而失败。

在代码中即是先伪造一次对 url_Form 的请求简单来说就是「我就蹭蹭不进去」,目的是为了「骗」到 JSESSIONID

至此,全流程所需的三个 cookie 都已经获取成功,这样就可以为接下来的 POST 所用。
这时可以正式登入疫情填报页面。通过抓包发现,这里需要此前获得的 TGC 作为 ticket 来从 UIS 跳转到 yqtb,同时 header 需要携带 JSESSIONID。故构造 header3data2 来 POST:

1
2
3
4
5
6
7
8
9
10
11
12
header3 = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_16_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.26 Safari/537.36',
'Host': 'yqtb.nwpu.edu.cn',
'cookie': 'JSESSIONID=' + str((session.cookies.values()[2])),
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
'upgrade-insecure-requests': '1',
'cache-control': 'no-cache'
}
data2 = {
'ticket': str((session.cookies.values()[1])),
'targetUrl': 'base64aHR0cDovL3lxdGIubndwdS5lZHUuY24vL3d4L3hnL3l6LW1vYmlsZS9pbmRleC5qc3A=',
}

然后完成 yqtb 系统的登录:

1
r_log_on_yqtb2 = session.post(url_for_yqtb_logon, data=data2, headers=header3)

之后就可以获取其他的信息了。

1.4 获取 Form 信息

先手动在浏览器上完成一次健康申报过程,抓包观察 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
HeadersForm = {
'Host': 'yqtb.nwpu.edu.cn',
'Origin': 'http://yqtb.nwpu.edu.cn',
'Referer': 'http://yqtb.nwpu.edu.cn/wx/ry/jrsb.jsp',
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Cookie': 'JSESSIONID=' + str((session.cookies.values()[2])),
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_16_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.26 Safari/537.36'
}
tbDataForm = {
'sfczbcqca': '',
'czbcqcasjd': '',
'sfczbcfhyy': '',
'czbcfhyysjd': '',
'actionType': 'addRbxx',
'userLoginId': username,
'fxzt': '9',
'userType': '2',
'userName': RealName, # 真实姓名;实践表明可留空,以防万一填上,下同
'szcsbm': loc_code_str, # 所在城市编码
'szcsmc': str(loc_name), # 所在城市名称
'sfjt': '0', # 是否经停
'sfjtsm': '', # 是否经停说明
'sfjcry': '0', # 是否接触人员
'sfjcrysm': '', # 说明
'sfjcqz': '0', # 是否接触确诊
'sfyzz': '0', # 是否有症状
'sfqz': '0', # 是否确诊
'ycqksm': '',
'glqk': '0', # 隔离情况
'glksrq': '', # 隔离开始日期
'gljsrq': '', # 隔离结束日期
'tbly': 'sso', # 填报来源:SSO 单点登录
'glyy': '' , # 隔离原因
'qtqksm': '', # 其他情况说明
'sfjcqzsm': '',
'sfjkqk': '0',
'jkqksm': '', # 健康情况说明
'sfmtbg': '',
'qrlxzt': '',
'xymc': RealCollege, # 学院名称;实践表明可留空
'xssjhm': PhoneNumber, # 手机号码;实践表明可留空
}

这里面我们会发现一些变量需要动态地从疫情填报网站中获取,例如 userNameszcsbmszcsmc 等,接下来对这些数据的动态获取进行介绍。

1.4.1 个人填表信息的获取

这一部分主要使用 BeautifulSoup 解析前端代码中表明的相关信息,并用正则表达式来提取。
其中 r2 获取到姓名和学院;r3 获取到电话号码;r5 获取上次填报的所在地,同时由此进行对「在外地」、「在西安」、「在学校」三种情况的判断。

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
r2 = r_log_on_yqtb2.text
# 登录后跳转到主页 [//yqtb.nwpu.edu.cn/wx/xg/yz-mobile/index.jsp];r2 最终可以获取姓名和学院
soup = BeautifulSoup(r2, 'html.parser')
global RealCollege, RealName, PhoneNumber
for k in soup.find('div', string=re.compile('姓名')):
RealName = k.replace('姓名:', '')
for l in soup.find('div', string=re.compile('学院')):
RealCollege = l.replace('学院:', '')

## r3 操作
# 在「基本信息」页面 / 此页面也可获得上述 k,l 信息,之前未发现
r3 = session.post(url_for_user_info, data=data2, headers=header3).text
soup2 = BeautifulSoup(r3, 'html.parser')
m = soup2.find_all('span')
PhoneNumber = m[6].string # 提取出列表的 #6 值即为电话号码

## r5 操作:获得上一次填报的所在地
r5 = session.post(url_for_list, data=data2, headers=header3).text
soup3 = BeautifulSoup(r5, 'html.parser')
v_loc = soup3.find("span", attrs={"class": "status"}).string
global loc_name, loc_code_str
loc_name = v_loc
loc_code = location.GetLocation(loc_name)
if loc_name == '在西安' or loc_name == '在学校':
loc_code_str = '' # 这两种情况 loc_code 留空
else:
loc_code_str = loc_code[0] # 在西安外则需要有 loc_code
if loc_code_str == '' and loc_name != '在西安' and loc_name != '在学校':
print('获取上一次填报的信息时出现错误!可能是本程序地理位置数据库中的数据有误。' + '\n' + '请联系作者(通过 Github Issue 或邮箱:i@pm-z.tech)并附上信息填报网站「个人中心→我的打卡」页面的截图,便于定位问题!')
exit()

需要注意的是 loc_code 的获取,这个数据没有办法通过前端获取的 html 来解析,因此需要通过 location.GetLocation(loc_name) 来进行匹配。因为返回的是一个 list,所以还要把它转换为 str
当人在西安市内时,即 loc_name != '在西安' or loc_name != '在学校',这个值留空。

1.5 提交 Form 信息

通过上述操作,我们已经得到了填表所需要动态获取的各个变量:loc_code_strloc_nameRealNameRealCollegePhoneNumber。接下来向 url_Submit post 整个 form,也就是 1.4 部分中所提到的 form。

篇幅所限这里就省略了,看上面 1.4 吧。

1
2
HeadersForm = {...}
tbDataForm = {...}

POST 完以后,不出意外学校的服务器已经收到了我们填报的数据,接下来需要对结果进行分析。

1.6 结果反馈及 Email 发送

通过 r4 来 GET url_Form,确认是否成功上传数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
r4 = session.get(url=url_Form, data=data2, headers=header3).text
session.close()
if (r4.find('重新提交将覆盖上一次的信息')) != -1:
print('申报成功!')
if email_switcher == 1:
email_status = sendEmail()
if debug_switcher == 1:
print(f'Email Status: {email_status}')
if email_status != -1:
print('邮件已发送!')
else:
print('邮件发送失败!请检查 Email 的各项设置(用户名、密码、SMTP 服务器及端口、收件人)等设置填写是否正确!')
else:
print('申报失败,请重试!')

Email 的实现直接看 sendEmail() 就好了,这部分就纯套用了,确实没啥值得说的了。233333

0x02 本地先行调试 / 基础用法

计算机需要配置 Python 3.6 或以上版本的环境。

  1. 首先通过 git clone 或直接在 Github 上将整个程序打包下载下来,解压进行配置。
  2. 根据「0x03 程序配置」一节来配置程序。

程序包自身已经封装了所需的第三方库。
如果你还是需要在计算机上安装所需 Requirements,在 Terminal (CMD) 中 cd 到程序目录,通过 pip 安装依赖项:pip install -r requirements.txt

  1. 使用 PythonVSCode 等软件,在自己的机器上测试几遍。

之后就可以进行相关的部署了。

0x03 程序配置

该程序需要配置西工大翱翔门户账号、邮箱信息(可选)才能正确运行。

使该程序正确运行,需要编辑 main.py 中的部分变量,配置西工大翱翔门户账号、邮箱信息(可选)。其他文件无需改动。

main.py 需要设置的变量如下:

变量 说明
username 填入登录翱翔门户的用户名,通常为学工号
password 填入对应用户的密码
email_switcher Email 服务开关,默认开启服务,赋值为 1;填 0 则关闭
如果关闭了 Email 服务则不需要配置以下变量。
mail_host (可选)发送方的 SMTP 服务器,如果使用 163 邮箱则不必修改;其他邮箱请对应更改。
mail_SMTPPort (可选)发送方的 SMTP 端口号,如果使用 163 邮箱则不必修改;其他邮箱请对应更改。
mail_user (可选)发送方邮箱,格式为 ****@***.com
mail_pass (可选)发送方邮箱对应的密码。
receivers (可选)接收方邮箱;以 列表 (list) 形式存入。
如果留空,则默认发送至发件邮箱。

对于 receivers 参数,如果只有一个接收邮箱,按照以下形式填入:

1
['*******@***.com']

如果有多个接收邮箱,则按照以下形式填入:

1
['******1@***.com', '******2@***.com', '...']

其余文件不需要进行修改。

0x04 云端部署

这里以阿里云函数计算为例。

  1. 首先注册一个阿里云账号,然后在控制台中搜索并进入「函数计算」。
  2. 点击「立即创建函数」。
  3. 选择「事件函数」。
  4. 按照下图的设置配置函数。其中 所在服务服务函数 可以任填
  5. 出现弹窗,点击「同意授权」。
  6. 找到新建的函数,选择「代码执行」→「代码包上传」,将你已经配置好的程序打包为 zip,然后上传。生成的压缩包文件结构应如图。上传后点击「保存」。
  7. 点击「保存并执行」,进行一次测试,观察输出的结果
  8. 如果输出结果无问题,点击「触发器」→「创建触发器」,如图填入参数。触发器名称可任填,时间配置则在「时间间隔」中填入适当的时间间隔即可,然后点击「确定」。

程序触发的时间点是:北京时间每日 0 时起的每个「时间间隔」后。

至此便可以实现确定时间间隔的每日自动健康填报。

目前阿里云函数存在 bug,即每次函数执行可能重复。所以你可能会在同一时间收到两次邮件,这我暂时不知道怎么解决,可能是阿里云的锅吧。应该不会是我的锅吧,不会吧不会吧

收到的 Email 效果如下:

如果需要关闭云端的自动填报,在「触发器」菜单中关闭即可。

关于潜在的收费提示

在建立函数过程中,你可能会发现关于该功能的收费提示。
本函数的请求量、请求时间及耗费公网流量均极小,每天执行三四次水平的请求的花费可以说近似是 0(考虑到会产生公网流量,费用可能是 0 一直写到两位以上小数的水平),请放心。大不了别充钱嘛!
具体收费标准详见:https://help.aliyun.com/document_detail/54301.html

关于本地部署

你也可以通过 Windows 的「计划任务」或类似功能在本地计算机上定时执行该程序,方法不再赘述。
设计初衷还是为了云函数考虑的,这样更方便一些,不需要本地计算机挂机运行。

0xFF 后记

这算是我个人第一次爬虫的尝试吧…… 也算是又一次感受到了 Python 的魅力。将整个过程记录下来也算是一种学习和巩固了。

至于代码本身以及这篇文章写得是相当混乱,可读性是不咋滴【滑稽】,很可能逻辑上也不怎么简洁高效。也希望各位看官多多包涵了,欢迎批评指正!

后期应该会增加对于 ServerChan 微信推送的支持吧,毕竟这个比 Email 好用多了。

最后,还是期望全人类包括美国能够早日战胜 COVID-19,这个程序早一天失去它的用武之地吧~

评论



Powered by Hexo.

博客内容遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议

本站使用 Volantis 为主题 | 总访问量为
© Pinming 2019-2015 | All Rights Reserved.
载入天数...载入时分秒...
粤 ICP 备 19139605 号
粤公网安备 44030502004717 号