img

四季学已正式发布并保持开源

1
https://github.com/cykzht/season-zhixue

介绍

四季学——一个美观的成绩查询网站。

正如它的名字一样,这是一个会变换四季的程序,每次启动的时候都会重新选择一个季节作为主题季节。

img

img

img

img

程序原理

网站整体的框架使用Python+Flask+HTML+layui开发,本项目的诞生离不开zhixuewanglayuiFlask等开源库,感谢这些开发者为开源社区做出的贡献。

Web服务

使用Python的Flask框架返回页面信息。

1
2
3
4
5
6
7
8
9
form flask import *

app = Flask(__name__, static_url_path='')

@app.route('/', methods=['GET', 'POST'])
def index():
"""返回登录页面"""
return render_template('index.html')
app.run(host='0.0.0.0', port=5000)

季节更新

随机选取一个季节,返回相对应的颜色和背景图片。

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
def get_season():
'''
随机选取季节
slog: 季节名
pic: 背景图片
color: 主题颜色
'''
ran = random.randint(1, 4)
if (ran == 1):
slog = '退学网——秋'
color = '#e5bc00'
pic = 'images/web_login_bg1.webp'
elif (ran == 2):
slog = '退学网——夏'
color = '#50e45c'
pic = 'images/web_login_bg2.webp'
elif (ran == 3):
slog = '退学网——春'
color = '#f1beea'
pic = 'images/web_login_bg3.webp'
elif (ran == 4):
slog = '退学网——冬'
color = '#27A9E3'
pic = 'images/web_login_bg4.webp'
return slog, pic, color

用户登录

获取网站的session以及用户信息(来自zhixuewang库)。

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
from zhixuewang.student import StudentAccount
from zhixuewang.exceptions import UserNotFoundError, UserOrPassError, LoginError, RoleError
import json
import requests

def get_session(username: str, password: str, _type: str = "auto") -> requests.Session:
"""通过用户名和密码获取session"""
if len(password) != 32:
password = encode_password(password)
session = get_basic_session()
r = session.get(Url.SSO_URL)
json_obj = json.loads(r.text.strip().replace("\\", "").replace("'", "")[1:-1])
if json_obj["code"] != 1000:
raise LoginError(json_obj["data"])
lt = json_obj["data"]["lt"]
execution = json_obj["data"]["execution"]
r = session.get(Url.SSO_URL,
params={
"encode": "true",
"sourceappname": "tkyh,tkyh",
"_eventId": "submit",
"appid": "zx-container-client",
"client": "web",
"type": "loginByNormal",
"key": _type,
"lt": lt,
"execution": execution,
"customLogoutUrl": "https://www.zhixue.com/login.html",
"username": username,
"password": password
})
json_obj = json.loads(r.text.strip().replace("\\", "").replace("'", "")[1:-1])
if json_obj["code"] != 1001:
if json_obj["code"] == 1002:
raise UserOrPassError()
if json_obj["code"] == 2009:
raise UserNotFoundError()
raise LoginError(json_obj["data"])
ticket = json_obj["data"]["st"]
session.post(Url.SERVICE_URL, data={
"action": "login",
"ticket": ticket,
})
session.cookies.set("uname", base64_encode(username))
session.cookies.set("pwd", base64_encode(password))
return session



def login_student(username: str, password: str) -> StudentAccount:
"""通过用户名和密码登录学生账号"""
session = get_session(username, password)
student = StudentAccount(session)
return student.set_base_info()

zxw = login_student(username, password)

登录实现

使用HTML的form表单提交POST请求,返回到Flask并进行登录。

前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<form class="layui-form" method="post" action="{{url_for('index')}}" autocomplete="on">
<div class="layui-form-item">
<input name="username" class="layui-input" style="height: 45px;" placeholder="用户名" required="" type="text">
</div>
<div class="layui-form-item">
<input name="password" class="layui-input" style="height: 45px;" placeholder="密码" required="" type="password">
</div>
<label>记住用户名</label>
<input type="checkbox" name="remember_user" id="remember_user" lay-skin="primary">
<div class="layui-form-item">
<p align="center" style="font-size:20px;color:#FF0000">{{ message }}</p>
</div>
<div class="layui-form-item">
<input lay-submit lay-filter="login" class="layui-btn layui-btn-lg layui-btn-normal" value="登录"
style="width:100%;font-size: 18px;height: 48px;" type="submit" name="login">
</div>
</form>

