语言:python3
平台:Windows10
涉及技术名词:Flask,MongoDB,RESTful,Bootstrap3,简单GUI,GoogleHack,Ajax懒加载等等
项目代码:https://download.csdn.net/download/qq_40929683/22743862
一、前言
1. 背景
在日常编码中,由于某些代码片段(例如:驱动程序,算法程序等)可以多次重用、借鉴,所以把它们归类存储起来。但是需要的时候就要去文件夹中查找,而且有新的代码需要存储时,需要不断管理分类(给文件夹分类)。
刚好在学Python,就想做一个代码仓库(这里的仓库就是字面上意思的仓库),方便管理自己的代码,需要的时候可以直接拿到想要的代码,有新的代码可以方便入库。
于是就有了CodeSpace(软件的名称)。
2. 注意
由于这个软件是一个课程作业练习作品,当基本可以使用后就没有继续完善。有很多功能不完善、也有一些bug,但是所有的基本功能都有了。
这个项目只介绍了Flask的路由部分,借助路由,搭建的项目。
通过这个软件(包含启动器,前端,后端开发)的开发,包含:
- Flask的路由基本使用
- 什么是RESTful
- MongoDB的简单使用
- 前端页面的快速搭建(使用Bootstrap3和某些模板)
- 简单Python的GUI设计
- 了解GoogleHack
- 前后端的请求处理的简单流程
软件的核心是围绕学习Flask框架的,其他技术只是简单涉及和使用。
由于这篇文章核心是介绍Flask的使用,所以前端设计、数据设计等只会粗略介绍。
3. 技术介绍
数据来源:百度百科,教程数据来源:互联网
3.1 Flask
Flask是一个轻量级的可定制框架,使用Python语言编写,较其他同类型框架更为灵活、轻便、安全且容易上手。它可以很好地结合MVC模式进行开发,开发人员分工合作,小型团队在短时间内就可以完成功能丰富的中小型网站或Web服务的实现。另外,Flask还有很强的定制性,用户可以根据自己的需求来添加相应的功能,在保持核心功能简单的同时实现功能的丰富与扩展,其强大的插件库可以让用户实现个性化的网站定制,开发出功能强大的网站。
- Flask中文文档:欢迎来到 Flask 的世界 — Flask 中文文档 (2.0.1)
3.2 MongoDB
MongoDB是一个基于分布式文件存储 [1] 的数据库。由C++语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。
MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。
- MongoDB中文教程(菜鸟教程):MongoDB 教程 | 菜鸟教程
3.3 RESTful
RESTful架构是对MVC架构改进后所形成的一种架构,通过使用事先定义好的接口与不同的服务联系起来。在RESTful架构中,浏览器使用POST,DELETE,PUT和GET四种请求方式分别对指定的URL资源进行增删改查操作。因此,RESTful是通过URI实现对资源的管理及访问,具有扩展性强、结构清晰的特点。
RESTful架构将服务器分成前端服务器和后端服务器两部分,前端服务器为用户提供无模型的视图;后端服务器为前端服务器提供接口。浏览器向前端服务器请求视图,通过视图中包含的AJAX函数发起接口请求获取模型。
项目开发引入RESTful架构,利于团队并行开发。在RESTful架构中,将多数HTTP请求转移到前端服务器上,降低服务器的负荷,使视图获取后端模型失败也能呈现。但RESTful架构却不适用于所有的项目,当项目比较小时无需使用RESTful架构,项目变得更加复杂。
我们的项目很小,但是使用了RESTful,是为了简单了解、学习RESTful。
- RESTful API:什么是REST | RESTful API 中文网
3.4 GUI设计
图形用户界面(Graphical User Interface,简称 GUI,又称图形用户接口)是指采用图形方式显示的计算机操作用户界面。 [1]
图形用户界面是一种人与计算机通信的界面显示格式,允许用户使用鼠标等输入设备操纵屏幕上的图标或菜单选项,以选择命令、调用文件、启动程序或执行其它一些日常任务。与通过键盘输入文本或字符命令来完成例行任务的字符界面相比,图形用户界面有许多优点。图形用户界面由窗口、下拉菜单、对话框及其相应的控制机制构成,在各种新式应用程序中都是标准化的,即相同的操作总是以同样的方式来完成,在图形用户界面,用户看到和操作的都是图形对象,应用的是计算机图形学的技术。
我们使用的是Tkinter,当前也可以使用其他的GUI设计包,例如:PySimpleGUI等。
- Tkinter教程: Python GUI 编程(Tkinter) | 菜鸟教程
3.5 GoogleHack
google hack是指使用Google等搜索引擎对某些特定的网络主机漏洞(通常是服务器上的脚本漏洞)进行搜索,以达到快速找到漏洞主机或特定主机的漏洞的目的。
例如:
site:xx.com 返回所有与该站有关的url
link:xx.com 返回所有与该站做了链接的站
site:xx.com filetype:txt 查找TXT文件 其他的以此类推
我们借用的GoogleHack的想法,利用搜索关键词进行组合,帮助我们快速定位代码,查找代码。
3.6 BootStrap3
Bootstrap是美国Twitter公司的设计师Mark Otto和Jacob Thornton合作基于HTML、CSS、JavaScript 开发的简洁、直观、强悍的前端开发框架,使得 Web 开发更加快捷。Bootstrap提供了优雅的HTML和CSS规范,它即是由动态CSS语言Less写成。Bootstrap一经推出后颇受欢迎,一直是GitHub上的热门开源项目,包括NASA的MSNBC(微软全国广播公司)的Breaking News都使用了该项目。 [1] 国内一些移动开发者较为熟悉的框架,如WeX5前端开源框架等,也是基于Bootstrap源码进行性能优化而来。
- BootStrap3中文文档:Bootstrap v3 中文文档 · Bootstrap 是最受欢迎的 HTML、CSS 和 JavaScript 框架,用于开发响应式布局、移动设备优先的 WEB 项目。 | Bootstrap 中文网
- BootStrap3教程:Bootstrap 教程 | 菜鸟教程
3.7 懒加载
懒加载(Load On Demand)是一种独特而又强大的数据获取方法,它能够在用户滚动页面的时候自动获取更多的数据,而新得到的数据不会影响原有数据的显示,同时最大程度上减少服务器端的资源耗用。
二、设计
有些设计是在系统设计中做的,但是为了看的层次清晰,放在了对应的设计中。
2.1 系统设计
首先我们的系统是有前后端的,我们不仅要考虑后端设计,还要考虑前端设计,数据库设计。
我们设计、使用如下非常简单的技术架构(物理存储不需要我们设计):
使用BootStrap3作为前端设计,Flask作为后端,MongoDB作为数据库,整体使用RESTful,软件通过启动器启动、控制系统的运行。(防止有写读者不熟悉某些技术,相关的技术介绍所以写在前面了)
2.2 数据库设计
使用MongoDB建立数据库CodeSpace。
建立多个数据集,每一个数据集代表一种分类。
每条代码数据包含字段名:
- _id:唯一的代码id,数据库自动生成
- code_name: 代码名称
- code_type:代码分类
- code_des:代码功能的简单介绍,描述
- code_cnt:代码内容,可以说简单的代码,也可以部分代码,详细代码在附件中
- code_file:代码附件有无,true为有附件,false为附件
由于这里的_id是唯一的那么可以使用_id进行文件夹命名,存储附件,下载时相应的代码附件时,直接定位到对应文件夹进行下载即可。
2.3 前端设计
1. index.html
我们需要一个首页,进行交互。这个首页应该包含功能:
- 上传代码(支持上传多个代码附件)
- 代码检索(支持关键词组合搜索)
- 仓库容量显示
2. search_res.html
代码查找后,需要一个页面返回显示的结果,我们使用的是懒加载(这里采用点击加载更多的方式),其中包含的功能:
- 检索结果懒加载
- 匹配代码的查看、修改、删除
- 返回首页
点击展开,弹出卡片小窗口,显示详细的代码信息,并提供下载附件功能。
同理,点击编辑,提供一个表单卡片,用于提交修改。
3. code_info.html
既然使用懒加载,我们使用code_info.html作为一条检索结果,n个结果进行返回。(n这里为5)
使用方法就是在code_info.html设置多个标志位预先占位,后端检索到数据后(json格式),依次替换生成一个code_info,同理n个code_info为一组返回到前端进行展示。
2.4 后端设计
根据RESTful的风格,我们使用请求方法(GET,PUSH,PUT,DELETE等)区分操作,这样也便于代码设计。
1. 路由设计
/ 就是路由,表示如果用户输入了这个地址,那么 Flask 就会调用对应的对于的函数来进行处理。我们可以给定义多个路由,这样不同的 URL 就会有不同的处理函数:
@app1.route('/')
def fun1():
return 'hi, flask'
@app1.fun1('/hello/')
def fun2():
return 'hello, flask'
当我们启动代码,浏览器输入127.0.0.1会路由到根目录(/),后端会执行fun1,显示hi, flask。
同理我们输入127.0.0.1/hello/,后端会执行fun2,显示hello,flask。
同时可以使用methods参数,能接受哪些类型请求,根据不同请求进行代码设计。
1.1 /和/index/
由于对于主页,我们打开主页只有一个获取(GET)需求(请求一个页面),所有只有GET方法可以使用。设置如下路由:
@app.route("/", methods=["GET"])
@app.route("/index/", methods=["GET"])
1.2 /code_data/
这里路径是表示代码数据(这里是代码数据,检索不属于这里的功能)相关的,对于代码的操作我们包含提交(PUST),修改(PUT),删除(DELETE),于是有以下路由:
@app.route("/code_data/", methods=["POST", "PUT", "DELETE"])
1.3 /search_data/
对于检索,我们的操作只有获取(GET)操作,用来请求对数据进行检索。于是有如下路由:
@app.route("/search_data/", methods=["POST"])
1.4 /load_more/
由于懒加载也需要请求,我们单独设计一个路由给它(我们使用POST方式提交数据)。
@app.route("/load_more/", methods=["POST"])
1.5 /uploads/<type>/<id>/
代码附件的下载也是一个单独的路由,它的路径动态生成,更具代码类型和代码的id生成。
@app.route("/uploads/<type>/<id>", methods=["GET"])
2. 功能设计
2.1 index()
首页路由到后,调用index()方法,读取首页html代码和配置信息展示到前端即可。
2.2 code_data()
路由到/code_data/,需要首先同步仓库状态、检测配置信息,在根据请求方式的不同对数据进行添加、修改、删除操作。使用如下代码进行处理。
if request.method == "POST":
# 数据保存
pass
elif request.method == "PUT":
# 数据修改
pass
elif request.method == "DELETE":
# 数据删除
pass
其中附件上传后会被再次压缩,压缩名为代码id名。 定义代码压缩函数zip_dir()。
- 删除数据,定义函数:remove_files()
- 保存数据,定义函数:save_code_info()
- 修改数据,使用remove_files()和save_code_info()组合实现
2.3 load_more()
懒加载大致流程如下:
请求到/load_mode/路由,有load_more()进行懒加载的数据操作。
2.4 download_files()
文件下载函数主要是根据请求的地址(/uploads/<type>/<id>),进行下载。
2.5 search_data()
需要对前端传入的搜索关键词组进行解析,定义函数search_data_analysis(),进行参数分析后。根据关键词组去数据库中查询。
# 关键词组
search_para = {"+": [], "-": [], "in": [], "text": []}
- +测试:必须包含文本"测试"
- -测试:必须不包含文件"测试"
- in:Java :在Java的代码库中查找
- text:为普通文件
必须包含、必须不包含、普通文本使用正则表达式实现文件的匹配。
2.6 MongoDBCtrl类
上述操作必须依靠数据库进行数据操作,我们定义MongoDBCtrl类,进行数据操作。包含如下方法:
- 数据库连接:mongodb_init()
- 数据插入:insert()
- 数据更新:update()
- 数据检索:find()
使用配置文件进行对数据库的配置,数据库的初始化中需要载入配置文件进行配置。
2.7 配置文件设计
使用ini类型文件作为配置文件,定义两个配置文件:
- 仓库信息配置:space_info.ini
- 数据库和调试模式配置:config.ini
space_info.ini
; 仓库内代码数量统计
[CodeSpace_Info]
cs_count = 0
[CodeSpace_State]
state = 1
config.ini
; mongodb 的配置
[MongoDB]
; 连接的地址
mongodbHost = 127.0.0.1
; 连接的端口
mongodbPort = 27017
; 连接的数据库
database = CodeSpace
; 测试模式 true 开启测试模式 ; false 关闭测试模式
runtest = false
2.6 桌面启动器
桌面启动器主要是为了方便我们控制系统的使用,使用Tkinter进行GUI设计。
2.7 目录结构设计
- lib为库文件:这里存储的是操作MongoDB的类
- static:为flask的静态文件,例如js,css,img等静态文件
- template:html文件
- tmp:临时文件
- uploads:上传的数据存储的地方
- app.py:核心代码
- start.pyw:启动器代码
- *.ini:配置文件
三、编码
3.1 前端编码
1. /和/index/
页面采用网页模板和bootstrap3快速搭建的。
模板名称:首页 - 光年(Light Year Admin)后台管理系统模板
隐藏提交表单
显示提交表单
2. /search_data/
删除
编辑
展开
3.2 后端编码(部分)
后端部分主要是使用Flask的路由部分,理解了路由的含义,其他的代码就是基础开发了,例如:
- 文件压缩
- 配置文件操作
- json
- 正则匹配
- ...
1. 编码(部分)
@app.route("/", methods=["GET"])
@app.route("/index/", methods=["GET"])
# 主页
def index():
cp = configparser.ConfigParser()
cp.read("./space_info.ini", encoding="utf-8-sig")
code_count = int(cp.get("CodeSpace_Info", "cs_count"))
return render_template("index.html", code_count=code_count)
@app.route("/code_data/", methods=["POST", "PUT", "DELETE"])
# 数据的新增,修改,删除
def code_data():
# ini文件解析
cp = configparser.ConfigParser()
cp.read("./space_info.ini", encoding="utf-8-sig")
# 仓库代码数量
count = int(cp.get("CodeSpace_Info", "cs_count"))
if request.method == "POST":
# 新增
count += 1
save_code_info(request=request)
# 更新记录文件(.ini)
cp.set("CodeSpace_Info", "cs_count", str(count))
cp.write(open("./space_info.ini", "w"))
# 返回成功信息(json)
return jsonify({"upload_status": "true"})
elif request.method == "PUT":
# 修改
if mdb_ctrl.del_by_id(request.form['_id'], request.form['old_type']) == 'ok':
save_code_info(request=request)
path_dir = os.getcwd().replace('\\', '/') + r"/uploads/{}/{}/".format(request.form['old_type'],
request.form['_id'])
if os.path.isdir(path_dir):
remove_files(path_dir)
return jsonify({'update_status': 'true'})
else:
return jsonify({'update_status': 'false'})
elif request.method == "DELETE":
# 删除
print(request.form['d_id'], request.form['d_type'])
if mdb_ctrl.del_by_id(request.form['d_id'], request.form['d_type']) == 'ok':
path_dir = os.getcwd().replace('\\', '/') + r"/uploads/{}/{}/".format(request.form['d_type'],
request.form['d_id'])
if os.path.isdir(path_dir):
remove_files(path_dir)
count -= 1
# 更新记录文件(.ini)
cp.set("CodeSpace_Info", "cs_count", str(count))
cp.write(open("./space_info.ini", "w"))
return jsonify({'del_status': 'true'})
else:
return jsonify({'del_status': 'false'})
@app.route("/search_data/", methods=["POST"])
# 提交要查询的数据,展示查询结果
def search_data():
res_list = []
if request.method == "POST":
para = search_data_analysis(request.form["search_data"])
if len(para["text"]) == 0:
return jsonify({"search_status": "false"})
print(para)
res = mdb_ctrl.find(para=para)
print("END-----")
for r in res.values():
res_list.append(r)
res.clear()
res["res"] = res_list
with open(sys.path[0] + r"\CodeSpace\tmp\code.json", "w", encoding="utf-8") as f:
f.write(str(res).replace('\'', '"'))
return render_template("search_res.html", code_collection=",".join(para["in"])
, in_text=",".join(para["+"])
, not_in_text=",".join(para["-"])
, text_cnt=",".join(para["text"]))
# return jsonify({"search_status": "true"})
@app.route("/load_more/", methods=["POST"])
def load_more():
if request.method == 'POST':
if int(request.form['next']) <= 5:
with open(sys.path[0] + r"\CodeSpace\tmp\code.json", "r", encoding="utf-8") as f:
load_dict = json.load(f)
print(load_dict)
if len(load_dict['res']) == 0:
return jsonify({"get_more_status": "empty"})
if len(load_dict['res']) > int(request.form['next']):
load_more_cnt = int(request.form['next'])
else:
load_more_cnt = len(load_dict['res'])
if load_more_cnt == 0:
return jsonify({"get_more_status": "end"})
with open("./static/code_info.html", "r", encoding="utf-8") as f:
str_res = f.read()
str_res_copy = str_res
res_list_str = ''
for i in range(0, load_more_cnt, 1):
c_id = load_dict['res'][i]['_id']
c_name = load_dict['res'][i]['code_name']
c_type = load_dict['res'][i]['code_type']
c_des = load_dict['res'][i]['code_des']
c_cnt = load_dict['res'][i]['code_cnt']
c_file = load_dict['res'][i]['code_file']
str_tmp = ''
if len(c_file) > 0:
str_tmp = str_res.replace('__FILE__', '<a href="/uploads/__TYPE__/__ID__" style="display: block; '
'float: right;"> 下载附件</a>')
else:
str_tmp = str_res.replace('__FILE__', '<p style="display: block; float: right;"> 无附件</p>')
str_tmp = str_tmp.replace('__ID__', c_id).replace('__NAME__', c_name).replace('__TYPE__', c_type) \
.replace('__DES__', c_des).replace('__CNT__', c_cnt).replace('__TYPE__', c_type)
res_list_str += str_tmp
str_res = str_res_copy
for i in range(0, load_more_cnt, 1):
load_dict['res'].pop(0)
with open(sys.path[0] + r"\CodeSpace\tmp\code.json", "w", encoding="utf-8") as f:
f.write(str(load_dict).replace('\'', '"'))
return jsonify({"code_info": res_list_str})
else:
return jsonify({"get_more_status": "false"})
@app.route("/uploads/<type>/<id>", methods=["GET"])
def download_files(type, id):
if request.method == 'GET':
dri_path = app.config["UPLOAD_FOLDER"] + r"{}/{}/".format(type, str(id))
print(type, id)
print(dri_path)
return send_from_directory(dri_path, filename="{}.zip".format(str(id)), as_attachment=True)
mdb_ctrl = mongodb_tools.MongoDBCtrl()
3.3 启动器编码
1. 编码(部分)
top_flag = False
run_flag = False
threadLock = threading.Lock()
ls = None
window = tk.Tk()
text = StringVar()
text.set('-')
cp = configparser.ConfigParser()
ls = tk.Label(window, textvariable=text, fg='white', bg='red', font=('宋体', 10), width=2, height=1)
class myThread(threading.Thread):
def __init__(self, threadID, name):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
def run(self):
print("开始线程:" + self.name)
thread_rcs(self.name)
print("退出线程:" + self.name)
threadCS = None
def run_CS():
global threadCS, run_flag
cp = configparser.ConfigParser()
cp.read("./space_info.ini", encoding="utf-8-sig")
state = cp.get("CodeSpace_State", "state")
if state == '0' and not run_flag:
run_flag = True
cp.set("CodeSpace_State", "state", "1")
cp.write(open("./space_info.ini", "w"))
threadCS = myThread(1, "run_cs")
threadCS.setDaemon(True)
threadCS.start()
text.set('O')
ls.configure(bg='green')
b1.configure(state=tk.DISABLED)
b2.configure(state=tk.NORMAL)
else:
tkinter.messagebox.showerror('失败', 'CodeSpace启动失败')
return
def thread_rcs(threadName):
print(threadName)
os.system('flask run')
b1 = tk.Button(window, text="启动CodeSpace", command=run_CS)
b2 = tk.Button(window, text="停止CodeSpace", command=stop_CS)
def main():
cp.read("./space_info.ini", encoding="utf-8-sig")
cp.set("CodeSpace_State", "state", "0")
cp.write(open("./space_info.ini", "w"))
window.title('CSC')
window.iconbitmap('.\\favicon.ico')
window.protocol('WM_DELETE_WINDOW', closeWindow)
window.resizable(False, False)
menubar = tk.Menu(window)
filemenu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label='更多', menu=filemenu)
filemenu.add_checkbutton(label='窗口置顶', command=top)
filemenu.add_command(label='关于软件', command=more)
filemenu.add_command(label='退出', command=window.quit)
window.config(menu=menubar)
l = tk.Label(window, text='CodeSpace Console', bg='white', font=('宋体', 10), width=30, height=2)
l.pack()
lt = tk.Label(window, text='如果发现启动失败、CS无法使用', fg='red', font=('宋体', 8), width=30, height=2)
lt.place(x=10, y=160)
lt2 = tk.Label(window, text='检查本地的MongoDB服务是否启动', fg='red', font=('宋体', 8), width=30, height=2)
lt2.place(x=10, y=190)
ls.place(x=35, y=65)
b1.place(x=80, y=40)
b2.place(x=80, y=80)
b3 = tk.Button(window, text="打开首页", command=open_home)
b3.place(x=120, y=120)
b4 = tk.Button(window, text=" 退 出 ", command=exit_CS)
b4.place(x=70, y=120)
b5 = tk.Button(window, text=" 重 置 ", command=reset_count)
b5.place(x=10, y=120)
window.geometry("200x230+600+300")
window.mainloop()
if __name__ == '__main__':
main()