├── .gitignore
├── README.assets
├── cookies_browser.jpg
├── cookies_pc1.jpg
├── cookies_pc2.jpg
├── cookies_phone.jpg
├── example.jpg
└── example_manual.jpg
├── README.md
├── report(manual).py
├── report(manual)_cookies.py
├── report.py
├── report_cookies.py
└── utils
├── httpRequestUtil.py
├── navigator.py
└── user.py
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | *.ini
3 | /build
4 | /dist
5 | *.spec
6 | pack.txt
7 | __pycache__
8 | *.bat
--------------------------------------------------------------------------------
/README.assets/cookies_browser.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/barryZZJ/ucas_cronavirus_report/433b6fc54427ddccac6ba4ad9b11d24a77cb5d19/README.assets/cookies_browser.jpg
--------------------------------------------------------------------------------
/README.assets/cookies_pc1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/barryZZJ/ucas_cronavirus_report/433b6fc54427ddccac6ba4ad9b11d24a77cb5d19/README.assets/cookies_pc1.jpg
--------------------------------------------------------------------------------
/README.assets/cookies_pc2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/barryZZJ/ucas_cronavirus_report/433b6fc54427ddccac6ba4ad9b11d24a77cb5d19/README.assets/cookies_pc2.jpg
--------------------------------------------------------------------------------
/README.assets/cookies_phone.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/barryZZJ/ucas_cronavirus_report/433b6fc54427ddccac6ba4ad9b11d24a77cb5d19/README.assets/cookies_phone.jpg
--------------------------------------------------------------------------------
/README.assets/example.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/barryZZJ/ucas_cronavirus_report/433b6fc54427ddccac6ba4ad9b11d24a77cb5d19/README.assets/example.jpg
--------------------------------------------------------------------------------
/README.assets/example_manual.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/barryZZJ/ucas_cronavirus_report/433b6fc54427ddccac6ba4ad9b11d24a77cb5d19/README.assets/example_manual.jpg
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ucas_cronavirus_report
2 |
3 | 适用于国科大企业微信平台的疫情每日打卡脚本。
4 |
5 | **原理:**
6 |
7 | 基于上次打卡记录构造本次打卡记录,因此仅适用于无异常情况时的打卡,如打卡信息有变动请手动打卡。(注:考虑到最近“昨日是否接受核酸检测”一项会经常变更,故新增了在昨日记录的基础上,支持手动修改该项的脚本,详见[脚本入口说明](#脚本入口说明))
8 |
9 | **运行演示:**
10 |
11 | | 自动打卡(账号密码登录) | 可手动修改参数(cookies登录) |
12 | | -------------------------------- | --------------------------------------- |
13 | |  |  |
14 |
15 | **注意:**
16 |
17 | 由于脚本需要登录sep系统,故(只会在本地)用明文存储用户名和密码。
18 |
19 | 如果担心安全问题或无法正常登录,也提供了使用cookies登陆的方法,详见[脚本入口说明](#脚本入口说明)。
20 |
21 | 获取cookies的方法见[获取cookies](#获取cookies)。
22 |
23 | ## 目录
24 |
25 | 1. [脚本入口说明](#脚本入口说明)
26 | 2. [运行脚本](#运行脚本)
27 | 3. [用户信息配置](#用户信息配置)
28 | 4. [实现自动化](#实现自动化)
29 |
30 | ## 脚本入口说明
31 |
32 |
33 |
34 |
35 | 数据构造方式 |
36 | 登陆方式 |
37 | 脚本名 |
38 | 备注 |
39 |
40 |
41 |
42 |
43 | 自动复制昨日报告 |
44 | 使用账号密码登录 |
45 | report.py |
46 | |
47 |
48 |
49 | 使用cookies登录 |
50 | report_cookies.py |
51 | |
52 |
53 |
54 | 可手动修改参数 |
55 | 使用账号密码登录 |
56 | report(manual).py |
57 | 目前只支持修改“昨日是否接受核酸检测”,其他参数修改请手动填报。 |
58 |
59 |
60 | 使用cookies登录 |
61 | report(manual)_cookies.py |
62 |
63 |
64 |
65 |
66 | 对于可手动修改参数的脚本,可以在调用时传入参数`y`或`n`,则不需要在执行过程中手动输入该参数,方便实现自动化。
67 |
68 | ## 运行脚本
69 |
70 | ### 用可执行文件运行
71 |
72 | 本脚本已打包成exe可执行程序,请在[releases](https://github.com/barryZZJ/ucas_cronavirus_report/releases)页面下载。
73 |
74 | ### 用python3运行
75 | 1. 配置环境
76 |
77 | ```
78 | pip install requests
79 | pip install easydict
80 | ```
81 |
82 | 2. - 运行入口脚本(用户名密码登录)
83 |
84 | ```sh
85 | python3 <入口脚本名>.py
86 | ```
87 |
88 | - 对于含`manual`的脚本,可提前传入参数值`y`或`n`表示昨日是否接受核酸检测
89 |
90 | ```sh
91 | python3 report(manual).py y
92 | ```
93 |
94 | ## 用户信息配置
95 |
96 | 首次运行时会要求输入sep系统用户名和密码(或cookies),并生成ini配置文件。
97 |
98 | 如果不小心输入了信息,可以修改`user.ini`(或`user_cookies.ini`)中的信息,或直接删除ini文件,重新运行脚本。
99 |
100 | ### 获取cookies
101 |
102 | 打卡系统需要`eai-sess`和`UUKey`两个cookie。
103 |
104 | 可以使用抓包软件获取,或手动从浏览器中获取。
105 |
106 |
107 | 抓包软件获取cookies
108 |
109 | - 安装抓包软件:我使用的是fiddler classic,安装与使用教程请自行上网搜索,注意需要安装证书才能抓取https报文。
110 | -
111 |
112 | - 抓PC端的包:用浏览器打开https://app.ucas.ac.cn/uc/wap/login,抓取登录时的POST报文,既可获得两个cookie。如图:
113 |
114 |
115 |
116 |
117 |
118 | - 抓手机端的包:参考这篇博客分别配置好PC端和手机端后,在手机上点开国科大企业微信——A疫情防控,然后随便找一个域名是
app.ucas.ac.cn
的报文,就能看到所需的cookie了。如图:
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | 从浏览器中获取cookies
129 | 使用浏览器打开https://app.ucas.ac.cn/uc/wap/login,登录后在开发者工具里找到cookie。
130 | 以Chrome为例,按下F12后,Application——左侧找到Cookies下拉菜单——选择ucas的域名。如图:
131 |
132 |
133 |
134 |
135 |
136 | ## 实现自动化
137 | 如果要挂服务器的话,可以自行魔改,记得把脚本里最后的`os.system('pause')`删掉。
138 |
139 | ### Windows
140 |
141 | 任务计划程序。可参考[Windows创建定时任务执行Python脚本](https://blog.csdn.net/u012849872/article/details/82719372)。
142 |
143 | ### Linux
144 | 使用`crontab`。可参考[Linux crontab 命令](https://www.runoob.com/linux/linux-comm-crontab.html)。
145 |
146 | 示例:每天早上8点运行脚本:
147 |
148 | `0 8 * * * python3 report.py`
149 |
--------------------------------------------------------------------------------
/report(manual).py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | from utils.navigator import Navigator
5 | from utils.user import UserManager
6 |
7 | def assert_input(msg, choices: list):
8 | while True:
9 | res = input(msg)
10 | if res in choices:
11 | return res
12 | print('输入内容错误!')
13 |
14 | print('***********************************************')
15 | print('*** ***')
16 | print('*** UCAS Cronavirus Report Script ***')
17 | print('*** Manual ***')
18 | print('*** Password Login - by ZZJ ***')
19 | print('*** ***')
20 | print('***********************************************')
21 | print()
22 |
23 | try:
24 | usermg = UserManager(False)
25 | user = usermg.get_user()
26 | nav = Navigator(user)
27 | print('登录中')
28 | nav.login()
29 | print('获取历史打卡记录中')
30 | old_data = nav.get_history_data()
31 | print('构造本次打卡记录中')
32 | new_data = nav.gen_report_data(old_data)
33 | print('请设置需修改的参数:')
34 | if len(sys.argv) > 1 and sys.argv[1] in ['y', 'Y', 'n', 'N']:
35 | res = sys.argv[1]
36 | else:
37 | res = assert_input('昨日是否接受核酸检测?[y/n]', ['y', 'Y', 'n', 'N'])
38 | sfjshsjc = 1 if res in ['y', 'Y'] else 0
39 | assert 'sfjshsjc' in new_data, '待提交数据内不含"sfjshsjc"参数!打卡系统可能有变动,则本脚本不再支持!'
40 | print('用户输入为 昨日' + ('已' if sfjshsjc else '未') + '接受核酸检测')
41 | new_data['sfjshsjc'] = sfjshsjc
42 | print('提交打卡记录中')
43 | resp = nav.do_report(new_data)
44 | if resp.json()['e']==0:
45 | print('打卡成功!')
46 |
47 | except Exception as err:
48 | print(err)
49 |
50 | os.system('pause')
51 |
--------------------------------------------------------------------------------
/report(manual)_cookies.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | from utils.navigator import Navigator
5 | from utils.user import UserManager
6 |
7 | def assert_input(msg, choices: list):
8 | while True:
9 | res = input(msg)
10 | if res in choices:
11 | return res
12 | print('输入内容错误!')
13 |
14 | print('***********************************************')
15 | print('*** ***')
16 | print('*** UCAS Cronavirus Report Script ***')
17 | print('*** Manual ***')
18 | print('*** Cookies Login - by ZZJ ***')
19 | print('*** ***')
20 | print('***********************************************')
21 |
22 | try:
23 | usermg = UserManager(True)
24 | user = usermg.get_user()
25 | nav = Navigator(user)
26 | nav.login()
27 | print('获取历史打卡记录中')
28 | old_data = nav.get_history_data()
29 | print('构造本次打卡记录中')
30 | new_data = nav.gen_report_data(old_data)
31 | print('请设置需修改的参数:')
32 | if len(sys.argv) > 1 and sys.argv[1] in ['y', 'Y', 'n', 'N']:
33 | res = sys.argv[1]
34 | else:
35 | res = assert_input('昨日是否接受核酸检测?[y/n]', ['y', 'Y', 'n', 'N'])
36 | sfjshsjc = 1 if res in ['y', 'Y'] else 0
37 | assert 'sfjshsjc' in new_data, '待提交数据内不含"sfjshsjc"参数!打卡系统可能有变动,则本脚本不再支持!'
38 | print('用户输入为 昨日' + ('已' if sfjshsjc else '未') + '接受核酸检测')
39 | new_data['sfjshsjc'] = sfjshsjc
40 | print('提交打卡记录中')
41 | resp = nav.do_report(new_data)
42 | if resp.json()['e']==0:
43 | print('打卡成功!')
44 |
45 | except Exception as err:
46 | print(err)
47 |
48 | os.system('pause')
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/report.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from utils.navigator import Navigator
4 | from utils.user import UserManager
5 |
6 |
7 | print('***********************************************')
8 | print('*** ***')
9 | print('*** UCAS Cronavirus Report Script ***')
10 | print('*** Password Login - by ZZJ ***')
11 | print('*** ***')
12 | print('***********************************************')
13 | print()
14 |
15 | try:
16 | usermg = UserManager(False)
17 | user = usermg.get_user()
18 | nav = Navigator(user)
19 | print('登录中')
20 | nav.login()
21 | print('获取历史打卡记录中')
22 | old_data = nav.get_history_data()
23 | print('构造本次打卡记录中')
24 | new_data = nav.gen_report_data(old_data)
25 | print('提交打卡记录中')
26 | resp = nav.do_report(new_data)
27 | if resp.json()['e']==0:
28 | print('打卡成功!')
29 |
30 | except Exception as err:
31 | print(err)
32 |
33 | os.system('pause')
34 |
--------------------------------------------------------------------------------
/report_cookies.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from utils.navigator import Navigator
4 | from utils.user import UserManager
5 |
6 |
7 | print('***********************************************')
8 | print('*** ***')
9 | print('*** UCAS Cronavirus Report Script ***')
10 | print('*** Cookies Login - by ZZJ ***')
11 | print('*** ***')
12 | print('***********************************************')
13 |
14 | try:
15 | usermg = UserManager(True)
16 | user = usermg.get_user()
17 | nav = Navigator(user)
18 | nav.login()
19 | print('获取历史打卡记录中')
20 | old_data = nav.get_history_data()
21 | print('构造本次打卡记录中')
22 | new_data = nav.gen_report_data(old_data)
23 | print('提交打卡记录中')
24 | resp = nav.do_report(new_data)
25 | if resp.json()['e']==0:
26 | print('打卡成功!')
27 |
28 | except Exception as err:
29 | print(err)
30 |
31 | os.system('pause')
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/utils/httpRequestUtil.py:
--------------------------------------------------------------------------------
1 | from typing import Callable, Union
2 | import requests
3 | from contextlib import contextmanager
4 |
5 | @contextmanager
6 | def httpRequest(session, url, method, payload: dict = None, **params):
7 | """
8 | Generate a context to automatically send http request, allowing user to focus on response handling.
9 |
10 | Parameters
11 | ----------
12 | session : requests.Session
13 | Provide a session to be used.
14 | url : str
15 | Full url address with scheme 'http://' or 'https://'.
16 | method : str
17 | Only 'get' or 'post' is supported.
18 | payload : dict, optional
19 | This parameter is a lazy replacement for "params" and "data" (correspond to GET and POST respectively).
20 | **params :
21 | Additional arguments passed to package ``request``, such as headers and cookies.
22 |
23 | Returns
24 | -------
25 | requests.Response
26 |
27 | Examples
28 | --------
29 | >>> s = requests.Session()
30 | >>> with httpRequest(s, 'https://www.bilibili.com', 'get',
31 | ... payload={'key': 'data'},
32 | ... headers={},
33 | ... cookies={}) as resp:
34 | ... # Handle the response object
35 | ... print(resp.ok)
36 | True
37 | """
38 | if params is None:
39 | params = {}
40 |
41 | # DEBUG = True
42 | # if DEBUG:
43 | # params = {
44 | # 'proxies': {
45 | # 'http': 'http://127.0.0.1:9999',
46 | # 'https': 'http://127.0.0.1:9999',
47 | # }
48 | # }
49 |
50 | # Set default headers
51 | headers = params.setdefault('headers', {})
52 | headers.setdefault('User-Agent',
53 | 'Mozilla/5.0 (Linux; Android 10; NOH-AN00 Build/HUAWEINOH-AN00; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.99 XWEB/3263 MMWEBSDK/20211202 Mobile Safari/537.36 MMWEBID/9462 MicroMessenger/8.0.18.2060(0x28001257) Process/toolsmp WeChat/arm64 Weixin NetType/WIFI Language/zh_CN ABI/arm64')
54 |
55 | if method not in ['get', 'post']:
56 | raise NotImplementedError('http method', method, 'is not supported!')
57 |
58 | if payload:
59 | # replace key 'payload' with the correct key
60 | payload_key = 'params' if method == 'get' else 'data'
61 | params[payload_key] = payload
62 |
63 | # send the request
64 | yield session.request(method, url, **params)
65 |
66 |
--------------------------------------------------------------------------------
/utils/navigator.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | import requests
4 | from easydict import EasyDict as edict
5 | from utils.user import User
6 | from utils.httpRequestUtil import httpRequest
7 |
8 | conf = edict(dict(
9 | auth_welcome=dict(url='https://app.ucas.ac.cn/uc/wap/login', method='get'),
10 | auth_check=dict(url='https://app.ucas.ac.cn/uc/wap/login/check', method='post'),
11 | history_data=dict(url='https://app.ucas.ac.cn/ucasncov/api/default/daily?xgh=0&app_id=ucas', method='get'),
12 | report=dict(url='https://app.ucas.ac.cn/ucasncov/api/default/save', method='post'),
13 | ))
14 |
15 |
16 | class Navigator:
17 | REPORT_KEYS = ['date', 'realname', 'number', 'jzdz', 'zrzsdd', 'sfzx', 'dqszdd', 'geo_api_infot', 'szgj', 'szgj_select_info[id]', 'szgj_select_info[name]', 'geo_api_info', 'dqsfzzgfxdq', 'zgfxljs', 'tw', 'sffrzz', 'dqqk1', 'dqqk1qt', 'dqqk2', 'dqqk2qt', 'sfjshsjc', 'dyzymjzqk', 'dyzwjzyy', 'dyzjzsj', 'dezymjzqk', 'dezwjzyy', 'dezjzsj', 'dszymjzqk', 'dszwjzyy', 'dszjzsj', 'gtshryjkzk', 'extinfo', 'app_id']
18 | def __init__(self, user: User):
19 | self.s = requests.Session()
20 | self.user = user
21 |
22 | def login(self):
23 | # load user config from file
24 | if self.user.use_cookies:
25 | cookies = self.user.get_info()
26 | self.s.cookies.update(cookies)
27 | else:
28 | # authenticater user to get cookies (stored in session)
29 | self._authenticate(self.user)
30 |
31 | def _authenticate(self, user: User):
32 | # get first cookie eai-sess
33 | with httpRequest(self.s, **conf.auth_welcome) as resp:
34 | assert 'eai-sess' in self.s.cookies, self._err_msg('获取cookie eai-sess', 'cookie中不含eai-sess')
35 |
36 | # get second cookie UUkey
37 | payload = user.get_info()
38 | with httpRequest(self.s, **conf.auth_check, payload=payload) as resp:
39 | self._assert_json(resp, '获取cookie UUkey')
40 | assert 'UUkey' in self.s.cookies, self._err_msg('获取cookie UUkey', 'cookie中不含UUkey')
41 |
42 | def get_history_data(self):
43 | with httpRequest(self.s, **conf.history_data) as resp:
44 | self._assert_json(resp, '获取上次记录')
45 | res = resp.json()
46 | assert 'd' in res, self._err_msg('获取上次记录', '结果不含数据:{}'.format(res))
47 | return res['d']
48 |
49 | def gen_report_data(self, old_data):
50 | new_data = {key: old_data[key] for key in old_data if key in self.REPORT_KEYS}
51 | # 注意,经对比需要对数据做一下小调整,
52 | new_data.update({
53 | 'date': time.strftime("%Y-%m-%d", time.localtime()),
54 | 'geo_api_infot': '{"area":{"label":"","value":""},"city":{"label":"","value":""},"address":"","details":"","province":{"label":"","value":""}}',
55 | 'szgj': '',
56 | 'szgj_select_info[id]': '0',
57 | 'szgj_select_info[name]': '',
58 | 'app_id': 'ucas',
59 | })
60 | assert len(new_data) == len(self.REPORT_KEYS), self._err_msg('构造打卡数据', '构造的数据不完整!已有数据为:{}'.format(list(new_data.keys())))
61 | return new_data
62 |
63 | def do_report(self, data):
64 | with httpRequest(self.s, **conf.report, payload=data) as resp:
65 | self._assert_json(resp, '提交打卡数据')
66 | return resp
67 |
68 | @staticmethod
69 | def _assert_json(resp: requests.Response, msg=''):
70 | try:
71 | js = resp.json() # type: dict
72 | except requests.exceptions.JSONDecodeError as err:
73 | raise requests.exceptions.ContentDecodingError(f'{msg}失败!json解析错误,返回结果为:{resp.text}')
74 | assert 'e' in js, f'{msg}失败!json不含键"e",返回结果为:{resp.text}'
75 | assert js['e'] == 0, f'{msg}失败!服务器报错信息为:{js["m"]}'
76 |
77 | @staticmethod
78 | def _err_msg(msg1, msg2=''):
79 | return f'{msg1}失败!{msg2}'
--------------------------------------------------------------------------------
/utils/user.py:
--------------------------------------------------------------------------------
1 | import os
2 | import configparser
3 | import sys
4 |
5 |
6 | class User:
7 | def __init__(self, use_cookies):
8 | self.use_cookies = use_cookies
9 | self.info = None
10 |
11 | def get_info(self):
12 | return self.info
13 |
14 | class User_pass(User):
15 | def __init__(self, username, password):
16 | super().__init__(False)
17 | self.info = dict(
18 | username=username,
19 | password=password
20 | )
21 |
22 | class User_cookie(User):
23 | def __init__(self, eai_sess, UUKey):
24 | super().__init__(True)
25 | self.info = {
26 | 'eai-sess': eai_sess,
27 | 'UUKey': UUKey
28 | }
29 |
30 |
31 | class UserManager:
32 | BASEPATH = os.path.dirname(os.path.dirname(sys.executable if hasattr(sys, 'frozen') else __file__)) # 打包成exe文件后__file__失效
33 | CONFPATH = [os.path.join(BASEPATH, 'user.ini'), os.path.join(BASEPATH, 'user_cookies.ini')]
34 | def __init__(self, use_cookies):
35 | self.use_cookies = use_cookies
36 | self.config = {}
37 | self.check_new_user()
38 | self.load_config()
39 |
40 | def check_new_user(self):
41 | if not os.path.exists(self.CONFPATH[self.use_cookies]) or os.path.getsize(self.CONFPATH[self.use_cookies]) == 0:
42 | print('未检测到配置文件'+self.CONFPATH[self.use_cookies]+',请输入', end='')
43 | if self.use_cookies:
44 | print('cookies\n(默认记住此配置,如需更新请删除或修改.ini文件)\n')
45 | eai_sess = input('eai-sess: ')
46 | UUKey = input('UUKey: ')
47 | self.config = {
48 | 'eai-sess': eai_sess,
49 | 'UUKey': UUKey
50 | }
51 | else:
52 | print('sep系统登录账号和密码\n(默认记住此配置,如需更新请删除或修改.ini文件)\n')
53 | print('说明:sep用户名一般为学校邮箱**@ucas.ac.cn。\n')
54 | username = input('sep账号:')
55 | password = input('sep密码:')
56 | self.config = dict(
57 | username=username,
58 | password=password
59 | )
60 | self.save_config()
61 |
62 | def save_config(self):
63 | parser = configparser.ConfigParser()
64 | if self.use_cookies:
65 | parser['user_cookies'] = self.config
66 | else:
67 | parser['user'] = self.config
68 | with open(self.CONFPATH[self.use_cookies], 'w') as conffile:
69 | parser.write(conffile)
70 |
71 | def load_config(self):
72 | parser = configparser.ConfigParser()
73 | parser.read(self.CONFPATH[self.use_cookies])
74 | if parser.has_section('user_cookies'):
75 | self.use_cookies = True
76 | config = parser['user_cookies']
77 | print('已读取cookies')
78 | elif parser.has_section('user'):
79 | self.use_cookies = False
80 | config = parser['user']
81 | print('已读取账号', config['username'])
82 | else:
83 | os.remove(self.CONFPATH[self.use_cookies])
84 | raise NotImplementedError('.ini配置文件已损坏,请重新配置')
85 | self.config = config
86 |
87 | def get_user(self):
88 | if self.use_cookies:
89 | return User_cookie(self.config['eai-sess'], self.config['UUKey'])
90 | else:
91 | return User_pass(self.config['username'], self.config['password'])
92 |
--------------------------------------------------------------------------------