前言
最近要把工程项目部署到服务器上。由于项目是用python3写的,所以大致学习了一下python的Flask架构,以此构建网络应用。初步体验了一下,只需要一两分钟就可以编写出简单的网站。
也可以直接看官网资料学习
用Flask可以方便快捷地实现后端服务(因为python用起来更无脑简单),为小规模的访问提供支持。
配置环境
- 配置pipvenv虚拟环境,便于迁移
pip -m venv .venv
(可选) - 安装Flask:
pip install Flask
简单运行
最简单的Flask应用只需要一个文件就可以了。创建web_server.py文件,注意不能用flask.py作为文件名
#导入包
from flask import Flask
#安全策略,escape函数可以过滤<,>,',",&等特殊字符
from markupsafe import escape
#将当前文件作为单一模块导入。__name__也可以换成一个包名
app = Flask(__name__)
#装饰器,当url为根时执行index()函数
@app.route('/')
def index():
return 'Index Page'
@app.route('/hello')
def hello():
return 'Hello, World'
编辑好后,临时设置环境变量并运行:
- Windows下,使用cmd时:
set FLASK_APP=web_server.py
- Windows下,使用powershell时:
$env:FLASK_APP = "web_server.py"
- Linux下:
export FLASK_APP=web_server.py
不设置FLASK_APP时,会查找文件名为wsgi.py或app.py的文件来启动。
然后使用flask run
就可以开启本地的flask服务(不是执行这个py文件)。
然后在浏览器里访问http://127.0.0.1:5000即可看到网站了。当然,这里只是本地调试方便,而不是正式部署在服务器上。如果5000端口已经被占用,可以自行指定别的端口,比如flask run --port 5001
。
也可以直接python web_server.py
启动python文件本身:
if __name__ == '__main__':
app.run(host='0.0.0.0', port = 4000)
这里介绍一种比较简单的部署方法,使用的是CGI,其适应于所有主流服务器,但是其性能稍弱。
像上面这些使用装饰器修饰的函数又称为视图函数,这些函数的返回值会自动转化为一个响应对象。根据响应对象的类型,有如下转化规则:
- 如果视图返回的是一个响应对象,那么就直接返回它。
- 如果返回的是一个字符串,那么根据这个字符串和缺省参数生成一个用于返回的 响应对象。
- 如果返回的是一个字典,那么调用
jsonify
创建一个响应对象。 - 如果返回的是一个元组,那么元组中的项目可以提供额外的信息。元组中必须至少 包含一个项目,且项目应当由
(response, status)
、(response, headers)
或者(response, status, headers)
组成。status
的值会重载状态代码, headers 是一个由额外头部值组成的列表 或字典。 - 如果以上都不是,那么 Flask 会假定返回值是一个有效的 WSGI 应用并把它转换为 一个响应对象。
Flask可以方便地构建动态url,并且页面内容可以据此进行响应。有点像临时创建动态网页的感觉,十分方便。
#使用<varname>来表示动态参数传递,可以使用<vartype:varname>指定参数类型和参数名
#这里的 int 范围是自然数 还可以使用float指定非负浮点数
@app.route('/post/<int:post_id>') # post_id必须和下面函数参数post_id命名保持一致
def show_post(post_id):
# show the post with the given id, the id is an integer
return 'Post %d' % post_id
#字符串参数使用 string 来指定,但是可以缺省
#string是不包含 '/'的字符串
#这里的@app是根据之前的app=Flask(__name__)来确定的
@app.route('/user/<username>')
def show_user_profile(username):
# show the user profile for that user
return 'User %s' % escape(username)
# 如果字符串包含'/',那么它是一个路径串,需要用path指定
@app.route('/path/<path:subpath>')
def show_subpath(subpath):
# show the subpath after /path/
return 'Subpath %s' % escape(subpath)
此外还有一种uuid类型。至于uuid是什么,详情见这里
#projects后面加上'/'后,访问/projects时将自动补上'/'
@app.route('/projects/')
def projects():
return 'The project page'
#如果不加'/',访问/about/会404
@app.route('/about')
def about():
return 'The about page'
flask也可以根据GET、POST等不同的HTTP方法来应答。默认时,只处理GET方法。
如果当前使用了 GET 方法, Flask 会自动添加 HEAD 方法支持,并且同时还会 按照 HTTP RFC 来处理 HEAD 请求。同样, OPTIONS 也会自动实现。
from flask import request
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
return do_the_login()
else:
return show_the_login_form()
程序可以使用url_for()
函数来构建URL,该函数的第一个参数是@app.route
修饰的那些函数名字符串,其他的参数如果和那些参数使用的参数名一样则可以表示传参,不同则用作查询,比如下面的例子:
from flask import url_for
app = Flask(__name__)
@app.route('/')
def index():
return 'index'
@app.route('/login')
def login():
return 'login'
@app.route('/user/<username>')
def profile(username):
return f'{username}\'s profile'
with app.test_request_context(): #表示用于处理请求,可以不真的在浏览器里去访问
print(url_for('index')) # /
print(url_for('login')) # /login
print(url_for('login', next='/')) # /login?next=/
print(url_for('profile', username='John Doe')) # /user/John%20Doe
可见url_for()
可以自动处理地址、url转义保护。
渲染返回页面模板
使用render_template()
方法来渲染模板,如下所示。此外需要注意的是,这里两个@app.route
说明不同的url可以由相同的函数处理。第一个/hello/没有加name,所以hello函数的name参数需要添加默认值,否则访问时会报错。
from flask import render_template
@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
return render_template('hello.html', hname=name)
render_template
会在templates文件夹下寻找模板文件。文件组织结构应当如下所示:
#如果服务器代码写在一个文件里
/application.py
/templates
/hello.html
#如果服务器是一个包
/application
/__init__.py
/templates
/hello.html
其中,模板文件应当遵照Jinja2的语法规则,比如对于上面的代码,hello.html可以这么写:
<!doctype html>
<title>Hello from Flask</title>
{% if hname %}
<h1>Hello {{ hname }}!</h1>
{% else %}
<h1>Hello, World!</h1>
{% endif %}
其中,
{% xxx %}
用于存放函数语句,用来控制文档结构{{ xxx }}
用于存放表达式,用来显示文本{# xxx #}
用于写注释
请求数据处理
使用request来处理请求,如下所示。此处request.method
可以获取请求的方法,request.form['xxx']
用于处理具体的表单数据。若form没有拿到变量,会抛出KeyError。此外也可以用request.args.get('key', '')
拿到url中?key=value
对应的参数。如果前端请求头是application/json格式,后端可以用request.get_json()
来拿数据
from flask import request
app = Flask(__name__)
@app.route('/login', methods=['POST', 'GET'])
def login():
error = None
if request.method == 'POST':
if valid_login(request.form['username'],
request.form['password']):
return log_the_user_in(request.form['username'])
else:
error = 'Invalid username/password'
# the code below is executed if the request method
# was GET or the credentials were invalid
return render_template('login.html', error=error)
from werkzeug.utils import secure_filename
app.config['UPLOAD_FOLDER'] = 'upload/'
#也可以用来处理文件上传,注意表单需要设定enctype="multipart/form-data"
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
f = request.files['the_file']
f.save('/var/www/uploads/uploaded_file.txt')
f.save(os.path.join(app.config['UPLOAD_FOLDER'],secure_filename(f.filename))")
...
文件下载
使用send_from_directory
函数来管理文件下载
from flask import send_from_directory
app = Flask(__name__)
app.config['DOWNLOAD_FOLDER'] = 'download/'
@app.route('/api/download', methods=['GET'])
def download():
# 获取访问请求中 ?filename=xxx中的xxx
filename = request.args.get('filename','')
try:
return send_from_directory(app.config['DOWNLOAD_FOLDER'],filename )
except Exception as e:
return str(e)
Cookie管理
管理cookies时,如下所示:
# 读取cookies
from flask import request
@app.route('/')
def index():
username = request.cookies.get('username')
# use cookies.get(key) instead of cookies[key] to not get a
# KeyError if the cookie is missing.
# 储存cookies
from flask import make_response
@app.route('/')
def index():
resp = make_response(render_template(...))
resp.set_cookie('username', 'the username')
return resp
Session管理
session允许在不同请求之间传递信息,相当于密钥加密的cookies。如下所示:
from flask import session
# 使用session需要设置密钥,相当于加密的cookies。一般使用随机字符,密钥必须严格保密
# 可以使用python -c 'import secrets; print(secrets.token_hex())' 快捷生成一个密钥
app.secret_key = b'_5#y2L"F4Q8z\n\xec]/'
@app.route('/')
def index():
if 'username' in session:
return f'Logged in as {session["username"]}'
return 'You are not logged in'
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
session['username'] = request.form['username']
return redirect(url_for('index'))
return '''
<form method="post">
<p><input type=text name=username>
<p><input type=submit value=Login>
</form>
'''
@app.route('/logout')
def logout():
# remove the username from the session if it's there
session.pop('username', None)
return redirect(url_for('index'))
页面重定向与错误返回
使用redirect()
方法来重定向页面,使用abort()
方法来退出请求并返回错误代码,如下所示:
from flask import abort, redirect, url_for
@app.route('/')
def index():
return redirect(url_for('login'))
@app.route('/login')
def login():
abort(401) # 401,禁止访问
this_is_never_executed()
使用errorhandler()
方法来定制出错页面
@app.errorhandler(404)
def page_not_found(error):
return render_template('page_not_found.html'), 404
使用make_response()
方法来在试图内部创建响应对象,如下所示:
@app.errorhandler(404)
def not_found(error):
resp = make_response(render_template('error.html'), 404)
resp.headers['X-Something'] = 'A value'
return resp
Dict返回值自动处理为json响应
如果从视图返回一个dict,会自动转化为JSON响应,如下所示:
@app.route("/me")
def me_api():
user = get_current_user()
return {
"username": user.username,
"theme": user.theme,
"image": url_for("user_image", filename=user.image),
}
#也可以使用jsonfy来进行更复杂的转化
@app.route("/users")
def users_api():
users = get_all_users()
return jsonify([user.to_json() for user in users])
实用性配置
CORS规避
有时,浏览器会禁止跨站访问,常见于前后端url不一致。可以在脚本里添加响应头:
试加这个库 pip install flask-cors
from flask_cors import CORS
# 跨域支持
app = Flask(__name__)
CORS(app)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=4000)
支持HTTPS
if __name__ == '__main__':
app.run(host='0.0.0.0', port=4000, ssl_context=('<公钥文件路径>', '<私钥文件路径>'))
缓存
可以用python装饰器来实现缓存功能
from functools import wraps
def cached(max_cache_time=300):
"""
缓存装饰器,可以自定义最大缓存时间(秒)
"""
def decorator(func):
# 缓存字典
cache = {}
@wraps(func)
def wrapper(*args, **kwargs):
# 生成缓存键
key = (func.__name__, args, frozenset(kwargs.items()))
# 检查缓存
if key in cache and time.time() - cache[key][0] < max_cache_time:
return cache[key][1]
# 执行函数并缓存结果
result = func(*args, **kwargs)
cache[key] = (time.time(), result)
return result
return wrapper
return decorator
#之后使用,举例
# 显示服务器硬件信息
@app.route('/server/hardware')
@cached(max_cache_time=86400)
def hardware():
connection = get_db()
result = []
with connection.cursor() as cursor:
cursor.execute(f'SELECT * FROM {Database.TB_BASIC} WHERE `display` is true;')
while True:
row = cursor.fetchone()
if row is None:
break
result.append(row)
return result
例子
下面将以上各种内容进行汇总,快速实现后端逻辑时直接复制粘贴使用:
import json
from flask import Flask, jsonify, send_from_directory
from flask import request
import pymysql.cursors
import time
import datetime
import re
from flask_cors import CORS
from functools import wraps
from pathlib import Path
# 缓存字典
cache = {}
# 如果Flask同时担任网页前端服务器功能,可以不使用nginx,后续用send_from_directory直接返回构建好的页面
#app = Flask(__name__, static_folder='experiment_viewer')
#WORKSPACE_ROOT = Path(__file__).resolve().parent
#EXPERIMENT_VIEWER_DIR = WORKSPACE_ROOT / 'experiment_viewer'
#@app.route('/')
#def serve_index():
# return send_from_directory(EXPERIMENT_VIEWER_DIR , 'index.html')
app = Flask(__name__)
CORS(app)
class Database:
TB_BASIC = 'server_basic'
TB_APPLY = 'user_apply'
def __init__(self):
host = "localhost"
def cached(max_cache_time=300):
"""
缓存装饰器,可以自定义最大缓存时间(秒)
"""
def decorator(func):
# 缓存字典
cache = {}
@wraps(func)
def wrapper(*args, **kwargs):
# 生成缓存键
key = (func.__name__, args, frozenset(kwargs.items()))
# 检查缓存
if key in cache and time.time() - cache[key][0] < max_cache_time:
return cache[key][1]
# 执行函数并缓存结果
result = func(*args, **kwargs)
cache[key] = (time.time(), result)
return result
return wrapper
return decorator
# 数据库连接
def get_db():
connection = pymysql.connect(host='localhost',
port=1111,
user='root',
password='xxx',
db='xxx',
cursorclass=pymysql.cursors.DictCursor)
return connection
# 更新服务器数据
@app.route('/api/server/upload', methods=['GET','POST'])
def upload_info():
post_data = request.get_json()
connection = get_db()
if post_data['code'] == 0:
_st = f"FROM_UNIXTIME({post_data['_st']})"
hostname = f"'{post_data['hostname']}'"
sql = []
if post_data['service'] == 'check_disk_usage':
data = "'"+json.dumps(post_data['data'])+"'"
sql.append(f"UPDATE {Database.TB_BASIC} SET storage={data}, _st={_st} WHERE hostname={hostname}")
with connection.cursor() as cursor:
for target_sql in sql:
cursor.execute(target_sql)
connection.commit()
return {'hostname': post_data['hostname'], 'service': post_data['service']}
# 显示服务器硬件信息
@app.route('/api/server/hardware')
@cached(max_cache_time=86400)
def hardware():
connection = get_db()
result = []
with connection.cursor() as cursor:
cursor.execute(f'SELECT * FROM {Database.TB_BASIC} WHERE `display` is true;')
while True:
row = cursor.fetchone()
if row is None:
break
result.append(row)
return result
# 申请账号
@app.route('/api/user/apply', methods=['GET','POST'])
def apply():
if request.method == 'GET':
result = []
connection = get_db()
with connection.cursor() as cursor:
cursor.execute(f'SELECT * FROM {Database.TB_APPLY} ORDER BY `id` DESC;')
while True:
row = cursor.fetchone()
if row is None:
break
row['_st'] = (row['_st'] ).strftime('%Y-%m-%d %H:%M:%S')
result.append(row)
return result
elif request.method == 'POST':
post_data = request.get_json()
token = post_data['token']
if len(token) == 64:
connection = get_db()
with connection.cursor() as cursor:
cursor.execute(f"SELECT * FROM {Database.TB_APPLY} WHERE `token` = '{token}';")
row = cursor.fetchone()
if row is None:
realname = post_data['realname']
username = post_data['username']
pubkey = post_data['pubkey']
usernote = post_data['usernote']
cursor.execute(f"INSERT INTO {Database.TB_APPLY} (`realname`,`username`,`pubkey`,`usernote`,`token`) VALUES ('{realname}','{username}','{pubkey}','{usernote}','{token}');")
connection.commit()
return {'error':0,'msg':'申请已提交'}
else:
return {'error':1,'msg':'请勿重复提交申请'}
else:
return {'error':1,'msg':'未知错误'}
# 申请记录
@app.route('/api/db/fetch')
def get_apply():
connection = get_db()
result = []
with connection.cursor() as cursor:
cursor.execute(f'SELECT * FROM {Database.TB_APPLY} ORDER BY `id` DESC;')
while True:
row = cursor.fetchone()
if row is None:
break
result.append(row)
return result
if __name__ == '__main__':
# app.run(host='0.0.0.0', port=4000, ssl_context=('xxx.pem', 'xxx.key'))
app.run(host='0.0.0.0', port=4000, debug=True)