后端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from zhixuewang import login_student

if (request.method == 'POST'):
username = request.form.get("username")
password = request.form.get("password")
session['username'] = username
session['password'] = password

username = session.get('username')
password = session.get('password')
exam = request.args.get('exam')
try:
zxw = login_student(username, password)
exams = zxw.get_exams()
except Exception as e:
session.clear()
return render_template("index.html", season=season, message=e)

获取考试

调用API获取所有考试。

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
import json
from typing import List, Tuple, Union
from zhixuewang.models import (ExtendedList, Exam, Mark, Subject, SubjectScore,
StuClass, School, Sex, Grade, Phase, ExtraRank, ExamInfo,
StuPerson, StuPersonList)
from zhixuewang.exceptions import UserDefunctError, PageConnectionError, PageInformationError
from json import JSONDecodeError

def get_page_exam(self, page_index: int) -> Tuple[ExtendedList[Exam], bool]:
"""获取指定页数的考试列表"""
self.update_login_status()
exams: ExtendedList[Exam] = ExtendedList()
r = self._session.get(Url.GET_EXAM_URL,
params={
"pageIndex": page_index,
"pageSize": 10
},
headers=self._get_auth_header())
if not r.ok:
raise PageConnectionError(
f"get_page_exam中出错, 状态码为{r.status_code}")
try:
json_data = r.json()["result"]
for exam_data in json_data["examList"]:
exam = Exam(
id=exam_data["examId"],
name=exam_data["examName"]
)
exam.create_time = exam_data["examCreateDateTime"]
exams.append(exam)
hasNextPage: bool = json_data["hasNextPage"]
except (JSONDecodeError, KeyError) as e:
raise PageInformationError(
f"get_page_exam中网页内容发生改变, 错误为{e}, 内容为\n{r.text}")
return exams, hasNextPage

def get_exams(self) -> ExtendedList[Exam]:
"""获取所有考试"""

# 缓存
if len(self.exams) > 0:
latest_exam = self.get_latest_exam()
if self.exams[0].id == latest_exam.id:
return self.exams

exams: ExtendedList[Exam] = ExtendedList()
i = 1
check = True
while check:
cur_exams, check = self.get_page_exam(i)
exams.extend(cur_exams)
i += 1
self.exams = exams
return exams

获取成绩

调用API获取最新一次的成绩或指定的考试成绩(该版本为魔改版)。

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
import json
from typing import List, Tuple, Union
from zhixuewang.models import (ExtendedList, Exam, Mark, Subject, SubjectScore,
StuClass, School, Sex, Grade, Phase, ExtraRank, ExamInfo,
StuPerson, StuPersonList)
from zhixuewang.exceptions import UserDefunctError, PageConnectionError, PageInformationError
from json import JSONDecodeError

def __get_self_mark(self, exam: Exam, has_total_score: bool) -> Mark:
self.update_login_status()
mark = Mark(exam=exam, person=self)
r = self._session.get(Url.GET_MARK_URL,
params={"examId": exam.id},
headers=self._get_auth_header())
if not r.ok:
raise PageConnectionError(
f"__get_self_mark中出错, 状态码为{r.status_code}")
try:
json_data = r.json()
json_data = json_data["result"]
# exam.name = json_data["total_score"]["examName"]
# exam.id = json_data["total_score"]["examId"]
for subject in json_data["paperList"]:
subject_score = SubjectScore(
score=subject["userScore"],
subject=Subject(
id=subject["paperId"],
name=subject["subjectName"],
code=subject["subjectCode"],
standard_score=subject["standardScore"],
exam_id=exam.id),
person=StuPerson()
)
# subject_score.create_time = 0
mark.append(subject_score)
total_score = json_data.get("totalScore")
if has_total_score and total_score:
subject_score = SubjectScore(
score=total_score["userScore"],
subject=Subject(
id="",
name=total_score["subjectName"],
code="99",
standard_score=total_score["standardScore"],
exam_id=exam.id,
),
person=StuPerson(),
class_rank=exam.class_rank,
grade_rank=exam.grade_rank
)
# subject_score.create_time = 0
mark.append(subject_score)
self._set_exam_rank(mark)
except (JSONDecodeError, KeyError) as e:
if(not mark):
raise PageInformationError("本次考场的成绩不可查询")
return mark
return mark

