系列文章目录
上一篇:【Python Onramp】5.利用Pyecharts进行可视化:综合应用
下一篇:【Python Onramp】7. web端可视化:北京地铁数据统计分析实例以及简易Echarts绘图
本文目录
项目描述
这个项目中你将学习简单的网站开发逻辑,前端用HTML+JavaScript,后端使用Python的Flask框架。
代码仓库见:https://github.com/Honour-Van/CS50/tree/master/Web
分别实现了两个版本,一个在python+flask端进行排序,另一个在JavaScript端实现大部分代码逻辑。
目标效果如图所示。图片第一行显示成绩的排序方式,即按姓名、平时成绩、期中成绩、期末成绩、总成绩中的某一项升序或降序排列;单击提交按钮,则按指定的方式排序成绩;
说明
总成绩的计算方式是:平时成绩和期中成绩占30%,期末成绩占 40%,四舍五入。Python 提供round()函数,具体用法查找相关资料。
成绩表格采用HTML 表格实现。该表格的部分代码如下:
<table cellpadding='0' cellspacing='0' border='1'>
<tr><th>姓名</th><th>平时成绩</th><th>期中成绩</th><th>期末成绩
</th><th>总成绩</th><th>可视</th></tr>
<tr><td>陈子昂
</td><td>97</td><td>100</td><td>94</td><td>97</td><td><div
class='stateDiv' style='backgroundcolor:
Green;width:97px;'></div></td></tr>
</table>
“可视”列中颜色的宽度与分数相关,大于等于85 为绿色Green,大 于等于60 小于85 为黄色Yellow,其余为红色Red;
在head-style 中定义th、td 和stateDiv 的style 内容,使其效果尽量与图片一致,不要求绝对一致;
相关数据在附件中(poetScore.txt),不得用其他源数据;
附加要求:可分别实现两个版本,即排序在Python+Flask 端完成, 或者在浏览器端的网页部分完成。另外,思考如果有分数录入模块,分数查看模块,浏览器端排序是否会导致数据不一致,如何规避?
要点总览
要点1:web的基本结构
https://www.liaoxuefeng.com/wiki/1016959663602400/1017804650182592
现在主流的web架构都是客户端+服务器结构,粗略地讲,用户界面就是前端,实现我们客户端浏览器中看到的部分的工作;服务器逻辑叫后端,虽然并不是很容易理解这个工作,但其实服务器也没什么玄学的,如果服务器请求频率够低且逻辑够简单,你完全坐在一个盒子里,对外界的请求进行相对应的反馈,这时你就是一个服务器。
http协议在二者的沟通中发挥非常重要的作用,详情了解https://blog.csdn.net/huangwwu11/article/details/23531111
我们这里只使用get和post,前者是从服务器端取,后者也可以传一些数据回服务器。
要点2:前端常用的语言
前端常用的语言三件套是HTML+CSS+JavaScript,简单地说,HTML构筑页面基本结构;CSS为网页提供样式的规定,负责美化;JavaScript增添一些交互的逻辑。
由于这个系列重点是熟悉Python,所以如果对web开发没有足够了解的话,可以选择性地学习以下内容
HTML语言30分钟入门:https://www.runoob.com/w3cnote/html-30-minutes-introductory-tutorial.html
HTML官方教程:https://www.w3school.com.cn/h.asp
HTML菜鸟教程:https://www.runoob.com/html/html-tutorial.html
JavaScript语言快速入门:https://www.w3cschool.cn/programming/programming-javascript.html
但实际上这个项目中使用到的HTML只有表格内容,只需按行进行复制粘贴。
对于表格整体,我们只需要构建
<table cellpadding='0' cellspacing='0' border='1'>
<tr><th>姓名</th><th>平时成绩</th><th>期中成绩</th><th>期末成绩
</th><th>总成绩</th><th>可视</th></tr>
<tr><td>陈子昂
</td><td>97</td><td>100</td><td>94</td><td>97</td><td><div
class='stateDiv' style='backgroundcolor:
Green;width:97px;'></div></td></tr>
</table>
另外JavaScript也非常类似于C++,上手比较简单。
在HTML中引入JavaScript脚本需使用script标签,我们以如下的一段js代码来讲解js的基本使用逻辑:
$("#btn1").click(function () { // 美元符号‘$’是jquery选择器,
// 使用#进行选择是选出id为btn1的按键(.是按class进行选择,什么都不加就是按name选择)
// click是点击
var basis = $("[name='basis']").filter(":checked").attr("id"); //var是JavaScript中的变量,这里选择name为basis的一个html元素,找出哪个radio被按下,具体见后文有叙述
var ascending = $("[name='order']").filter(":checked").attr("id"); //同上
$.post("/sortby", { // post操作,这里sortby用于对接指定flask端的函数
Basis: basis, // 指明flask端函数的变量,用js端的结果替代
Ascending: ascending
},
function (data) {
$("tr").remove("#rd"); //去除表格模板中上一次的内容
$("table").append(data); //然后逐次添加
if (ascending == '1'){ //简单的分支逻辑,用于表示标题
$('title').text('按'+basis+'升序');
$('h1').text('按'+basis+'升序');}
else{
$('title').text('按'+basis+'降序');
$('h1').text('按'+basis+'降序');}
}
);
});//绑定click事件到button元素,function()相当于Python的lambda函数
CSS的部分完全可以留在HTML中,我们出于练习,将其抽出来
要点3:Python的web开发框架:Flask
越往后学,就会发现示例代码越发重要,前人开发的工具可能很好用,但如果要具体搞懂每个细节,可能会影响到实用的效率。
比如Flask中常用的的装饰器,如果初次使用,难免会一头雾水。但对于初学或者只是偶然使用来完成一个任务,那么完全可以直接上手拼贴代码(尽管这从长远来看可能并不健康
详细内容可见官方文档https://dormousehole.readthedocs.io/en/latest/index.html
或者博客https://zhuanlan.zhihu.com/p/43929790
本文中使用的静态html都会给出,所以使用的Flask相关内容都会非常简单
import flask # 导入Flask模块
app = flask.Flask(__name__) # 初始化Flask对象
@app.route("/hello")
def hello():
return flask.render_template("hello_world.html") # 直接使用模板html进行渲染
if __name__ == '__main__':
app.run()
在静态模板html中,我们只需要用双大括号将待导入数据名称括起来即可,注意要加一个safe的过滤器,否则row这个变量中的字符串将不会行使html代码的作用。
具体实现
数据组织
在python文件中利用二维列表读入名称并计算,并排序。
本来尝试用json组织数据
def txt2json():
with open("poetScore.txt", 'r', encoding='utf-8') as f:
f.readline()
data = ""
for line in f.readlines():
content = line.split(',')
score = round((int(content[1])+int(content[2])+int(content[3]))/3)
data = data + \
f'{{"name":"{content[0]}","usual":{content[1]},"midterm":{content[2]},"final":{content[3]},"score":{score}}},'
data = data[:-1]
with open("poetScore.json", 'w', encoding='utf-8') as f:
f.write(data)
但是由于数据中的重复,所以我们不采用这个方法。显然作为一个排名,其中可能存在重名。
with open('poetScore.txt', 'r', encoding='utf-8') as f:
df1 = pd.DataFrame(pd.read_csv('poetScore.txt'))
结合这两篇文章:
dataFrame添加列:https://blog.csdn.net/zx1245773445/article/details/99445332
行的遍历:https://blog.csdn.net/ls13552912394/article/details/79349809
我们利用列表生成式将rawdata进行部分分析:
df.insert(4, '总成绩', [round(int(row['平时成绩'])*0.3+int(row['期中成绩'])*0.3+int(row['期末成绩'])*0.4)
for _, row in df.iterrows()])
动态部分的构建
dataframe排序:https://blog.csdn.net/houyanhua1/article/details/87804111
df2 = read_data().sort_values(by=sb, ascending=asd) # dataframe排序是容易的
排序之后,利用Python生成HTML表格代码
def draw_table(df: pd.DataFrame, sortby:str) -> str:
rowdata = ''
for _, row in df.iterrows():
rowunit = f"<tr id='rd'><td>{row['name']}</td><td>{row['平时成绩']}</td><td>{row['期中成绩']}</td><td>{row['期末成绩']}</td><td>{row['总成绩']}</td>"
if sortby == "姓名":
rowdata += rowunit
rowdata += '</tr>'
else:
vis = row[sortby]
color = choose_color(vis)
rowunit += f"<td><div class='stateDiv' style='background:{color}; width:{vis}px;'></div></td></tr>"
rowdata += rowunit
return rowdata
网页radio获取
https://blog.csdn.net/newborn2012/article/details/17289345
在jQuery中很容易利用选择器和过滤器获得被选项:
var basis = $("[name='basis']").filter(":checked").attr("id");
var ascending = $("[name='order']").filter(":checked").attr("id");
一个小问题:大小写敏感
flask和html中js脚本的交互,注意,网址虽然是大小写不敏感的,但是在写接口的时候是敏感的。
$.post("/sortby", {
Basis: basis,
Ascending: ascending
},
function (data) {
$("tr").remove("#rd");
$("table").append(data);
if (ascending == '1'){
$('title').text('按'+basis+'升序');
$('h1').text('按'+basis+'升序');}
else{
$('title').text('按'+basis+'降序');
$('h1').text('按'+basis+'降序');}
}
);
如果其中的域名写作sortBy将无法识别。
拼音排名
使用xpinyin模块
p = Pinyin()
df.insert(5, '姓名', [p.get_pinyin(row['name'], tone_marks='numbers') for _, row in df.iterrows()])
但是如果将拼音列标记为‘姓名’,那么在显示的时候,就会列出所有的拼音,所以这里要对dataframe原来的‘姓名’列改名:
p = Pinyin()
df.insert(5, '姓名', [p.get_pinyin(row['name'], tone_marks='numbers') for _, row in df.iterrows()])
补充内容
尝试将df传到前端进行处理。
将json对象传递到前端
采用了https://www.cnblogs.com/lazyboy1/p/5015111.html中的方法5,但是模板中的替代数据是字符串即可。
flask代码如下,这是非常简单的:
@app.route('/')
def root():
return render_template('render2.html', data=read_data().to_json(orient='records', force_ascii=False))
接下来的重要问题出现在我们不太熟悉的JavaScript上
然后是排序,https://www.jianshu.com/p/92c3bf42a7b1
排序在点击按钮的时候扫描得出模式,然后进行排序。
var basis = $("[name='basis']").filter(":checked").attr("id");
var ascending = $("[name='order']").filter(":checked").attr("id");
data = mysort(stu_data, basis, ascending);
$("tr").remove("#rd");
console.log($("table"));
$.each(data, function (index, value) {
rowdata = `<tr id='rd'><td>${value.name}</td><td>${value.平时成绩}</td><td>${value.期中成绩}</td><td>${value.期末成绩}</td><td>${value.总成绩}</td>`
if (basis !== "姓名") {
var vis = value[basis];
var color = choose_color(vis);
rowdata += `<td><div class='stateDiv' style='background:${color}; width:${vis}px;'></div></td></tr>`;
}
rowdata += '</tr>';
$("table tbody").append(rowdata);
if (ascending == '1') {
$('title').text('按' + basis + '升序');
$('h1').text('按' + basis + '升序');
}
else {
$('title').text('按' + basis + '降序');
$('h1').text('按' + basis + '降序');
}
});
但是第一次我们得到了如下的表格:
预估是table的html代码发生了问题。观察如下:
查找资料,我们发现这是因为jquery会自动生成tbody,将加入的tr直接加入进去。这时候如果按照如下代码分次加入,则会使得空的tr被提取到tbody中。
$.each(data, function (index, value) {
$("table").append("<tr id='rd'>");
$("table").append(`<td>${value.name}</td>`);
$("table").append(`<td>${value.平时成绩}</td>`);
$("table").append(`<td>${value.期中成绩}</td>`);
$("table").append(`<td>${value.期末成绩}</td>`);
$("table").append(`<td>${value.总成绩}</td>`);
if (basis !== "姓名") {
var vis = value[basis];
var color = choose_color(vis);
$("table").append(`<td><div class='stateDiv' style='background:${color}; width:${vis}px;'></div></td></tr>`);
}
$("table").append('</tr>');
});
因而我们可以通过一次性添加的方式来解决这个问题:
$.each(data, function (index, value) {
rowdata = `<tr id='rd'><td>${value.name}</td><td>${value.平时成绩}</td><td>${value.期中成绩}</td><td>${value.期末成绩}</td><td>${value.总成绩}</td>`
if (basis !== "姓名") {
var vis = value[basis];
var color = choose_color(vis);
rowdata += `<td><div class='stateDiv' style='background:${color}; width:${vis}px;'></div></td></tr>`;
}
rowdata += '</tr>';
$("table").append(rowdata);
});
问题显然已经得到了解决:
当然我们也可以自己编写tbody来避免这个问题。这篇博客https://blog.csdn.net/wangdabin_1216/article/details/8206043给出的方法是写全tbody,减小不确定性。
function add(udata){$("table tbody").append(udata);}
add("<tr id='rd'>");
add(`<td>${value.name}</td>`);
add(`<td>${value.平时成绩}</td>`);
add(`<td>${value.期中成绩}</td>`);
add(`<td>${value.期末成绩}</td>`);
add(`<td>${value.总成绩}</td>`);
if (basis !== "姓名") {
var vis = value[basis];
var color = choose_color(vis);
add(`<td><div class='stateDiv' style='background:${color}; width:${vis}px;'></div></td></tr>`);
}
add('</tr>');
为了避免更多次修改source代码,这种方法不一定是更好的,但不犯懒,去定义tbody一定是没错的。
完整代码
结构如下:
render.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>排名</title>
<style>
th {
border: 1px solid;
width: 100px;
}
td {
width: 100px;
height: auto;
text-align: center;
vertical-align: middle;
border: 1px solid;
}
table {
border-spacing: 0;
border-collapse: collapse;
border: 1px solid;
margin: auto;
}
.stateDiv {
height: 20px;
}
h1{
text-align: center;
}
</style>
</head>
<body>
<h1>默认按总成绩降序</h1>
排序方式:
</br>
<input type="radio" name="basis" id="姓名">姓名
<input type="radio" name="basis" id="平时成绩">平时成绩
<input type="radio" name="basis" id="期中成绩">期中成绩
<input type="radio" name="basis" id="期末成绩">期末成绩
<input type="radio" name="basis" id="总成绩">总成绩
</br>
<input type="radio" name='order' id='1'>升序
<input type="radio" name="order" id='0'>降序
</br>
<button id="btn1">确定</button>
</br>
<table>
<tr>
<th>姓名</th>
<th>平时成绩</th>
<th>期中成绩</th>
<th>期末成绩</th>
<th>总成绩</th>
<th>可视</th>
</tr>
{{row|safe}}
</table>
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
<script>
$("#btn1").click(function () {
var basis = $("[name='basis']").filter(":checked").attr("id");
var ascending = $("[name='order']").filter(":checked").attr("id");
$.post("/sortby", {
Basis: basis,
Ascending: ascending
},
function (data) {
$("tr").remove("#rd");
$("table").append(data);
if (ascending == '1'){
$('title').text('按'+basis+'升序');
$('h1').text('按'+basis+'升序');}
else{
$('title').text('按'+basis+'降序');
$('h1').text('按'+basis+'降序');}
}
);
});//绑定click事件到button元素,function()相当于Python的lambda函数
</script>
</body>
</html>
render2.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>排名</title>
<!-- css -->
<style>
th {
border: 1px solid;
width: 100px;
}
td {
width: 100px;
height: auto;
text-align: center;
vertical-align: middle;
border: 1px solid;
}
table {
border-spacing: 0;
border-collapse: collapse;
border: 1px solid;
margin: auto;
}
.stateDiv {
height: 20px;
}
h1 {
text-align: center;
}
</style>
</head>
<body>
<h1>默认按总成绩降序</h1>
排序方式:
</br>
<input type="radio" name="basis" id="姓名">姓名
<input type="radio" name="basis" id="平时成绩">平时成绩
<input type="radio" name="basis" id="期中成绩">期中成绩
<input type="radio" name="basis" id="期末成绩">期末成绩
<input type="radio" name="basis" id="总成绩">总成绩
</br>
<input type="radio" name='order' id='1'>升序
<input type="radio" name="order" id='0'>降序
</br>
<button id="btn1">确定</button>
</br>
<table>
<thead>
<tr>
<th>姓名</th>
<th>平时成绩</th>
<th>期中成绩</th>
<th>期末成绩</th>
<th>总成绩</th>
<th>可视</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<script src="https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js"></script>
<script>
stu_data = eval('{{data|safe}}');
$("#btn1").click(function () {
var basis = $("[name='basis']").filter(":checked").attr("id");
var ascending = $("[name='order']").filter(":checked").attr("id");
data = mysort(stu_data, basis, ascending);
$("tr").remove("#rd");
console.log($("table"));
$.each(data, function (index, value) {
rowdata = `<tr id='rd'><td>${value.name}</td><td>${value.平时成绩}</td><td>${value.期中成绩}</td><td>${value.期末成绩}</td><td>${value.总成绩}</td>`
if (basis !== "姓名") {
var vis = value[basis];
var color = choose_color(vis);
rowdata += `<td><div class='stateDiv' style='background:${color}; width:${vis}px;'></div></td></tr>`;
}
rowdata += '</tr>';
$("table tbody").append(rowdata);
if (ascending == '1') {
$('title').text('按' + basis + '升序');
$('h1').text('按' + basis + '升序');
}
else {
$('title').text('按' + basis + '降序');
$('h1').text('按' + basis + '降序');
}
});
});
function mysort(array, key, asd) {
return array.sort(function (a, b) {
var x = a[key]; var y = b[key];
res = ((x < y) ? -1 : ((x > y) ? 1 : 0));
if (asd == "1") { return res; }
return -1 * res;
});
}
function choose_color(vis) {
return ((vis >= 85) ? "Green" : ((vis >= 60) ? "Yellow" : "Red"));
};
</script>
</body>
</html>
studScore.py
'''
@file: studScore.py
@author: 范皓年 1900012739 电子学系
@description: (基础版)
js端获得radio选项
python端利用dataframe进行排序
生成html表格字符串
传递到js端进行渲染
'''
from flask import Flask, request, render_template
import pandas as pd
from xpinyin import Pinyin
'''
# read_data->dataFrame:六列,分别为汉字姓名,平时成绩、期中成绩、期末成绩、总成绩,拼音姓名(便于按照音序进行排序)
'''
def read_data() -> pd.DataFrame:
with open('poetScore.txt', 'r', encoding='utf-8') as f:
df = pd.DataFrame(pd.read_csv('poetScore.txt'))
df.insert(4, '总成绩', [round(int(row['平时成绩'])*0.3+int(row['期中成绩'])*0.3+int(row['期末成绩'])*0.4)
for _, row in df.iterrows()])
df.rename(columns={'姓名': 'name'}, inplace=True)
p = Pinyin()
df.insert(5, '姓名', [p.get_pinyin(row['name'], tone_marks='numbers') for _, row in df.iterrows()])
return df
'''
# choose_color->str:
# 返回一个字符串,表示成绩对应的颜色
'''
def choose_color(grade):
if grade >= 85:
return 'Green'
elif grade >= 60:
return 'Yellow'
else:
return 'Red'
'''
# draw_table->str:
# 将dataframe数据转化成html表格格式的字符串
'''
def draw_table(df: pd.DataFrame, sortby:str) -> str:
rowdata = ''
for _, row in df.iterrows():
rowunit = f"<tr id='rd'><td>{row['name']}</td><td>{row['平时成绩']}</td><td>{row['期中成绩']}</td><td>{row['期末成绩']}</td><td>{row['总成绩']}</td>"
if sortby == "姓名":
rowdata += rowunit
rowdata += '</tr>'
else:
vis = row[sortby]
color = choose_color(vis)
rowunit += f"<td><div class='stateDiv' style='background:{color}; width:{vis}px;'></div></td></tr>"
rowdata += rowunit
return rowdata
app = Flask(__name__)
@app.route("/")
def root():
df1 = read_data().sort_values(by="总成绩", ascending=False)
rowdata = draw_table(df1,"总成绩")
return render_template('render.html', row=rowdata)
@app.route("/sortby", methods=("post",))
def sortBy():
sb = request.form["Basis"]
asd = int(request.form["Ascending"])
df2 = read_data().sort_values(by=sb, ascending=asd) # dataframe排序是容易的
rowdata = draw_table(df2,sb)
return rowdata
if __name__ == "__main__":
app.run(host="0.0.0.0", port=80, debug=True)
studScore_js.py
'''
@file: studScore.py
@author: 范皓年 1900012739 电子学系
@description: js端获得radio选项
js获取python端以json格式传递的dataframe
js端进行排序、颜色选择和html表格生成
'''
from flask import Flask, jsonify, render_template
import pandas as pd
from xpinyin import Pinyin
def read_data() -> pd.DataFrame:
with open('poetScore.txt', 'r', encoding='utf-8') as f:
df = pd.DataFrame(pd.read_csv('poetScore.txt'))
df.insert(4, '总成绩', [round(int(row['平时成绩'])*0.3+int(row['期中成绩'])*0.3+int(row['期末成绩'])*0.4)
for _, row in df.iterrows()])
df.rename(columns={'姓名': 'name'}, inplace=True)
p = Pinyin()
df.insert(5, '姓名', [p.get_pinyin(row['name'], tone_marks='numbers')
for _, row in df.iterrows()])
return df
app = Flask(__name__)
@app.route('/')
def root():
return render_template('render2.html', data=read_data().to_json(orient='records', force_ascii=False))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=80, debug=True)
总结
一个思想:为了实用可以牺牲对细节的过度关心。使用框架的核心思想。
几个要点:
- 网页开发逻辑:客户端-服务器/前端、后端,http协议
- 前端开发三件套:HTML,CSS,JavaScript
- Python后端开发:Flask框架