├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── config.py ├── controller ├── admin │ └── sys_user.py └── apis.py ├── database ├── orm_tool.py ├── orms.py ├── sql_bak │ └── testdb.sql └── sys_user.py ├── db_migrate_manager.py ├── gunicorn_config.py ├── log └── red_flask_access.log ├── redflask.md ├── requirements.txt ├── res ├── cache │ └── readme.md ├── help.txt ├── prefab │ └── readme.md └── upload │ └── readme.md ├── run.py ├── server.crt ├── server_nopwd.key ├── service ├── README.md └── admin │ └── sys_user_manager.py ├── start.sh ├── test.sh ├── test_case └── admin │ └── sys_user.py └── utils ├── common ├── encrypt.py ├── logger.py ├── regex_matcher.py ├── text_similarity.py ├── xls_tool.py └── zipper.py ├── decorator ├── dasyncio.py └── oauth2_tool.py ├── http └── responser.py └── msg_queue ├── celery_base.py ├── celery_dojob.py ├── rmq_recieve.py └── rmq_send.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | __pycache__/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 redtree.chs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # red_flask 2 | 3 | #### 项目介绍 4 | 5 | Restful framework base on flask without blueprint. 6 | 一个基于Flask搭建的Restful_Api服务框架,实现了路由分离的功能但无需依赖于Flask的蓝本。提供了常用的http请求类型和文件传输类型的相关接口。 7 | 提供了较简单的异步任务分发方案。提供了最常用的用户密码校验模块和token验证模块(集成Oauth2)。集成了Gunicorn+Gevent的服务器自动配置方案。 8 | 数据库ORm使用flask_sqlalchemy,迁移工具使用flask_migrate(旧版本的sqlalchemy已弃用) 9 | 提供了Flask作为Https服务的解决方案。解决了Flask跨域问题。双系统生产化部署(windows/linux)。 10 | 11 | #### 通过PIP快速构建redflask工程【Only For LINUX】 12 | 13 | 1. pip install redflask==0.1.5 14 | 2. redflask -b [your_project_name] 15 | 16 | #### 通过github获取源码 17 | 18 | 1. 使用 git clone 命令将项目复制到本地 19 | 2. pip install -r requirements.txt 安装所有依赖 20 | 21 | #### 使用说明 22 | 23 | 1. 本地调试,直接python run.py (请在config.py中配置WEB_IP为localhost) 24 | 2. 部署调试,chmod +x test.sh 然后 ./test.sh 25 | 3. 部署生产, chmod +x start.sh 然后 ./start.sh 26 | 27 | #### 工程目录架构 28 | 29 | 30 | controller(目录) : 路由控制器,用来注解并分配工程的api地址和分发对应的接口任务, 31 | 其作用类似于Java SpringBoot 中的restcontroller.子下的文件目录根据对应业务类型进行区分 32 | 33 | database(目录): mysql-orm 文件目录 34 | 其中orms.py用于注册所有使用到的model, 35 | orm_tool.py用来辅助生成orm的__init__/__repr__方法 36 | 37 | init_settings(目录): 放置系统初始化数据的配置文件和脚本。如系统菜单/字典/表单/流程的配置 38 | 39 | log(目录): 用于存放服务生成的日志文件 40 | 41 | migrations: flask_migrate生成的文件目录,用于管理数据库版本文件和迁移脚本 42 | 43 | res : 如果采用本地化对象资源存储,则使用此目录,若使用云对象存储服务器,则忽略。 44 | cache : 存放系统生成的缓存文件 45 | prefab : 存放系统预置的多媒体文件和文档数据 46 | upload : 存放用户上传的多媒体文件或文档数据 47 | 48 | service: 用于编写API实际业务逻辑的文件目录, 49 | Controller下的api接口一般会从此处调用具体服务, 50 | 其目录结构与controller的API业务对应。 51 | 52 | test_case : 测试案例写在这里,请保持目录结构与controller的API业务对应。 53 | 54 | utils: 泛用工具类及全局装饰器的存储目录。 55 | common : 如编码规则生成器,加解密工具,算法等第三方常用拓展 56 | decorator : 常用的全局装饰器,如自定义异步任务/Oauth2接口认证等 57 | http : 基于flask的http-requeset请求,实现get/post方法的自动参数匹配和请求返回错误码自定义。规范联调。 58 | 59 | __init.py__ : flask_app的工程初始化配置文件,常用的flask服务组件需在此全局初始化。 60 | 另外,由于取消了Flask蓝图的使用,所以controller中的业务接口需要在此注册才能生效。 61 | 62 | config.py : 用于部署Flask工程的基础配置文件,包含本地部署配置/数据库连接配置/数据查询习惯配置等 63 | 64 | db_migrate_manager : 数据库迁移脚本控制器 65 | 66 | gunicorn_config.py : Linux环境下,使用Gunicorn部署Flask的配置脚本 67 | 68 | requirements.txt 记录依赖模块 方便pip安装 69 | 70 | run.py 服务启动入口,wsgi配置文件 71 | 72 | start.sh 服务启动脚本 73 | 74 | test.sh 测试脚本 75 | 76 | #### 参与贡献 77 | 78 | 感谢吴航/郑松银/黄应飞 提供的部分架构思路 79 | 80 | #### 版本更新日志 81 | 82 | 20190802 83 | 1 添加常用的正则校验工具 utils/common/regex_matcher.py 84 | 2 添加redis常用的操作函数 utils/common/redisor.py 85 | 3 添加文本相似度通用算法工具 utils/common/text_similarity.py 86 | 4 添加xls,excel文本读写工具 utils/common/xls_tool.py 87 | 5 添加Flask框架对用户请求的参数校验工具及错误类型封包 utils/http/responser.py 88 | 20191202 89 | 1 添加常用的消息队列、分布式任务框架案例,celery+rabbitMQ。 utils/msg_queue 90 | 2 添加一个快捷日志纪录工具。 utils/common/logger.py 91 | 20200416 92 | 1 service中添加了一个常规的系统用户表CRUD操作,并在controller中实现了对应接口,对应的数据库已转存为.sql文件存放在 database/sql_bak目录下 93 | 2 utils/decorator 目录下添加了目前最常用的基于token的Oauth2认证装饰器,区别于之前将token存储于浏览器cookie中的方案,客户端可以自由选择如何存储token. 94 | 3 contorller/service 中 提供了使用http_resonser的案例,可以对接口传参进行自动校验并返回对应的错误码。 95 | 4 添加zip解析工具 96 | 5 为windows环境下的生产化部署提供了新的方案,基于tornado做一层转发,性能未知。 97 | 6 配置文件新增了部署端口、数据库常量配置、系统超时时间、加密方式等字段 98 | 20210412 99 | 1 ORM舍弃原生的sqlalchemy,改用flask_sqlalchemy,原因是flask_sqlalchemy在自动回收数据库连接方面做的更好 100 | 2 数据库迁移工具弃用alembic,改用flask_migrate,使用起来更简洁。 101 | 3 修改了项目架构,去除了一些过分细节化的模块。只保留重要的演示模块。 102 | 4 用户表预置接口只保留登录功能,删除了原本的增删改查业务(懒得写了) 103 | 5 提醒:redflask.md 中提到的设计理念很多已经被我自己推翻了,虽然应该也没什么人看。 104 | 105 | ## 设计理念 106 | 107 | 我并不打算把这个框架做的很细致全面,尽管他在企业级应用中已经有了许多复杂的变化。 108 | 我只是想分享这个框架的设计思路,所以尽量保留一个简洁的版本。 109 | 框架的目的是使多人合作开发的过程中,每个人能很清楚代码的结构,快速定位自己coding的模块。 110 | 111 | ## 结束语 112 | 113 | 接下来这个框架可能再也不会更新了,纪念我的python-coding之路。-----20210412 114 | 115 | 116 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Flask 初始化配置文件,包含基本配置,ORM,数据缓存,模型预加载,控制器注册等,注意业务逻辑顺序 4 | ''' 5 | 6 | import subprocess 7 | from flask_cors import CORS # 添加CORS组件 允许跨域访问 8 | from flask import Flask, request, make_response, send_from_directory 9 | from flask_sqlalchemy import SQLAlchemy 10 | from flask_sockets import Sockets 11 | from flask_migrate import Migrate,MigrateCommand 12 | import platform 13 | import config 14 | from flask_script import Manager 15 | 16 | ''' 17 | Flask基础配置部分 18 | ''' 19 | print('初始化Flask—APP,加载相关组件,启动账号管理服务v.01') 20 | # 初始化Flask对象 21 | 22 | app = Flask(__name__) 23 | # 初始化session 会话 需要配置key 24 | app.secret_key = '\xca\x0c\x86\x04\x98@\x02b\x1b7\x8c\x88]\x1b\xd7"+\xe6px@\xc3#\\' 25 | # 实例化 cors 26 | CORS(app, supports_credentials=True) 27 | app.config.from_object(config) 28 | db = SQLAlchemy(app) 29 | migrate = Migrate(app, db) 30 | # 子命令 MigrateCommand 包含三个方法 init migrate upgrade 31 | manager = Manager(app) 32 | manager.add_command('db', MigrateCommand) 33 | sockets = Sockets(app) 34 | 35 | SYS_ENV = 'Windows' 36 | if(platform.system()=='Windows'): 37 | SYS_ENV='Windows' 38 | elif(platform.system()=='Linux'): 39 | SYS_ENV='Linux' 40 | # 其他组件注册 41 | request = request 42 | make_response = make_response 43 | send_from_directory = send_from_directory 44 | 45 | 46 | 47 | ''' 48 | 适应性配置,方便本地调试和部署线上 49 | ''' 50 | print('读取适应性配置') 51 | # 获取服务器基本配置信息 52 | 53 | # 获取部署目录,ROOT_PATH即可作为工程中的绝对路径根目录,方便业务逻辑调用 os.popen会导致pyinstaller打包无窗口模式后报错 54 | ROOT_PATH= subprocess.Popen("chdir",shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.read() 55 | ROOT_PATH=str(ROOT_PATH,encoding="utf-8") 56 | ROOT_PATH =str(ROOT_PATH).replace('\n', '') 57 | 58 | ''' 59 | 数据库对象的创建和预加载,包含Redis/MongoDB等皆应在此提前实例化,如果需要从resource预先缓存数据, 60 | 例如读取txt/csv等文件,也可以在此处预先加载,以供全局调用。 61 | ''' 62 | from database import orms 63 | 64 | ''' 65 | 路由注册 66 | ''' 67 | from controller import apis 68 | from controller.admin import sys_user #系统账号相关服务 -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @ Time : 2020/9/8 14:56 3 | # @ Author : Redtree 4 | # @ File : config.py 5 | # @ Desc : 6 | 7 | 8 | VERSION = 'V0.1' 9 | 10 | ''' 11 | database_setting 12 | ''' 13 | 14 | DIALCT = "mysql" 15 | DRIVER = "pymysql" 16 | USERNAME = "mysql-username" 17 | PASSWORD = "123456" 18 | HOST = "数据库连接地址" 19 | PORT = "数据库服务端口" 20 | DATABASE = "testdb" 21 | DB_URI = "{}+{}://{}:{}@{}:{}/{}?charset=utf8".format(DIALCT,DRIVER,USERNAME,PASSWORD,HOST,PORT,DATABASE) 22 | SQLALCHEMY_DATABASE_URI = DB_URI 23 | SQLALCHEMY_POOL_SIZE = 5 24 | SQLALCHEMY_POOL_TIMEOUT = 30 25 | SQLALCHEMY_POOL_RECYCLE = 3600 26 | SQLALCHEMY_MAX_OVERFLOW = 5 27 | SQLALCHEMY_TRACK_MODIFICATIONS = False 28 | ''' 29 | Flask-SQLAlchemy有自己的事件通知系统,该系统在SQLAlchemy之上分层。为此,它跟踪对SQLAlchemy会话的修改。 30 | 这会占用额外的资源,因此该选项SQLALCHEMY_TRACK_MODIFICATIONS允许你禁用修改跟踪系统。 31 | 当前,该选项默认为True,但将来该默认值将更改为False,从而禁用事件系统。 32 | ''' 33 | 34 | ''' 35 | WEB SETTINT 36 | ''' 37 | WEB_IP = 'localhost' 38 | WEB_PORT = '5000' 39 | #字符串编码格式 40 | STRING_CODE = 'utf-8' 41 | #加密方式名 42 | ENCRYPTION_SHA1 = 'sha1' 43 | #token过期时间配置(默认一周 604800/测试的时候5分钟) 44 | TOKEN_EXPIRE = 604800 45 | 46 | ''' 47 | db-select-habit 48 | ''' 49 | 50 | USER_SALT_LENGTH = 4 51 | PAGE_LIMIT = 10 52 | DEFAULT_PAGE = 1 -------------------------------------------------------------------------------- /controller/admin/sys_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @ Time : 2020/4/16 10:55 3 | # @ Author : Redtree 4 | # @ File : sys_user 5 | # @ Desc : 6 | 7 | 8 | from __init__ import app 9 | from __init__ import request 10 | from service.admin import sys_user_manager 11 | from utils.http import responser 12 | from utils.decorator.oauth2_tool import oauth2_check 13 | 14 | 15 | @app.route('/login', endpoint='login', methods=['POST']) 16 | def check_login(): 17 | ''' 18 | username string 用户名 19 | password string 密码 20 | ''' 21 | res_status, rjson = responser.post_param_check(request, ['username', 'password']) 22 | if res_status == 'success': 23 | return sys_user_manager.check_login(rjson['username'], rjson['password']) 24 | else: 25 | return rjson 26 | 27 | 28 | @app.route('/auto_login', endpoint='auto_login', methods=['POST']) 29 | @oauth2_check 30 | def auto_login(): 31 | ''' 32 | action_username 执行用户 33 | access_token 认证令牌 34 | ''' 35 | res_status, rjson = responser.post_param_check(request, ['action_username', 'access_token']) 36 | if res_status == 'success': 37 | return sys_user_manager.auto_login(rjson['action_username'], rjson['access_token']) 38 | else: 39 | return rjson 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /controller/apis.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | flask作为restful服务时,常用的一些接口类型 4 | 5 | 在实际产品的操作中,应该在业务逻辑的执行写在service目录下,在通过controller进行对应的调用, 6 | 调用之前利用utils/http_response.py封装 的get_param_check()函数 或 post_param_check()函数进行参数校验。 7 | 8 | ''' 9 | 10 | 11 | 12 | from __init__ import app 13 | from __init__ import request,send_from_directory 14 | import json 15 | from utils.decorator.oauth2_tool import oauth2_check 16 | 17 | #最简单的接口 18 | @app.route('/',methods=['GET']) 19 | def welcome(): 20 | return 'Hello World' 21 | 22 | #GET方式传递参数 23 | @app.route('/api/get',methods=['GET']) 24 | def get1(): 25 | page = request.args.get('page') 26 | return 'this is page'+str(page) 27 | 28 | #POST方式传递参数 29 | @app.route('/api/post',methods=['POST']) 30 | def post1(): 31 | # request.json 只能够接受方法为POST、Body为raw,header 内容为 application/json类型的数据 32 | # json.loads(request.dada) 能够同时接受方法为POST、Body为 raw类型的 Text 33 | username = request.json('username') 34 | password = json.loads(request.data)['password'] 35 | if username=='admin' and password == 'admin': 36 | res = {'code':200,'msg':'suceess','data':username} 37 | return json.dumps(res) 38 | res = {'code': 401, 'msg': 'error'} 39 | return json.dumps(res) 40 | 41 | #POST方式传递文件流 42 | @app.route('/api/uploadfile',methods=['POST']) 43 | def uploadfile(): 44 | try: 45 | f = request.files['file'] 46 | #f.save('path') 47 | # 获取文件名 48 | fname = request.form.get("fname") 49 | res = {'code': 200, 'msg': 'suceess', 'fname': fname} 50 | return json.dumps(res) 51 | except Exception as err: 52 | print(err) 53 | res = {'code': 401, 'msg': 'error'} 54 | return json.dumps(res) 55 | 56 | #提供下载 57 | @app.route('/api/download',methods=['GET']) 58 | def download(): 59 | return send_from_directory('/directory', 'file', as_attachment=True) 60 | 61 | #当使用auth_check装饰时,接口就需要通过token校验。可用来实现登录状态控制等功能, 62 | # 另外,当工程中含有多个auth_check装饰的接口时,需要添加不同的节点命名 63 | 64 | @app.route('/api/get1',endpoint='getn1' ,methods=['GET']) 65 | @oauth2_check 66 | def get1a(): 67 | page = request.args.get('page') 68 | return 'this is page'+str(page) 69 | 70 | @app.route('/api/get2',endpoint='getn2' ,methods=['GET']) 71 | @oauth2_check 72 | def get2a(): 73 | page = request.args.get('page') 74 | return 'this is page'+str(page) 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /database/orm_tool.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 18-11-7 下午3:56 3 | # @Author : Redtree 4 | # @File : orm_tool.py 5 | # @Desc : ORM 文件编写辅助工具,快速生成__init__/__repr__定制,有空再升级. 6 | 7 | 8 | def dojob(): 9 | 10 | #class_data中填入orm_class的基础属性 11 | 12 | class_data = ''' 13 | uuid = Column(Integer, primary_key=True) 14 | username = Column(String(50)) 15 | nickname = Column(String(50)) 16 | salt = Column(String(16)) 17 | password=Column(String(32)) 18 | del_flag= Column(Integer) 19 | created_time=Column(Integer) 20 | updated_time=Column(Integer) 21 | created_user = Column(String(50)) 22 | ''' 23 | 24 | ''' 25 | 执行dojob函数,获得如下输出: 26 | 27 | def __repr__(self): 28 | obj={ 29 | "uuid" : self.uuid, 30 | "username" : self.username, 31 | "nickname" : self.nickname, 32 | "salt" : self.salt, 33 | "password" : self.password, 34 | "del_flag" : self.del_flag, 35 | "created_time" : self.created_time, 36 | "updated_time" : self.updated_time, 37 | "created_user" : self.created_user 38 | } 39 | return json.dumps(obj) 40 | 41 | Process finished with exit code 0 42 | 43 | ''' 44 | 45 | lines = class_data.split('\n') 46 | #initlist 47 | 48 | r2 = " def __repr__(self):" 49 | print(r2) 50 | r3 = " obj={" 51 | print(r3) 52 | 53 | new_lines = [] 54 | for l in lines: 55 | if l.__contains__('='): 56 | new_lines.append(l) 57 | ll = len(new_lines) 58 | index = 0 59 | for l in new_lines: 60 | if l.__contains__('='): 61 | name = l.split('=')[0].replace(' ','') 62 | index+=1 63 | if index>> 60 | 61 | #四、 快速构建应用 62 | 如果你对完全未接触过Flask,建议先跟着官方文档敲下案例。 63 | Flask官方网站:https://palletsprojects.com/p/flask/ 64 | 中文版文档:https://dormousehole.readthedocs.io/en/latest/ 65 | 66 | 在掌握Flask各个基础模块的使用后,我们再对Flask的工程结构进行梳理。不难发现,Flask最初的设计是趋向于构建一个单文件应用的,但在实际开发中,将代码分割到多个文件进行管理则更为合理。在不加入蓝图(flask官方的一个拓展模块)的情况下,较难实现分离式项目结构。 67 | 68 | 如果选择加入blueprint来构建你的Flask项目,那么参照官方文档的说明即可。blueprint本质上是通过注册一个全局__name__参数并绑定相应的路由和模板,然后通过内置钩子函数在Flask应用生成前注册到全局视图层之中。由于是静态配置,一旦载入后,蓝图将不支持拔插,也就是说注册蓝图生成的资源目录和组件都会随着Flask应用的启动而一直存在,不管你是否去调用。 69 | 70 | 由于本教程针对的是构建restful服务,Flask仅作为提供api的服务,而不执行模板渲染的任务,也就是说不会涉及到视图层的管理,所以我提供了一种新设计方案,并将模板工程上传到了github,同时将快速构建工具上传到了pip官方源。 71 | 72 | 工程源码:https://github.com/redtreeai/red-flask 73 | 快速构建工具:redflask 74 | 75 | 在配置有python标准开发环境的机器上,通过终端输入命令来快速构建一个red-flask工程。 76 | 77 | pip install redflask==0.1.5 78 | redflask -b [your_project_name] 79 | 80 | 基于这个示范工程,我会讲解一种较容易上手的Flask企业项目基础架构方案。 81 | 82 | #五、 无蓝本路由分离式架构 83 | 84 | 如果你已经构建了一个redflask工程,并且通过IDE工具(推荐使用pycharm/vscode)打开,你会看到工程结果,如下图: 85 | 86 | ![redflask](https://redtreeblog-1253690989.cos.ap-guangzhou.myqcloud.com/flask/1.png) 87 | 88 | 通过工程的readme文件我们看到整体的架构说明如下: 89 | 90 | 一个基于Flask搭建的Restful_Api服务框架,实现了路由分离的功能但无需依赖于Flask的蓝本。提供了常用的http请求类型和文件传输类型的相关接口。提供了较简单的异步任务分发方案。提供了最常用的用户密码校验模块和token验证模块。集成了Gunicorn+Gevent的服务器自动配置方案。数据库端使用Sqlalchemy和Redis。提供了Flask作为Https服务的解决方案。解决了Flask跨域问题。 91 | 92 | #### 使用说明 93 | 94 | 1. 本地调试,直接python run.py (请在setting.yaml中配置IP为localhost) 95 | 2. 部署调试,chmod +x test.sh 然后 ./test.sh 96 | 3. 部署生产, chmod +x start.sh 然后 ./start.sh 97 | 98 | #### 软件架构 99 | 100 | controller : 路由控制器,用来注解并分配工程的api地址和分发对应的接口任务,其作用类似于Java SpringBoot中的restcontroller。子下的文件目录根据对应业务类型进行区分。 101 | database: 包含各类数据库链接和对象映射的基本配置,以及数据库其他模式(如主从、池化、分布式等)的配置都在此处生效。 102 | Exceptions: 用来自定义继承或重写python的错误类型,并编写特定的处理方式。 103 | Resource: 用于存放工程中使用到的各类静态文本数据和多媒体数据,还有机器学习模型。 104 | Service: 用于编写API实际业务逻辑的文件目录,Controller下的api接口一般会从此处调用具体服务,其目录结构与controller相似。 105 | Utils: 泛用工具类及全局装饰器的存储目录。 106 | 其他工程文件详解: 107 | test_case 测试案例写在这里 108 | __init__.py red_flask工程的基本配置文件,各项配置须在此初始化 109 | requirements.txt 记录依赖模块 方便pip安装 110 | run.py 服务启动入口,wsgi配置文件 111 | start.sh 服务启动脚本 112 | test.sh 测试脚本 113 | configm.py Gunicorn网络模型配置脚本 114 | setting.yaml 用于自适应部署的配置文件 115 | 116 | 本章我们先重点讲解__init__.py,controller/apis.py这两个文件。前者这是整个flask工程的初始化配置文件,后者是flask实现路由分离后接口控制器的标准形式文件。 117 | 118 | __init__.py(markdown语法问题,此处省略下划线) 119 | 120 | # -*- coding: utf-8 -*- 121 | ''' 122 | Flask 初始化配置文件,包含基本配置,ORM,数据缓存,模型预加载,控制器注册等,注意业务逻辑顺序 123 | ''' 124 | from flask_cors import CORS #添加CORS组件 允许跨域访问 125 | from flask import Flask, request,make_response,send_from_directory 126 | import os 127 | import yaml 128 | 129 | ''' 130 | Flask基础配置部分 131 | ''' 132 | # 初始化Flask对象 133 | app = Flask(__name__) 134 | # 初始化session 会话 需要配置key 135 | app.secret_key = '\xca\x0c\x86\x04\x98@\x02b\x1b7\x8c\x88]\x1b\xd7"+\xe6px@\xc3#\\' 136 | #实例化 cors 137 | CORS(app, supports_credentials=True) 138 | #其他组件注册 139 | request = request 140 | make_response = make_response 141 | send_from_directory = send_from_directory 142 | 143 | ''' 144 | 适应性配置,方便本地调试和部署线上 145 | ''' 146 | #获取服务器基本配置信息 147 | setting = yaml.load(open('setting.yaml')) 148 | SERVER_IP = setting['SERVER_IP'] 149 | 150 | #获取部署目录,ROOT_PATH即可作为工程中的绝对路径根目录,方便业务逻辑调用 151 | ROOT_PATH = os.popen('pwd','r',1).read() 152 | ROOT_PATH = str(ROOT_PATH).replace('\n','') 153 | 154 | ''' 155 | 数据库对象的创建和预加载,包含Redis/MongoDB等皆应在此提前实例化,如果需要从resource预先缓存数据, 156 | 例如读取txt/csv等文件,也可以在此处预先加载,以供全局调用。 157 | ''' 158 | 159 | # from sqlalchemy import create_engine #加载配置文件内容 160 | # from sqlalchemy.ext.declarative import declarative_base 161 | # from sqlalchemy.orm import sessionmaker 162 | # 163 | # from database.sqlalchemy import _mysql # 连接数据库的数据 164 | # 165 | # engine_mysql = create_engine(_mysql.DB_URI, echo=False, pool_recycle=3600) # 创建引擎 166 | # Base_mysql = declarative_base(engine_mysql) 167 | # DBSession_mysql = sessionmaker(bind=engine_mysql) # sessionmaker生成一个session类 此后DBSession_mysql将可在全局作为一个数据库会话对象持续服务,不用重复创建 168 | # 169 | 170 | ''' 171 | 所有的控制器在此处注册方可生效 172 | ''' 173 | #注册控制器 174 | from controller import apis 175 | 176 | 可以看到,该Flask应用的实例化过程遵循一个基本原则,先声明并实例化基本库,然后载入第三方插件或数据集,最后再将接口控制器进行注册生效。这样的设计方式可以让Flask在服务启动前预载入一些会在后期被频繁调用的对象函数或数据到内存中,实现复用效果,而不用频繁重新实例化。另外控制器能在文件底部进行一个集成管理,免去了繁琐的蓝本注册方案。无需在路由文件和初始文件中配置注册蓝本即可实现路由分离的功能。原理相当于是通过python的path管理将Flask工程重新打包成一个单文件应用。 177 | 178 | apis.py 179 | 180 | # -*- coding: utf-8 -*- 181 | ''' 182 | flask作为restful服务时,常用的一些接口类型 183 | ''' 184 | from __init__ import app 185 | from __init__ import request,send_from_directory 186 | import json 187 | from utils.decorator.token_maneger import auth_check 188 | 189 | #最简单的接口 190 | @app.route('/',methods=['GET']) 191 | def welcome(): 192 | return 'Hello World' 193 | 194 | #GET方式传递参数 195 | @app.route('/api/get',methods=['GET']) 196 | def get1(): 197 | page = request.args.get('page') 198 | return 'this is page'+str(page) 199 | 200 | #POST方式传递参数 201 | @app.route('/api/post',methods=['POST']) 202 | def post1(): 203 | # request.json 只能够接受方法为POST、Body为raw,header 内容为 application/json类型的数据 204 | # json.loads(request.dada) 能够同时接受方法为POST、Body为 raw类型的 Text 205 | username = request.json('username') 206 | password = json.loads(request.data)['password'] 207 | if username=='admin' and password == 'admin': 208 | res = {'code':200,'msg':'suceess','data':username} 209 | return json.dumps(res) 210 | res = {'code': 401, 'msg': 'error'} 211 | return json.dumps(res) 212 | 213 | #POST方式传递文件流 214 | @app.route('/api/uploadfile',methods=['POST']) 215 | def uploadfile(): 216 | try: 217 | f = request.files['file'] 218 | #f.save('path') 219 | # 获取文件名 220 | fname = request.form.get("fname") 221 | res = {'code': 200, 'msg': 'suceess', 'fname': fname} 222 | return json.dumps(res) 223 | except Exception as err: 224 | print(err) 225 | res = {'code': 401, 'msg': 'error'} 226 | return json.dumps(res) 227 | 228 | #提供下载 229 | @app.route('/api/download',methods=['GET']) 230 | def download(): 231 | return send_from_directory('/directory', 'file', as_attachment=True) 232 | 233 | #当使用auth_check装饰时,接口就需要通过token校验。可用来实现登录状态控制等功能, 234 | # 另外,当工程中含有多个auth_check装饰的接口时,需要添加不同的节点命名 235 | 236 | @app.route('/api/get1',endpoint='getn1' ,methods=['GET']) 237 | @auth_check 238 | def get1a(): 239 | page = request.args.get('page') 240 | return 'this is page'+str(page) 241 | 242 | @app.route('/api/get2',endpoint='getn2' ,methods=['GET']) 243 | @auth_check 244 | def get2a(): 245 | page = request.args.get('page') 246 | return 'this is page'+str(page) 247 | 248 | apis.py文件提供了Flask工程在路由分离结构下,控制器文件的标准写法。并提供了部分常用功能的接口范例。从文件中我们可以看到,apis.py先从__init__.py中引入了实例化的Flask应用和基础模块对象,同时,引入了python系统模块组的json库和utils目录下用户自定义的第三方工具类,再去执行接口相关的业务逻辑。从设计角度上来说,控制器尽量只负责为service层(业务逻辑层)转发用户上报到接口的数据并返回处理结果,而不直接在控制器中执行业务逻辑。比如我们在控制器中编写了一个校验用户登录信息的接口,那么实际上他只需要把用户的帐号密码传递给来自service目录下对应的服务文件就行了,然后返回service文件对应函数的处理结果。 249 | 250 | 在实际的业务场景中,controller目录下建议再进行多级目录分类,然后再根据需求注册到主文件中。那么,在实现路由分离的场景下,整个系统的基础还少了哪个部分呢?没错,就是server的启动了,由于python的web_server大都遵循wsgi协议,所以我们把flask工程的启动部分也单独分离出来编写,以便整合调试不同的网络模型。在redflask工程中,由run.py和configm.py这两个文件来控制工程的调试和部署。 251 | 252 | run.py 253 | 254 | # -*- coding: utf-8 -*- 255 | # @File : run.py 256 | # @Author: redtree 257 | # @Date : 18-6-27 258 | # @Desc : 服务启动入口,自动化判断部署环境并配置UWGI代理, 259 | 260 | from __init__ import app 261 | from __init__ import SERVER_IP 262 | 263 | if SERVER_IP=='localhost': 264 | #本地调试 265 | app.run(host='0.0.0.0', port=5000, debug=True, threaded=True) 266 | #app.run() 267 | # ssl_context = ( 268 | # './server.crt', 269 | # './server_nopwd.key') 270 | else: 271 | from werkzeug.contrib.fixers import ProxyFix 272 | 273 | # 线上服务部署 对接gunicorn 274 | app.wsgi_app = ProxyFix(app.wsgi_app) 275 | 276 | 由于flask在作为本地调试服务时,采用的是自带的werkzeug服务器,而部署线上时,一般采用gunicorn或者uwsgi作为代理,所以我们需要配置一套自动部署的方案。在之前的__init__.py文件中,我们通过setting.yaml文件获取到了当前机器的配置信息,从而使得Flask启动时能够自动识别应该开启调试模式还是生产模式,当属于生产模式时,我们应该将werkzeug服务器挂载为代理转发模式,对接到生产类服务器上,如gunicorn。 277 | 278 | 当部署生产环境时,网络优化的任务则会交由configm.py文件进行调控,再后面章节中细讲。 279 | 280 | #六、 关于数据预加载和内存消耗 281 | 之前提到,Flask工程可以把模块、数据集都在服务启动前预加载到内存之中,类似机器学习生成的二进制模型之类,通常我们会伴随着服务启动变将其载入内存,加快后面接口执行相关任务时的资源调用速度。由于flask本身内核的内存资源占用极小,合理的运用这项行为可以提高项目的整体服务性能。 282 | 283 | 在Flask工程中,通常我会把需要预加载的资源放到resource目录下,包含一些字典文件、静态资源和多媒体资源等。然后在database目录下注册一个用于管理静态资源的模块,假设为res_manager.py。这个模块可以将资源进行分类读取,转换成列表或字典的形式存到内存中,然后在__init__.py文件中实例化供全局调用。 284 | 285 | 同理,数据库类型的资源也可以使用这样的方案,只要你能通过python支持数据库交互,包含mysql,mongoDB,redis等。 286 | 287 | #七、数据库的开放性选择 288 | redflask提供了两种数据库的简易交互方案,redis和mysql。我们先看redis: 289 | 290 | ![dababase](https://redtreeblog-1253690989.cos.ap-guangzhou.myqcloud.com/flask/2.png) 291 | 292 | redis.py 293 | 294 | # 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库 295 | #当redis IO操作较为频繁的时候,应将redis对象在FLask工程中全局初始化。 296 | import redis 297 | 298 | def connect_redis(): 299 | # host是redis主机,需要redis服务端和客户端都启动 redis默认端口是6379 300 | r = redis.Redis(host='localhost', port=6379, decode_responses=True) 301 | return r 302 | 303 | def add_kv(redis_obj,r_key,r_value): 304 | redis_obj.set(r_key, r_value) 305 | return 'success' 306 | 307 | def get_by_key(redis_obj,r_key): 308 | print(redis_obj[r_key]) 309 | print(redis_obj.get(r_key)) # 取出键name对应的值 310 | print(type(redis_obj.get(r_key))) 311 | return 'success' 312 | 313 | 然后是mysql,这里我们选择了sqlalchemy这个python最通用的orm框架。 314 | 315 | _mysql.py 316 | 317 | # -*- coding: utf-8 -*- 318 | ''' 319 | 基于sqlalchemy模块的orm基础配置,当性能不佳时,建议使用原生sql语句重写核心模块 320 | ''' 321 | 322 | #调试模式是否开启 323 | DEBUG = False 324 | 325 | SQLALCHEMY_TRACK_MODIFICATIONS = False 326 | #session必须要设置key 327 | SECRET_KEY='~XHH!jmN]LWX/,?RTA0Zr98j/3yX R' 328 | 329 | #mysql数据库连接信息,这里改为自己的账号 #直连模式启动 330 | HOSTNAME = '127.0.0.1' 331 | PORT = '3310' 332 | DATABASE = 'dbname' 333 | USERNAME = 'root' 334 | PASSWORD = 'root' 335 | #链接模式 336 | DB_URI = 'mysql+pymysql://{}:{}@{}:{}/{}?charset=utf8'.format(USERNAME,PASSWORD,HOSTNAME,PORT,DATABASE) 337 | 338 | 在配置好数据库的链接信息后,便可以根据业务表建立对象模型文件,如sys_user.py: 339 | 340 | import json 341 | from __init__ import Base_mysql 342 | from sqlalchemy import (Column, String, Integer) 343 | 344 | # 定义好一些属性,与user表中的字段进行映射,并且这个属性要属于某个类型 345 | class Sys_user(Base_mysql): 346 | __tablename__ = 'sys_user' 347 | 348 | xid = Column(Integer, primary_key=True) 349 | userid = Column(String(50)) 350 | nickname = Column(String(50)) 351 | salt = Column(String(16)) 352 | password=Column(String(32)) 353 | del_flag= Column(Integer) 354 | create_time=Column(Integer) 355 | update_time=Column(Integer) 356 | role = Column(String(50)) 357 | create_user = Column(String(50)) 358 | ban_flag = Column(Integer) 359 | 360 | def __repr__(self): 361 | get_data = {"id":self.id, "userid":self.userid, "nickname":self.nickname, "salt":self.salt, 362 | "password":self.password, "del_flag":self.del_flag, "create_time":self.create_time, "update_time":self.update_time,"role":self.role,"create_user":self.create_user 363 | ,"ban_flag":self.ban_flag} 364 | get_data = json.dumps(get_data) 365 | return get_data 366 | 367 | 在数据库基础配置和表映射都建立好后,我们便可以在init.py中注册并实例化相关组件,然后在各类utils或service文件中调用sqlalchemy组件进行curd操作了。如果遇到性能瓶颈时,建议使用pymysql编写原生sql语句处理业务。 368 | 369 | from sqlalchemy import create_engine #加载配置文件内容 370 | from sqlalchemy.ext.declarative import declarative_base 371 | from sqlalchemy.orm import sessionmaker 372 | 373 | from database.sqlalchemy import _mysql # 连接数据库的数据 374 | 375 | engine_mysql = create_engine(_mysql.DB_URI, echo=False, pool_recycle=3600) # 创建引擎 376 | Base_mysql = declarative_base(engine_mysql) 377 | DBSession_mysql = sessionmaker(bind=engine_mysql) # sessionmaker生成一个session类 此后DBSession_mysql将可在全局作为一个数据库会话对象持续服务,不用重复创建 378 | 379 | #八、 加密/验证等基础模块的实现 380 | 在redflask工程的utils目录下,提供了几项通用工具类,包含加密验证工具和装饰器。 381 | 382 | ![auth](https://redtreeblog-1253690989.cos.ap-guangzhou.myqcloud.com/flask/3.png ) 383 | 384 | 首先是encrypt.py: 385 | 386 | # -*- coding: utf-8 -*- 387 | ''' 388 | 此工具提供了一个常用的用户密码加密方案,基于md5和随机盐值. 389 | ''' 390 | import random 391 | import string 392 | import hashlib 393 | 394 | # 获取由4位随机大小写字母、数字组成的salt值 395 | def create_salt(): 396 | salt = ''.join(random.sample(string.ascii_letters + string.digits, 4)) 397 | return salt 398 | 399 | # 获取原始密码+salt的md5值 400 | def md5_password(password,salt): 401 | trans_str = password+salt 402 | md = hashlib.md5() 403 | md.update(trans_str.encode('utf-8')) 404 | return md.hexdigest() 405 | 406 | 此工具提供了一个通用数据库用户密码加密方案,你可以自定义使其更复杂化,甚至模仿实现类似java,shiro的效果。 407 | 408 | 然后是装饰器部分,先看dasyncio.py: 409 | 410 | # -*- coding: utf-8 -*- 411 | ''' 412 | 基于python Threading模块封装的异步函数装饰器 413 | ''' 414 | from threading import Thread 415 | import time 416 | 417 | ''' 418 | async_call为一次简单的异步处理操作,装饰在要异步执行的函数前,再调用该函数即可执行单次异步操作(开辟一条新的线程) 419 | ''' 420 | def async_call(fn): 421 | def wrapper(*args, **kwargs): 422 | Thread(target=fn, args=args, kwargs=kwargs).start() 423 | return wrapper 424 | 425 | ''' 426 | async_pool为可定义链接数的线程池装饰器,可用于并发执行多次任务 427 | ''' 428 | def async_pool(pool_links): 429 | def wrapper(func): 430 | def sub_wrapper(*args,**kwargs): 431 | for x in range(0,pool_links): 432 | Thread(target=func, args=args, kwargs=kwargs).start() 433 | #func(*args, **kwargs) 434 | return sub_wrapper 435 | return wrapper 436 | 437 | ''' 438 | async_retry为自动重试类装饰器,不支持单独异步,但可嵌套于 call 和 pool中使用 439 | ''' 440 | def async_retry(retry_times,space_time): 441 | def wrapper(func): 442 | def sub_wrapper(*args, **kwargs): 443 | try_times = retry_times 444 | while try_times > 0: 445 | try: 446 | func(*args, **kwargs) 447 | break 448 | except Exception as e: 449 | print(e) 450 | time.sleep(space_time) 451 | try_times = try_times - 1 452 | return sub_wrapper 453 | return wrapper 454 | 455 | # 以下为测试案例代码 456 | # 457 | # @async_call 458 | # def sleep2andprint(): 459 | # time.sleep(2) 460 | # print('22222222') 461 | # 462 | # @async_pool(pool_links=5) 463 | # def pools(): 464 | # time.sleep(1) 465 | # print('hehe') 466 | # 467 | # 468 | # @async_retry(retry_times=3,space_time=1) 469 | # def check(): 470 | # a = 1 471 | # b ='2' 472 | # print(a+b) 473 | # 474 | # def check_all(): 475 | # print('正在测试async_call组件') 476 | # print('111111') 477 | # sleep2andprint() 478 | # print('333333') 479 | # print('若3333出现在22222此前,异步成功') 480 | # print('正在测试async_pool组件') 481 | # pools() 482 | # print('在一秒内打印出5个hehe为成功') 483 | # print('正在测试async_retry组件') 484 | # check() 485 | # print('打印三次异常则成功') 486 | # 487 | # check_all() 488 | 489 | 异步装饰器可以使工程中的任意函数单独开辟一条额外的线程去执行特殊业务,比如用户行为纪录,以确保用户的在执行操作时不受系统纪录行为的结果影响。 490 | 491 | token_manager.py: 492 | 493 | # -*- coding: utf-8 -*- 494 | ''' 495 | 服务端token验证器 496 | ''' 497 | 498 | import time 499 | import base64 500 | import hmac 501 | from __init__ import make_response,request 502 | 503 | #生成token 504 | def generate_token(key, expire=1800): 505 | r''' 506 | @Args: 507 | key: str (用户给定的key,需要用户保存以便之后验证token,每次产生token时的key 都可以是同一个key) 508 | expire: int(最大有效时间,单位为s) 509 | @Return: 510 | state: str 511 | ''' 512 | ts_str = str(time.time() + expire) 513 | ts_byte = ts_str.encode("utf-8") 514 | sha1_tshexstr = hmac.new(key.encode("utf-8"),ts_byte,'sha1').hexdigest() 515 | token = ts_str+':'+sha1_tshexstr 516 | b64_token = base64.urlsafe_b64encode(token.encode("utf-8")) 517 | return b64_token.decode("utf-8") 518 | 519 | #token校验 520 | def certify_token(key, token): 521 | r''' 522 | @Args: 523 | key: str 524 | token: str 525 | @Returns: 526 | boolean 527 | ''' 528 | token_str = base64.urlsafe_b64decode(token).decode('utf-8') 529 | token_list = token_str.split(':') 530 | if len(token_list) != 2: 531 | return False 532 | ts_str = token_list[0] 533 | if float(ts_str) < time.time(): 534 | # token expired 535 | return False 536 | known_sha1_tsstr = token_list[1] 537 | sha1 = hmac.new(key.encode("utf-8"),ts_str.encode('utf-8'),'sha1') 538 | calc_sha1_tsstr = sha1.hexdigest() 539 | if calc_sha1_tsstr != known_sha1_tsstr: 540 | # token certification failed 541 | return False 542 | # token certification success 543 | return True 544 | 545 | #token校验 546 | def auth_check(func): 547 | def inner(*args,**kwargs): 548 | try: 549 | key = request.cookies.get('key') 550 | token = request.cookies.get('token') 551 | 552 | if certify_token(key,token) == True: 553 | pass 554 | else: 555 | return 'Auth-Error' 556 | except Exception as e : 557 | return 'Auth-Error' 558 | return func(*args,**kwargs) 559 | return inner 560 | 561 | 提供了auth_check装饰器,用于校验用户的请求合法性或者客户端cookie的有效期。 562 | 563 | pub_decorator.py中则是提供了一些关于失败任务自动重新调度的解决方案,由于大部分程序在出现异常时会有对应的抛出和解决方案,这里就不细说,感兴趣的小伙伴自行翻看下源码便可理解。 564 | 565 | 值得一提的是,在引入装饰器修饰接口函数时,会涉及到一个问题:flask路由的节点冲突。所以,当有多个接口同时调用一个装饰器时,必须为接口的路由配置不同的节点名,请看下一章。 566 | 567 | #九、节点映射和装饰器的使用 568 | 回到apis.py文件中来,我们看文件的最下面部分。 569 | 570 | #当使用auth_check装饰时,接口就需要通过token校验。可用来实现登录状态控制等功能, 571 | # 另外,当工程中含有多个auth_check装饰的接口时,需要添加不同的节点命名 572 | 573 | @app.route('/api/get1',endpoint='getn1' ,methods=['GET']) 574 | @auth_check 575 | def get1a(): 576 | page = request.args.get('page') 577 | return 'this is page'+str(page) 578 | 579 | @app.route('/api/get2',endpoint='getn2' ,methods=['GET']) 580 | @auth_check 581 | def get2a(): 582 | page = request.args.get('page') 583 | return 'this is page'+str(page) 584 | 585 | flask默认将路由的的函数名作为endpoint的标识名。通常情况下不会出现路由之间节点冲突的问题。但当两个路由都被装饰器修饰时,endpoint会读取到类似匿名函数的节点名从而分配給默认的greeting节点上引发冲突。所以要为不同路由手动添加endpoint。 586 | 587 | #十、并发处理及网络模型的选择 588 | 589 | 大部分web框架在设计时,为了方便本地调试代码,会封装一个简易httpserver,少部分web框架本身就是一个强力的webserver(tonardo)。werkzeug对于Flask来说,便是对wsgi实现了一个简单封装,让flask能专注于application的业务逻辑本身,将网络层的模式设计交由其他模块拓展。在本次教程中,我会对python常用的httpserver及网络模型做个简单介绍,但不会深入讲解,重点依然是让读者快速上手框架的整体结构并用于实战。 590 | 591 | wsgi:Web Server Gateway Interface。 592 | 关于wsgi,这里我摘录了廖大的一段解释,讲得很清楚了。 593 | 594 | (摘录至廖雪峰个人网站) 595 | 了解了HTTP协议和HTML文档,我们其实就明白了一个Web应用的本质就是: 596 | 597 | 浏览器发送一个HTTP请求; 598 | 599 | 服务器收到请求,生成一个HTML文档; 600 | 601 | 服务器把HTML文档作为HTTP响应的Body发送给浏览器; 602 | 603 | 浏览器收到HTTP响应,从HTTP Body取出HTML文档并显示。 604 | 605 | 所以,最简单的Web应用就是先把HTML用文件保存好,用一个现成的HTTP服务器软件,接收用户请求,从文件中读取HTML,返回。Apache、Nginx、Lighttpd等这些常见的静态服务器就是干这件事情的。 606 | 607 | 如果要动态生成HTML,就需要把上述步骤自己来实现。不过,接受HTTP请求、解析HTTP请求、发送HTTP响应都是苦力活,如果我们自己来写这些底层代码,还没开始写动态HTML呢,就得花个把月去读HTTP规范。 608 | 609 | 正确的做法是底层代码由专门的服务器软件实现,我们用Python专注于生成HTML文档。因为我们不希望接触到TCP连接、HTTP原始请求和响应格式,所以,需要一个统一的接口,让我们专心用Python编写Web业务 610 | 611 | 也就是说,wsgi被作为python最通用的http协议接口,大量的python_web_application 与 python_http_server都是基于wsgi构建的。而其中最常用的server便是uwsgi和gunicorn。 612 | 613 | 他们之间的异同点主要如下: 614 | 1 uwsgi和gunicorn 都过 pre-fork 方式增加 server 并发处理能力。 615 | 2 在不采用网络模型优化的情况下,两者皆采用默认形态,在高并发情况下,gunicorn的响应时间会快于uwsgi,但丢包率稍高,且吞吐量较低。 616 | 3 gunicorn的配置较为简单,且具备较多网络模型可选择:select、epoll、gevent、meinheld等。 617 | 4 gunicorn默认仅支持部署unix系统,uwsgi则支持部署windows。 618 | 5 两者都能完美搭配nginx使用。 619 | 620 | 由于gunicorn对gevent和meinheld这两个目前性能最优秀的通用网络模型有较好的支持,且配置启动方式极其简洁,redflask选用gunicorn作为WSGIserver。 621 | 622 | 网络模型的选择: gevent vs meinheld,两者都是基于python的greenlet库实现协程。 623 | 624 | meinheld:greenlet(协程) + picoev(高性能网络库) 625 | gevent:greenlet(协程) + libevent(高性能网络库) 626 | 627 | 协程:又称微线程,纤程。 628 | 协程的这种“挂起”和“唤醒”机制实质上是将一个过程切分成了若干个子过程,给了我们一种以扁平的方式来使用事件回调模型。优点:共享进程的上下文,一个进程可以创建百万,千万的coroutine。 629 | 630 | picoev vs libevent 631 | picoev在项目下有把picoev和libevent这些库做对比,作者也提了一下为什么picoev的速度会这么快。主要有两个原因。 632 | 633 | 1. picoev几乎所有顺序结构都是用数组实现的,索引访问速度比libevent的链表快很多。 634 | 2. picoev采用了环形队列+vector+bitmap来实现定时事件的检测。 635 | 636 | 好了,我们再来看下gevent和meinheld的设计理念不同之处: 637 | 638 | Gevent 639 | 640 | 1. 先通过Timer最小堆(以时间为排序的键)找出至少要等待的时间。(代码中的timeout_next()函数)。 641 | 2. 通过select发送这些事件fd到内核并设置时间为1中所求的等待时间。然后把select返回的就绪事件放到就绪列表。(对应 evsel->dispatch(base, evbase, tv_p))。 642 | 3. 然后把现在超时的计时事件放到就绪列表。(对应gettime(base, &base->tv_cache))。 643 | 4. 最后调用处理函数处理就绪列表中的事件(timeout_process(base))。 644 | 645 | MeinHeld 646 | 647 | 概要:双轮询异步非阻塞模型 648 | MeinHeld是目前python_web网络框架中的性能怪兽,douban、知乎都应用了这个设计模式。 649 | MeinHeld基于Gunicorn的基础上,能根据不同机器的cpu内核性能自动调整worker进程(的数量), 650 | 并且每个worker下的工作子进程也会根据cpu作调整,保持在一个最佳性能区间。以下是meinheld的几个优秀之处: 651 | 1 meinheld的master_worker可设置为damon模式,即守护进程模式。unix系统中,守护进程将拥有最高级的内存使用权限,master_worker可以自动托起挂死进程,重新分配内存。 652 | 2 meinheld 的 双轮巡机制,首先是slave_worker间的轮询,meinheld采用了master和slave间的双向交互,一旦slave进程上报请求结果,将自动进入空闲队列,master在作完记录后优先向空闲进程分配http请求。其次,每个worker也对自己的子线程进行轮询,每轮轮巡完毕后会向master_worker上报空闲thread数量和上次请求完成时间点。 双轮巡的机制能够完美的利用所有线程,达到最佳访问性能。 653 | 654 | (下面是关于meinheld的timeout_队列原理图和一段说明,部分摘自csdn,但原作者不详,若有侵权请联系作者。) 655 | 656 | ![4](https://redtreeblog-1253690989.cos.ap-guangzhou.myqcloud.com/flask/4.jpg) 657 | 658 | 对于timeout环形队列,每经过resolution时间就往后移动一块,当前队头永远指向刚刚到达时间的事件块,如图当前处理的是2,那么说明队列头在2,那么再经过resolution时间就会到3,根据时间不断后移,循环利用。 659 | 1. 在处理每一块timeout里面注册的事件时,遍历所有不为0的vector,得出对应的fd。图中已经写的很清楚的,其实原理和16进制一样简单。插入一个事件的时间复杂度为O(1),遍历所有在timeout块的注册事件时间复杂度等价为O(n)[注:这里n为timeout里面注册事件的个数],对比libevent的最小堆O(logn)插入,每次处理一个后调整堆的复杂度O(logn)处理n个就为O(nlogn),确实是高效很多。 660 | 2. 还有一个高效的地方在于,meinheld是检测到有一个事件就马上处理(无阻塞),不像gevent挂起等待最小等待时间到达(阻塞),然后才对所有就绪事件队列里面的事件进行处理,不过这也导致了meinheld不能设定事件处理的优先级。 661 | 662 | 简单来说就是,master进程将任务分发给上一轮上报后空闲thread数量最多的slaver,接到请求的slaver分发给空闲thread后重新上报给master.反复操作。另外当线程死锁时,master会释放掉slaver重建worker. 663 | 664 | 现在,我们来举个直观的例子,假设现在有一个足球场,有个教练在一边的底线负责给球员传递足球,场上的球员接到球后要将球带到对面的球门里并返回上报结果。 665 | 666 | 1 、当Flask仅作为一个简易server时,可以这么理解: 667 | 只有一个教练和一个球员在执行这项任务。 668 | 2 、当引入gunicorn代理时,假设我们fork4个worker,这时候是这样: 669 | 还是只有一个教练,但多了4个球员来完成这项任务。教练依旧是把球往地上一扔,4个人谁跑得快先搞定任务就拿走下个球。 670 | 3 、当引入gunicorn+gevent模式的时候,我们引入组员的概念,还是有4个球员,但是他们通过自己的手段各自号召了若干个小弟来完成任务: 671 | 这时候教练会纪录每个球员之间完成任务的速度,比如A1分钟4个,B3C2D1这样子,那么教练会优先把球分配给之前做得快球员,可能一次性给多个球过去。 672 | 4 、当使用meinheld时,假设有一个教练,4个组,每组人数若干: 673 | 教练一开始看哪个组的空闲人数比较多,就优先发给那个组的组长,组长接过球后再把球分发给空闲的组员。另外,教练还要充当全程保姆,一旦发现哪个组干活效率出问题了,或者不同的球员踢了同一个球,或者有球员受伤了,他会立马替换新球员。 674 | 675 | OK,说了这么多,我们来看一下如果在redflask工程中编写gunicorn代理的基础配置,这是根目录下的configm.py文件: 676 | 677 | ''' 678 | Gunicorn的配置文件,默认选用gevent模型,有兴趣的话可以自己研究下更强大的meinheld。 679 | ''' 680 | import multiprocessing 681 | 682 | #ssl证书 如果需要部署https协议的服务则使用下列命令 683 | #certfile="server.crt" 684 | #keyfile="server_nopwd.key" 685 | 686 | # 监听本机的5000端口 687 | bind = '0.0.0.0:5000' 688 | 689 | preload_app = True 690 | 691 | # 开启进程 692 | workers=4 693 | #workers = multiprocessing.cpu_count() * 2 + 1 694 | 695 | # 每个进程的开启线程 696 | threads = 10 697 | backlog = 2048 698 | 699 | # 工作模式为meinheld 700 | #worker_class = "egg:meinheld#gunicorn_worker" 701 | #gevent 702 | worker_class = "gevent" 703 | 704 | # debug=True 705 | # 如果不使用supervisord之类的进程管理工具可以是进程成为守护进程,否则会出问题 706 | daemon = False 707 | 708 | # 进程名称 709 | proc_name = 'red_flask.pid' 710 | 711 | # 进程pid记录文件 712 | pidfile = 'app_pid.log' 713 | 714 | loglevel = 'debug' 715 | logfile = 'log/red_flask_debug.log' 716 | accesslog = 'log/red_flask_access.log' 717 | access_log_format = '%(h)s %(t)s %(U)s %(q)s' 718 | 719 | 可以看出,gunicorn的配置文件十分简洁。通过更改worker_class即可在gevent和meinheld中进行模式切换,另外还能根据cpu内核数量进行进程调控,支持https等。 720 | 721 | 在编写完成configm.py后,我们便可以通过gunicorn启动flask的生产服务。 722 | 723 | 1、pip install -r requirements.txt (包含gunicorn、gevent、meinheld等依赖) 724 | 2、终端输入 gunicorn -c configm.py run:app 725 | 3、通常在linux环境下,我们会编写一个shell文件,通过nohup命令将gunicorn服务部署到系统后台。 726 | 727 | #十一、 服务端部署方案详解 728 | 在商用场景中,flask+gunicorn的组合并不够完善,通常我们还需要结合另外两个软件,即Nginx+Supervisor。 729 | 730 | Nginx是一个高性能的Http和反向代理服务器,对wsgiServer完美支持。通常我们会把后端服务挂载在Nginx的端口服务下,让Nginx进行请求转发。同时,Nginx还具备执行静态资源处理,资源限制,gzip,负载均衡等功能,十分强大。 731 | 732 | Supervisor则是一个由python编写的后台进程管理工具,程序托管在supervisor的时候可以实现自重启,缓加载更新等功能。 733 | 734 | 下面就来介绍如何在一台linux服务器上部署redflask服务:(以下以ubuntu为例子) 735 | 736 | 1、通过git或scp将代码上传到服务器。(略) 737 | 2、通过gunicorn启动redflask。(略) 738 | 3、安装部署nginx,并配置代理到gunicorn 739 | 4、安装部署supervisor,并配置gunicorn服务 740 | 741 | **Nginx的对应配置** 742 | 743 | **如何安装** 744 | 745 | 如果您是ubuntu系统,并且不需求nginx的最新特性,那么完全可以通过apt市场进行快速安装。 746 | 安装nginx前,需要先安装相关依赖,包括: 747 | 748 | 安装gcc g++的依赖库 749 | ubuntu平台可以使用如下命令。 750 | 751 | **1 gcc g++ 模块** 752 | 753 | apt-get install build-essential 754 | 755 | apt-get install libtool 756 | 757 | **2 pcre 依赖** 758 | 759 | sudo apt-get update 760 | 761 | sudo apt-get install libpcre3 libpcre3-dev 762 | 763 | **3 zlib 依赖** 764 | 765 | apt-get install zlib1g-dev 766 | 767 | **4 ssl 依赖库** 768 | 769 | apt-get install openssl 770 | 771 | **然后就是** 772 | 773 | sudo apt-get install nginx 774 | 775 | ubuntu会自动帮你完成系统服务的配置,之后可以通过命令: 776 | 777 | **修改nginx配置:** 778 | 779 | sudo vim /etc/nginx/nginx.conf 780 | 781 | **启动服务** 782 | 783 | service nginx start 784 | 785 | 打开浏览器,访问本机ip地址,看到welcome to nginx界面,表示安装成功。 786 | 787 | **NGINX主要功能** 788 | 789 | **1、静态HTTP服务器** 790 | 791 | 首先,Nginx是一个HTTP服务器,可以将服务器上的静态文件(如HTML、图片)通过HTTP协议展现给客户端。 792 | 793 | server { 794 | listen80; # 端口号 795 | location / { 796 | root /usr/share/nginx/html; # 静态文件路径 797 | } 798 | } 799 | 800 | **2、反向代理服务器** 801 | 802 | server { 803 | listen80; 804 | location / { 805 | proxy_pass http://192.168.20.1:8080; # 应用服务器HTTP地址 806 | } 807 | } 808 | 809 | **3、负载均衡** 810 | 811 | upstream myapp { 812 | server192.168.20.1:8080; # 应用服务器1 813 | server192.168.20.2:8080; # 应用服务器2 814 | } 815 | server { 816 | listen80; 817 | location / { 818 | proxy_pass http://myapp; 819 | } 820 | } 821 | 822 | 以上配置会将请求轮询分配到应用服务器,也就是一个客户端的多次请求,有可能会由多台不同的服务器处理。可以通过ip-hash的方式,根据客户端ip地址的hash值将请求分配给固定的某一个服务器处理。 823 | 824 | upstream myapp { 825 | ip_hash; # 根据客户端IP地址Hash值将请求分配给固定的一个服务器处理 826 | server192.168.20.1:8080; 827 | server192.168.20.2:8080; 828 | } 829 | server { 830 | listen80; 831 | location / { 832 | proxy_pass http://myapp; 833 | } 834 | } 835 | 836 | 另外,服务器的硬件配置可能有好有差,想把大部分请求分配给好的服务器,把少量请求分配给差的服务器,可以通过weight来控制。 837 | 838 | upstream myapp { 839 | server192.168.20.1:8080weight=3; # 该服务器处理3/4请求 840 | server192.168.20.2:8080; # weight默认为1,该服务器处理1/4请求 841 | } 842 | server { 843 | listen80; 844 | location / { 845 | proxy_pass http://myapp; 846 | } 847 | } 848 | **4、虚拟主机** 849 | 有的网站访问量大,需要负载均衡。然而并不是所有网站都如此出色,有的网站,由于访问量太小,需要节省成本,将多个网站部署在同一台服务器上。 850 | 851 | 例如将www.aaa.com和www.bbb.com两个网站部署在同一台服务器上,两个域名解析到同一个IP地址,但是用户通过两个域名却可以打开两个完全不同的网站,互相不影响,就像访问两个服务器一样,所以叫两个虚拟主机。 852 | 853 | server { 854 | listen80default_server; 855 | server_name _; 856 | return444; # 过滤其他域名的请求,返回444状态码 857 | } 858 | server { 859 | listen80; 860 | server_name www.aaa.com; # www.aaa.com域名 861 | location / { 862 | proxy_pass http://localhost:8080; # 对应端口号8080 863 | } 864 | } 865 | server { 866 | listen80; 867 | server_name www.bbb.com; # www.bbb.com域名 868 | location / { 869 | proxy_pass http://localhost:8081; # 对应端口号8081 870 | } 871 | } 872 | 873 | **简易爬虫过滤方案** 874 | 875 | location / { 876 | if ($http_user_agent ~* "python|curl|java|wget|httpclient|okhttp") { 877 | return 503; 878 | } 879 | #正常处理 880 | ... 881 | } 882 | 这个是在廖雪峰博客上看到的,原文为: 883 | 现在的网络爬虫越来越多,有很多爬虫都是初学者写的,和搜索引擎的爬虫不一样,他们不懂如何控制速度,结果往往大量消耗服务器资源,导致带宽白白浪费了。 884 | 885 | 其实Nginx可以非常容易地根据User-Agent过滤请求,我们只需要在需要URL入口位置通过一个简单的正则表达式就可以过滤不符合要求的爬虫请求: 886 | 887 | 变量$http_user_agent是一个可以直接在location中引用的Nginx变量。~*表示不区分大小写的正则匹配,通过python就可以过滤掉80%的Python爬虫 888 | 889 | **其他功能** 890 | 891 | 自行探索吧。 892 | 893 | **supervisor的对应配置** 894 | 服务端运维的重点,系统中各类进程的守护者。由于不支持python3的环境,所以配置会稍微麻烦一点,我们需要通过virtualenv 创建一个python2虚拟环境。 895 | 896 | pip install virtualenv 897 | 898 | 首先在自定义目录下创建 super 虚拟环境 899 | 900 | virtualenv --distribute -p /usr/bin/python2 super 901 | 902 | cd super 903 | source bin/activate #激活虚拟环境 904 | ./bin/pip install supervisor# 安装supervier 905 | echo_supervisord_conf > supervisor.conf # 生成 supervisor 默认配置文件 906 | 907 | #热后便可以通过以下命令对supervisor进行操作: 908 | supervisord -c supervisor.conf #通过配置文件启动supervisor 909 | supervisorctl -c supervisor.conf status #察看supervisor的状态 910 | supervisorctl -c supervisor.conf reload 重新载入 #配置文件 911 | supervisorctl -c supervisor.conf start [all]|[appname] #启动指定/所有 supervisor管理的程序进程 912 | supervisorctl -c supervisor.conf stop [all]|[appname] 关闭指定/所有 supervisor管理的程序进程 913 | 914 | supervisor.conf中配置对应的gunicorn应用,其他类型的应用配置也类似,比如java的springboot。 915 | 916 | [program:start_gunicorn] 917 | command=/your/bin/path/to/gunicorn -w 4 -b 0.0.0.0:5000 -k gevent run:app 918 | directory:/home/ubuntu/project/test1/ #run.py所在的目录 919 | autostart=true 920 | redirect_stderr=true 921 | user=root 922 | directory=/usr/local/qs-project/web 923 | stdout_logfile=/var/log/test_test.log 924 | 925 | 如果要启用supervisor的web端:9001端口. 926 | 927 | 需要注释掉 配置文件中 928 | 929 | [inet_http_server] 的所有子配置项 930 | 931 | 以及 932 | 933 | [supervisorctl] 中的前四项 934 | 935 | deactivate #退出环境 936 | 937 | **至此,教程结束,第一次编写小册,如有错误或不足,欢迎指正、交流。** 938 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.0.2 2 | pymysql==0.9.3 3 | sqlalchemy==1.3.3 4 | Gunicorn==20.0.4 5 | Gevent==1.4.0 6 | flask-cors==3.0.9 7 | Flask-SQLAlchemy==2.3.2 8 | tornado==6.0.4 9 | Flask-Migrate==2.7.0 10 | Flask-Sockets==0.2.1 11 | flask_script==2.0.6 -------------------------------------------------------------------------------- /res/cache/readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtreeai/red-flask/bb097f299aec37cb3712149ac0f41c4b32c9f83c/res/cache/readme.md -------------------------------------------------------------------------------- /res/help.txt: -------------------------------------------------------------------------------- 1 | cache : 存放系统生成的缓存文件 2 | prefab : 存放系统预置的多媒体文件和文档数据 3 | upload : 存放用户上传的多媒体文件或文档数据 -------------------------------------------------------------------------------- /res/prefab/readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtreeai/red-flask/bb097f299aec37cb3712149ac0f41c4b32c9f83c/res/prefab/readme.md -------------------------------------------------------------------------------- /res/upload/readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redtreeai/red-flask/bb097f299aec37cb3712149ac0f41c4b32c9f83c/res/upload/readme.md -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @File : run.py 3 | # @Author: redtree 4 | # @Date : 18-6-27 5 | # @Desc : 服务启动入口,自动化判断部署环境并配置UWSGI代理, 6 | 7 | from __init__ import app 8 | from __init__ import SYS_ENV 9 | from tornado.httpserver import HTTPServer 10 | from tornado.wsgi import WSGIContainer 11 | from tornado.ioloop import IOLoop 12 | from config import WEB_IP,WEB_PORT 13 | 14 | 15 | if __name__ == "__main__": 16 | if WEB_IP=='localhost': 17 | #本地调试 18 | app.run(host='0.0.0.0', port=WEB_PORT, debug=False, threaded=True) 19 | #app.run() 20 | # ssl_context = ( 21 | # './server.crt', 22 | # './server_nopwd.key') 23 | else: 24 | if SYS_ENV=='Linux': 25 | ''' 26 | Linux环境下使用gunicorn作为生产环境server 27 | ''' 28 | from werkzeug.contrib.fixers import ProxyFix 29 | # 线上服务部署 对接gunicorn 30 | app.wsgi_app = ProxyFix(app.wsgi_app) 31 | else: 32 | ''' 33 | windows环境下采用 tornado 充当生产环境的wsgi代理,配合nginx使用。 此处的tornado并不能实现异步无阻塞。 34 | ''' 35 | s = HTTPServer(WSGIContainer(app)) 36 | s.listen(WEB_PORT) # 监听 5000 端口 37 | print('environment:product wsgi:tornado port:'+str(WEB_PORT)) 38 | #tornado 在linux上支持多进程模式,-1位根据cpu核数开启 39 | #s.start(-1) 40 | IOLoop.current().start() 41 | -------------------------------------------------------------------------------- /server.crt: -------------------------------------------------------------------------------- 1 | 请使用自己的证书,本文件仅作演示 2 | -------------------------------------------------------------------------------- /server_nopwd.key: -------------------------------------------------------------------------------- 1 | 请使用自己的证书,本文件仅作演示 2 | 3 | -------------------------------------------------------------------------------- /service/README.md: -------------------------------------------------------------------------------- 1 | 用于编写API实际业务逻辑的文件目录,Controller下的api接口一般会从此处调用具体服务,其目录结构与controller相似。 -------------------------------------------------------------------------------- /service/admin/sys_user_manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @ Time : 2020/1/13 15:50 3 | # @ Author : Redtree 4 | # @ File : sys_user_manager 5 | # @ Desc : 系统用户管理 6 | 7 | 8 | import random 9 | import string 10 | import hashlib 11 | from __init__ import db 12 | from config import USER_SALT_LENGTH,PAGE_LIMIT, DEFAULT_PAGE 13 | from sqlalchemy import func,or_ 14 | from database.sys_user import Sys_user 15 | from utils.http import responser 16 | import json 17 | import time 18 | from utils.decorator import oauth2_tool 19 | 20 | 21 | # 获取由4位随机大小写字母、数字组成的salt值 22 | def create_salt(): 23 | salt = ''.join(random.sample(string.ascii_letters + string.digits, USER_SALT_LENGTH)) 24 | return salt 25 | 26 | # 获取原始密码+salt的md5值 27 | def md5_password(password,salt): 28 | trans_str = password+salt 29 | md = hashlib.md5() 30 | md.update(trans_str.encode('utf-8')) 31 | return md.hexdigest() 32 | 33 | #用户登录认证(HTTP/LDAP) 34 | def check_login(username,password): 35 | 36 | #先检查用户是否被封禁或删除 37 | try: 38 | info = Sys_user.query.filter(Sys_user.username == username).first() 39 | user_data = json.loads(str(info)) 40 | ud_salt = user_data['salt'] 41 | ud_password =user_data['password'] 42 | ud_nickname = user_data['nickname'] 43 | if not md5_password(password,ud_salt)==ud_password: 44 | #说明密码错误 45 | return responser.send(10002) 46 | #无拦截则登录成功,生成access_token返回给用户,token_key为username 47 | access_token = oauth2_tool.generate_token(username) 48 | if access_token==False: 49 | #属于系统级异常,一般是部署的python环境有问题,正常不会触发 50 | return responser.send(40001) 51 | 52 | res = {'access_token':access_token,'username':username,'nickname':ud_nickname} 53 | #异步更新登录日志 54 | return responser.send(10000,res) 55 | 56 | except Exception as err: 57 | print(err) 58 | #账号不存在 59 | return responser.send(10001) 60 | 61 | 62 | #用户登录认证(HTTP/LDAP) 63 | def auto_login(username,access_token): 64 | 65 | #先检查用户是否被封禁或删除 66 | try: 67 | info = Sys_user.query.filter(Sys_user.username == username).first() 68 | user_data = json.loads(str(info)) 69 | ud_nickname = user_data['nickname'] 70 | 71 | res = {'access_token':access_token,'username':username,'nickname':ud_nickname} 72 | #异步更新登录日志 73 | return responser.send(10000,res) 74 | 75 | except Exception as err: 76 | print(err) 77 | #账号不存在 78 | return responser.send(10001) 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | echo '服务部署成功 端口5000' 2 | nohup gunicorn -c gunicorn_config.py run:app & -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | echo '调试进程启动 端口5000' 2 | gunicorn -c gunicorn_config.py run:app 3 | -------------------------------------------------------------------------------- /test_case/admin/sys_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @ Time : 2021/4/12 14:59 3 | # @ Author : Redtree 4 | # @ File : sys_user 5 | # @ Desc : 6 | 7 | 8 | import requests 9 | import json 10 | 11 | def check_login(username,password): 12 | url = 'http://localhost:5000/login' 13 | rd = { 14 | 'username':username, 15 | 'password':password 16 | } 17 | rd=json.dumps(rd) 18 | res = requests.post(url,data=rd).text 19 | return res 20 | 21 | ''' 22 | 账号登录 23 | ''' 24 | # res = check_login('admin','admin123') 25 | -------------------------------------------------------------------------------- /utils/common/encrypt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | 此工具提供了一个常用的用户密码加密方案,基于md5和随机盐值. 4 | ''' 5 | import random 6 | import string 7 | import hashlib 8 | 9 | # 获取由4位随机大小写字母、数字组成的salt值 10 | def create_salt(): 11 | salt = ''.join(random.sample(string.ascii_letters + string.digits, 4)) 12 | return salt 13 | 14 | # 获取原始密码+salt的md5值 15 | def md5_password(password,salt): 16 | trans_str = password+salt 17 | md = hashlib.md5() 18 | md.update(trans_str.encode('utf-8')) 19 | return md.hexdigest() -------------------------------------------------------------------------------- /utils/common/logger.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 19-12-2 下午3:48 3 | # @Author : Redtree 4 | # @File : logger.py 5 | # @Desc : 调用python内置的logging模块实现日志管理,配置log.IO等框架实现日志可视化管理。 6 | 7 | import logging 8 | from __init__ import ROOT_PATH 9 | 10 | def set_log(logWord,level='warning',logName='error'): 11 | 12 | 13 | log_path = ROOT_PATH+'/log/'+str(logName)+'.log' 14 | logging.basicConfig(level=logging.DEBUG, 15 | format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s', 16 | datefmt='%a, %d %b %Y %H:%M:%S', 17 | filename=log_path, 18 | filemode='a') 19 | 20 | #一般采用warning 级别 日志即可 21 | 22 | if level=='warning': 23 | logging.warning(logWord) #warning 警告级别日志 会在控制台输出 24 | elif level=='info': 25 | logging.info(logWord) #info 消息备注 只能在log文件输出 26 | elif level=='error': # 普通错误 会在控制台输出 27 | logging.error(logWord) 28 | elif level=='critical': #严重错误 会在控制台输出 29 | logging.critical(logWord) -------------------------------------------------------------------------------- /utils/common/regex_matcher.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 18-12-7 上午9:50 3 | # @Author : Redtree 4 | # @File : regex_matcher.py 5 | # @Desc : 正则匹配工具 6 | 7 | import re 8 | #匹配电话号码 9 | def match_phone_number(phone_number): 10 | phone_pat = re.compile('^(13\d|14[5|7]|15\d|166|17[3|6|7]|18\d)\d{8}$') 11 | res = re.search(phone_pat, phone_number) 12 | if res: 13 | return True 14 | else: 15 | return False 16 | #匹配邮箱地址 17 | def match_email(email): 18 | res = re.match(r'^[0-9a-zA-Z_]{0,19}@[0-9a-zA-Z]{1,13}\.[com,cn,net]{1,3}$', email) 19 | if res: 20 | return True 21 | else: 22 | return False 23 | 24 | #匹配用户id 英文+数字组合 25 | def match_user_id(user_id): 26 | if re.match('[a-zA-Z0-9]+', user_id) and len(user_id)<=20: 27 | return True 28 | else: 29 | return False 30 | 31 | -------------------------------------------------------------------------------- /utils/common/text_similarity.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @File : text_similarity.py 3 | # @Author: redtree 4 | # @Date : 18-6-27 5 | # @Desc : 字符串相似度匹配算法工具包 6 | 7 | import difflib 8 | from nltk.util import ngrams 9 | from urllib.parse import unquote 10 | 11 | 12 | #difflib的算法,类余弦相似度匹配算法 13 | def difflib_rank(strA,strB): 14 | strB = unquote(strB, 'utf-8') 15 | for rsp in ['\n', ' ', '。','+']: 16 | strA= str(strA).replace(rsp,'') 17 | strB= str(strB).replace(rsp,'') 18 | seq = difflib.SequenceMatcher(None, str(strA), str(strB)) 19 | ratio = seq.ratio() 20 | return float(ratio) 21 | 22 | #基于nGram算法匹配字符串相似度 23 | def nGram_rank(strA,strB,N_value=2): 24 | nGram_A = list(ngrams(strA,N_value)) 25 | nGram_B = list(ngrams(strB,N_value)) 26 | LEN_A = len(nGram_A) 27 | LEN_B = len(nGram_B) 28 | MATCHS = 0 29 | for n_A in nGram_A: 30 | if n_A in nGram_B: 31 | nGram_B.remove(n_A) 32 | MATCHS=MATCHS+1 33 | 34 | rank = LEN_A+LEN_B-2*MATCHS 35 | return rank 36 | -------------------------------------------------------------------------------- /utils/common/xls_tool.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 19-4-10 下午3:18 3 | # @Author : Redtree 4 | # @File : xls_tool.py 5 | # @Desc : excel表格文件处理工具 6 | 7 | import xlrd #xls文件流读取工具 8 | 9 | def xls_reader(filepath,x,y): 10 | xls_file = xlrd.open_workbook(filepath) #打开文件 11 | xls_sheet = xls_file.sheets()[0] #打开工作谱 12 | 13 | row_value = xls_sheet.row_values(x) #按行读取 14 | col_value = xls_sheet.col_values(y)#按列读取 15 | value = xls_sheet.cell(x, y).value #定位读取 16 | 17 | print(xls_sheet.nrows) #总行数 18 | print(row_value,col_value,value) 19 | 20 | 21 | def xls2list (filepath): 22 | try: 23 | xls_file = xlrd.open_workbook(filepath) # 打开文件 24 | xls_sheet = xls_file.sheets()[0] # 打开工作谱 25 | n = xls_sheet.nrows 26 | 27 | res = [] 28 | for x in range(0, n): 29 | c_obj = xls_sheet.row_values(x) 30 | res.append(c_obj) 31 | return res 32 | 33 | except: 34 | return [] 35 | 36 | 37 | #xls2list('t1.xlsx') -------------------------------------------------------------------------------- /utils/common/zipper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @ Time : 2020/2/25 15:37 3 | # @ Author : Redtree 4 | # @ File : zipper 5 | # @ Desc : 基于python-zipfile 模块的文件管理工具 6 | 7 | 8 | import zipfile # 引入zip管理模块 9 | import os 10 | 11 | 12 | def package_zip(dir_path,zip_path): 13 | """ 14 | 压缩指定文件夹 15 | :param dir_path: 目标文件夹路径 16 | :param zip_path: 压缩文件保存路径+xxxx.zip 17 | :return: 无 18 | """ 19 | 20 | try: 21 | zip = zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) 22 | for path, dirnames, filenames in os.walk(dir_path): 23 | # 去掉目标跟路径,只对目标文件夹下边的文件及文件夹进行压缩 24 | fpath = path.replace(dir_path, '') 25 | 26 | for filename in filenames: 27 | zip.write(os.path.join(path, filename), os.path.join(fpath, filename)) 28 | zip.close() 29 | return True 30 | except: 31 | return False 32 | 33 | 34 | def unzip(zip_path,dir_path,password): 35 | if password: 36 | password = password.encode() 37 | zf = zipfile.ZipFile(zip_path) 38 | try: 39 | zf.extractall(path=dir_path,pwd=password) 40 | except Exception as err: 41 | print(err) 42 | zf.close() 43 | 44 | # dirpath = "../upload/dicom_ae/ID-1" 45 | # outFullName = "ID-1.zip" 46 | 47 | #unzip('../../upload/dcmzip/14070904.zip','../../upload/dicom_ae',None) -------------------------------------------------------------------------------- /utils/decorator/dasyncio.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | 基于python Threading模块封装的异步函数装饰器 4 | ''' 5 | from threading import Thread 6 | import time 7 | 8 | ''' 9 | async_call为一次简单的异步处理操作,装饰在要异步执行的函数前,再调用该函数即可执行单次异步操作(开辟一条新的线程) 10 | ''' 11 | def async_call(fn): 12 | def wrapper(*args, **kwargs): 13 | Thread(target=fn, args=args, kwargs=kwargs).start() 14 | return wrapper 15 | 16 | ''' 17 | async_pool为可定义链接数的线程池装饰器,可用于并发执行多次任务 18 | ''' 19 | def async_pool(pool_links): 20 | def wrapper(func): 21 | def sub_wrapper(*args,**kwargs): 22 | for x in range(0,pool_links): 23 | Thread(target=func, args=args, kwargs=kwargs).start() 24 | #func(*args, **kwargs) 25 | return sub_wrapper 26 | return wrapper 27 | 28 | ''' 29 | async_retry为自动重试类装饰器,不支持单独异步,但可嵌套于 call 和 pool中使用 30 | ''' 31 | def async_retry(retry_times,space_time): 32 | def wrapper(func): 33 | def sub_wrapper(*args, **kwargs): 34 | try_times = retry_times 35 | while try_times > 0: 36 | try: 37 | func(*args, **kwargs) 38 | break 39 | except Exception as e: 40 | print(e) 41 | time.sleep(space_time) 42 | try_times = try_times - 1 43 | return sub_wrapper 44 | return wrapper 45 | 46 | # 以下为测试案例代码 47 | # 48 | # @async_call 49 | # def sleep2andprint(): 50 | # time.sleep(2) 51 | # print('22222222') 52 | # 53 | # @async_pool(pool_links=5) 54 | # def pools(): 55 | # time.sleep(1) 56 | # print('hehe') 57 | # 58 | # 59 | # @async_retry(retry_times=3,space_time=1) 60 | # def check(): 61 | # a = 1 62 | # b ='2' 63 | # print(a+b) 64 | # 65 | # def check_all(): 66 | # print('正在测试async_call组件') 67 | # print('111111') 68 | # sleep2andprint() 69 | # print('333333') 70 | # print('若3333出现在22222此前,异步成功') 71 | # print('正在测试async_pool组件') 72 | # pools() 73 | # print('在一秒内打印出5个hehe为成功') 74 | # print('正在测试async_retry组件') 75 | # check() 76 | # print('打印三次异常则成功') 77 | # 78 | # check_all() 79 | 80 | -------------------------------------------------------------------------------- /utils/decorator/oauth2_tool.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @ Time : 2020/1/14 9:31 3 | # @ Author : Redtree 4 | # @ File : oauth2_tool 5 | # @ Desc : Oauth2认证工具 6 | 7 | 8 | import time 9 | import base64 10 | import hmac 11 | from __init__ import request 12 | from utils.http import responser 13 | from config import TOKEN_EXPIRE 14 | import json 15 | 16 | 17 | #生成token 18 | def generate_token(key): 19 | r''' 20 | @Args: 21 | key: str (用户给定的key,需要用户保存以便之后验证token,每次产生token时的key 都可以是同一个key) 22 | expire: int(最大有效时间,单位为s) 23 | @Return: 24 | state: str 25 | ''' 26 | try: 27 | ts_str = str(time.time() + int(TOKEN_EXPIRE)) 28 | ts_byte = ts_str.encode("utf-8") 29 | sha1_tshexstr = hmac.new(key.encode("utf-8"),ts_byte,'sha1').hexdigest() 30 | token = ts_str+':'+sha1_tshexstr 31 | b64_token = base64.urlsafe_b64encode(token.encode("utf-8")) 32 | return b64_token.decode("utf-8") 33 | except: 34 | return False 35 | 36 | #token校验 37 | def certify_token(key, token): 38 | r''' 39 | @Args: 40 | key: str 41 | token: str 42 | @Returns: 43 | boolean 44 | ''' 45 | try: 46 | token_str = base64.urlsafe_b64decode(token).decode('utf-8') 47 | token_list = token_str.split(':') 48 | if len(token_list) != 2: 49 | return False 50 | ts_str = token_list[0] 51 | if float(ts_str) < time.time(): 52 | # token expired 53 | return False 54 | known_sha1_tsstr = token_list[1] 55 | sha1 = hmac.new(key.encode("utf-8"), ts_str.encode('utf-8'), 'sha1') 56 | calc_sha1_tsstr = sha1.hexdigest() 57 | if calc_sha1_tsstr != known_sha1_tsstr: 58 | # token certification failed 59 | return False 60 | # token certification success 61 | return True 62 | except: 63 | return False 64 | 65 | 66 | 67 | def request2json(request): 68 | return json.loads(request.data) 69 | 70 | 71 | #Oauth2认证装饰器 72 | def oauth2_check(func): 73 | def inner(*args,**kwargs): 74 | try: 75 | if request.method=='GET': 76 | rjson = request.args.to_dict() 77 | key = rjson['action_username'] 78 | token = rjson['access_token'] 79 | if certify_token(key, token) == True: 80 | #认证通过则不拦截 81 | pass 82 | else: 83 | return responser.send(20001) 84 | elif request.method=='POST': 85 | rjson = request2json(request) 86 | key = rjson['action_username'] 87 | token = rjson['access_token'] 88 | if certify_token(key, token) == True: 89 | # 认证通过则不拦截 90 | pass 91 | else: 92 | return responser.send(20001) 93 | else: 94 | #暂不支持验证其他请求 95 | return responser.send(30001) 96 | except Exception as e : 97 | print('Oauth2认证失败:'+str(e)) 98 | return responser.send(30001) 99 | return func(*args,**kwargs) 100 | return inner 101 | 102 | 103 | -------------------------------------------------------------------------------- /utils/http/responser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 18-10-30 上午9:56 3 | # @Author : Redtree 4 | # @File : responser.py 5 | # @Desc : http请求响应数据封装 6 | 7 | import json 8 | 9 | CODE = { 10 | # 正常操作 11 | 10000: '操作成功', 12 | 13 | # 逻辑异常 14 | 10001: '用户不存在', 15 | 10002: '帐号或密码错误', 16 | 10003:'该帐号已被封禁', 17 | 10004:'该角色已被封禁', 18 | 10005:'包含不可操作用户', 19 | 10006:'该记录已存在', 20 | 10007:'机构封禁', 21 | 10008:'邮箱格式错误', 22 | 10009:'电话格式错误', 23 | 10010:'已启用虚拟设备中', 24 | 10021: '用户密码重置失败', 25 | # http异常 26 | 20001: 'token超时', 27 | 20002: 'ssl证书异常', 28 | 20003: 'JSON格式异常', 29 | 20004: '服务端链接超时', 30 | 20005:'COS服务异常', 31 | # 非法行为 32 | 30001: '操作非法', 33 | 30002: 'IP黑名单', 34 | 30003: '访问过于频繁', 35 | 30004: '创建帐号含非法角色', 36 | 30005: '填入数据非法', 37 | 30006: '机构类型非法', 38 | 30007: '非法获取授权资源', 39 | # 系统异常 40 | 40001: '系统异常', 41 | 40002: 'mysql服务异常', 42 | 40003: 'redis服务异常', 43 | 40004: '模型文件丢失', 44 | 40005: 'FFmpeg异常', 45 | 40006: 'shell功能异常', 46 | # 接口服务 47 | 50001: '接口鉴权失败', 48 | 50002: '参数名错误', 49 | 50003: '文件格式异常', 50 | 50004: '数据长度溢出', 51 | 50005: '转码失败', 52 | 50006: '文件上传失败', 53 | 50007: '数据格式异常', 54 | } 55 | 56 | 57 | # 返回数据封装 58 | def send(code, data=''): 59 | if code == 10000 or code == 50002 or code == 10010: 60 | return json.dumps({'code': code, 'msg': CODE[code], 'data': data}) 61 | else: 62 | return json.dumps({'code': code, 'msg': CODE[code]}) 63 | 64 | 65 | # request.data 转json 66 | def request2json(request): 67 | return json.loads(request.data) 68 | 69 | 70 | def get_param_check(request, param_list): 71 | try: 72 | rjson = request.args.to_dict() 73 | rkeys_list = list(rjson.keys()) 74 | no_match_list = list(set(rkeys_list) ^ set(param_list)) 75 | if len(no_match_list) == 0: 76 | return 'success', rjson 77 | else: 78 | return 'error', send(50002, no_match_list) 79 | except Exception as e: 80 | return 'error', send(20003) 81 | 82 | 83 | # 校验post_json 数据格式是否符合参数列表 若不符合,提示差集 84 | def post_param_check(request,param_list): 85 | try: 86 | rjson= request2json(request) 87 | rkeys_list = list(rjson.keys()) 88 | no_match_list = list(set(rkeys_list) ^ set(param_list)) 89 | if len(no_match_list) == 0: 90 | return 'success', rjson 91 | else: 92 | return 'error', send(50002, no_match_list) 93 | except Exception as e: 94 | return 'error', send(20003) 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /utils/msg_queue/celery_base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 19-12-2 下午3:34 3 | # @Author : Redtree 4 | # @File : celery_base.py 5 | # @Desc : 基于celery实现简单的分布式消息分发 6 | 7 | 8 | from celery import Celery 9 | 10 | app = Celery('tasks', broker='amqp://guest@localhost//') 11 | 12 | @app.task 13 | def add(x, y): 14 | return x + y -------------------------------------------------------------------------------- /utils/msg_queue/celery_dojob.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 19-12-2 下午3:36 3 | # @Author : Redtree 4 | # @File : celery_dojob.py 5 | # @Desc : 6 | 7 | from utils.msg_queue.celery_base import add 8 | 9 | add.delay(4,4) -------------------------------------------------------------------------------- /utils/msg_queue/rmq_recieve.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 19-12-2 下午3:18 3 | # @Author : Redtree 4 | # @File : rmq_recieve.py 5 | # @Desc :面向rabbitMQ消息队列的处理案例(消费者 ) 6 | 7 | 8 | import pika 9 | 10 | ''' 11 | 使用rabbitMQ需要实现在服务器安装rabbit_server,依赖于erlang环境.详细的安装方式自行搜索。 12 | ''' 13 | 14 | hostname = 'localhost' 15 | parameters = pika.ConnectionParameters(hostname) 16 | connection = pika.BlockingConnection(parameters) 17 | 18 | # 创建通道 19 | channel = connection.channel() 20 | channel.queue_declare(queue='hello') 21 | 22 | 23 | def callback(ch, method, properties, body): 24 | print (" [x] Received %r" % (body,)) 25 | 26 | # 告诉rabbitmq使用callback来接收信息 (旧的版本参数顺序是 callback 'hello'对调) 27 | channel.basic_consume('hello',callback,True) 28 | 29 | # 开始接收信息,并进入阻塞状态,队列里有信息才会调用callback进行处理,按ctrl+c退出 30 | print (' [*] Waiting for messages. To exit press CTRL+C') 31 | channel.start_consuming() -------------------------------------------------------------------------------- /utils/msg_queue/rmq_send.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # @Time : 19-12-2 下午2:28 3 | # @Author : Redtree 4 | # @File : rmq_send.py 5 | # @Desc : 面向rabbitMQ消息队列的处理案例(生产者) 6 | 7 | 8 | import pika 9 | import random 10 | 11 | 12 | ''' 13 | 使用rabbitMQ需要实现在服务器安装rabbit_server,依赖于erlang环境.详细的安装方式自行搜索。 14 | 15 | python依赖中,建议使用librabbitmq,这是官方推荐的基于c++链接的mq库 16 | 17 | pip install "celery[librabbitmq,redis,msgpack]" 18 | 19 | ''' 20 | 21 | # 新建连接,rabbitmq安装在本地则hostname为'localhost' 22 | hostname = 'localhost' 23 | parameters = pika.ConnectionParameters(hostname) 24 | connection = pika.BlockingConnection(parameters) 25 | 26 | # 创建通道 27 | channel = connection.channel() 28 | # 声明一个队列,生产者和消费者都要声明一个相同的队列,用来防止万一某一方挂了,另一方能正常运行 29 | channel.queue_declare(queue='hello') 30 | 31 | number = random.randint(1, 1000) 32 | body = 'hello world:%s' % number 33 | # 交换机; 队列名,写明将消息发往哪个队列; 消息内容 34 | # routing_key在使用匿名交换机的时候才需要指定,表示发送到哪个队列 35 | channel.basic_publish(exchange='', routing_key='hello', body=body) 36 | print(" [x] Sent %s" % body) 37 | connection.close() 38 | 39 | 40 | --------------------------------------------------------------------------------