def get_self_mark(self,
exam_data: Union[Exam, str] = "",
has_total_score: bool = True) -> Mark:
"""获取指定考试的成绩"""
exam = self.get_exam(exam_data)
if exam is None:
return Mark()
return self.__get_self_mark(exam, has_total_score)

exams = zxw.get_exams()
a = zxw.get_self_mark(exams[0])

表格呈现

根据返回来的成绩生成表格。

前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div class="login">
<center>
<table border="1px" width="300">
<caption style="font-size: 23px">
{{title}}
</caption>
<tr style="background: #ee0000">
<th style="font-size: 20px">科目</th>
<th style="font-size: 20px">分数</th>
</tr>
{% for row in data %}
<tr>
<th style="font-size: 20px">{{ row[0] }}</th>
<th style="font-size: 20px">{{ row[1] }}</th>
</tr>
{% endfor %}
</table>
</center>
</div>

后端

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
if len(exams) != 0:
data = []
examlist = []
for i in exams:
data.append(i.id)
examlist.append((i.id, i.name))
if (exam):
if (exam in data):
try:
a = zxw.get_self_mark(exam)
except Exception as e:
return render_template("examlist.html", data=examlist, season=season, avatar=avatar, message="本场考试未扫描")
msg = a.person.name+'-'+a.exam.name
data = []
for i in a:
if (i.class_rank):
data.append((i.subject.name, i.score, i.class_rank))
else:
data.append((i.subject.name, i.score))
return render_template("zhixue.html", data=data, season=season, title=msg, avatar=avatar)
else:
return render_template("examlist.html", data=examlist, season=season, avatar=avatar, message="该考试不可查询")
else:
return render_template("examlist.html", data=examlist, season=season, avatar=avatar)
else:
return render_template("index.html", season=season, message='未检测到考试')

自动登录

使用layui的框架进行二次开发。把账号密码通过cookie的方式存储在用户设备里,每次登录的时候自动填充(有一些浏览器不支持cookie)。

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
<script src="js/jquery-3.6.0.min.js"></script>
<script src="js/jquery.cookie.js"></script>
<script src="js/layui.js"></script>

<script>
layui.use(['form'], function () {
var form = layui.form,
layer = layui.layer;

/*记住用户名和密码*/
if ($.cookie("remember_user")) {
$("#remember_user").prop("checked", true);
form.val("add_form", {
"username": $.cookie("user_name"),
"password": $.cookie("user_password")
})
}
// 进行登录操作
form.on('submit(login)', function (data) {
data = data.field;
if (data.username == '') {
layer.msg('用户名不能为空');
return false;
}
if (data.password == '') {
layer.msg('密码不能为空');
return false;
}
//勾选记住密码
if (data.remember_user == "on") {
var user_name = data.username;
var user_password = data.password;
$.cookie("remember_user", "true", {
expires: 7
}); // 存储一个带7天期限的 cookie
$.cookie("user_name", user_name, {
expires: 7
}); // 存储一个带7天期限的 cookie
$.cookie("user_password", user_password, {
expires: 7
}); // 存储一个带7天期限的 cookie
} else {
$.cookie("remember_user", "false", {
expires: -1
}); // 删除 cookie
$.cookie("user_name", '', {
expires: -1
});
$.cookie("user_password", '', {
expires: -1
});
}
return true;
});
});

</script>

使用说明

所需环境

使用pip指令安装。

1
2
pip3 install flask
pip3 install zhixuewang

启用程序

1、安装好所需的库之后运行Python程序(开箱即用)。

2、访问http://127.0.0.1:5000/访问四季学网站。

3、输入某学网的账号和密码即可查询最新一次的考试成绩。

部署程序

如果你要把程序运行在服务器上,建议使用WSGI方式启动。

1
pip3 install gevent

在代码里添加

1
2
3
4
5
from gevent import pywsgi

#app.run(host='0.0.0.0', port=5000)
server = pywsgi.WSGIServer(('0.0.0.0', 5000), app)
server.serve_forever()

或者使用gunicorn启动。

1
pip3 install gunicorn
1
gunicorn app:app
  • app是flask的启动python文件,我们这是app.py
  • app则是flask应用程序实例,我们app.py里实例名为app