安装Flask框架
打开cmd,输入pip install flask
,它会自动安装Flask框架以及它依赖的两个外部库:Werkzeug和Jinja2。
第一个Flask程序
代码:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return '<h1>Hello World!</h1>'
if __name__ == '__main__':
app.run()
运行结果:
开启调试模式
在app.run()
方法中将debug参数设为True,或将app对象的debug属性设为True也可。
开启调试模式的作用是可以在不重启Flask服务器的前提下更新后端代码时前端页面也会跟着改变。
路由
服务器处理客户端发送过来的URL时需要知道哪个函数处理哪个URL,定义函数与URL之间关系的程序称为路由。
定义路由最简单的方式就是使用app.route(url)
装饰器。
如下面第一个路由定义的是首页,第二个路由定义的是菜单页。
代码:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return '<h1>首页</h1>'
@app.route('/menu/')
def menu():
return '<h1>菜单</h1>'
if __name__ == '__main__':
app.run()
变量规则
URL有时是动态的,可以把这些特殊的字段标记为<[类型:]变量名>
的格式,其中类型可选。
类型可为:int,接受整数、float,接受浮点数、path,和默认的相似,但是会接受斜线。
from flask import Flask
app = Flask(__name__)
@app.route('/<int:value>')
def index(value):
return 'value = ' + str(value)
if __name__ == '__main__':
app.run()
Flask构造URL
Flask也可以自己构造URL,使用url_for()
方法即可。该方法的第一个参数是函数名,后面的参数都会拼接到URL里面。
url_for()
方法常常和redirect()
方法使用,效果是跳转到指定地址。
例:
from flask import Flask, redirect, url_for
app = Flask(__name__)
@app.route('/')
def index():
return redirect(url_for('login'))
@app.route('/login')
def login():
return '<h1>Login</h1>'
if __name__ == '__main__':
app.run()
运行结果是首先浏览器跳转到默认的首页,但由于跳转到首页的URL设置了跳转到Login路由,因此浏览器实际上直接跳转到了Login路由。
HTTP方法
默认情况下路由只回应GET请求,可以通过指定装饰器的参数methods来实现让路由回应多个请求。
如:
app.route('/', methods=['GET', 'POST']
静态文件
动态Web页面也需要静态页面,通常是JS和CSS文件。默认情况下我们会将这些文件放到static文件夹,然后在URL中加/static访问。
在Flask中,可以使用url_for()来为静态文件生成URL,如:
url_for('static', filename=style.css')
模板
有了模板之后,Flask就可以构建动态Web界面了。在HTML中变量首先用占位符表示,然后Jinja2会在请求的上下文中知晓每个变量的真实值,然后再将占位符替换成真实值,这一过程叫做渲染。
默认情况下,Flask会在程序文件夹的子文件夹template文件夹下寻找模板。Flask使用render_template()方法寻找模板。它的第一个参数是模板名称,其余参数为模板中变量的值。该函数首先要从flask包中导入。
举例:render_template('index.html', username='root')
。它会找到index.html文件,然后将文件中的替换成root。模板变量支持多种类型,包含所有基本类型以及特殊类型如元组列表字典甚至是对象:
<p>从字典中取一个值:{{ mydict['key'] }}</p>
<p>从列表中取一个值:{{ mylist[3] }}</p>
<p>从列表中取一个带索引的值:{{mylist[integer] }}</p>
<p>从对象的方法中取一个值:{{ obj.func() }}</p>
此外,Jinja2还支持使用过滤器简单地修改变量的值。过滤器的值和变量名用一个竖线隔开,如{{ variable|filter }}
。
可用的过滤器类型如下:
- safe:渲染值时不转义
- capitalize:将值的首字母转换为大写,其他字母转换为小写l
- ower:将值转换为小写模式
- upper:将值转换为大写模式
- title:将值中每个单词的首字母都转换为大写t
- trim:把值的首尾空格去掉
- striptags:渲染之前将所有HTML标签都去掉
Jinja2还提供了一个特别强大的功能:控制结构。包括选择、循环结构。
- 条件语句:
{% if user %}
Hello, {{ user }}.
{% else %}
Hello stranger.
{% endif %}
- 循环语句:
<ul>
{% for item in list %}
<li>{{ item }}</li>
{% endfor %}
</ul>
Jinja2还支持宏,如:
{% macro render_comment(comment) %}
<li>{{ comment }}</li>
{% endmacro %}
<ul>
{% for comment in comments %}
{{ render_comment(comment) }}
{% endfor %}
</ul>
为了重复使用宏,我们可以将其单独保存在某一文件中。
{% import 'macros.html' as macros %}
<ul>
{% for comment in comments %}
{{ macros.render_comment(comment) }}
{% endfor %}
</ul>
需要重复使用的模板代码片段,可以写入单独的文件,再包含在所有模板中,避免重复。
{% include 'common.html' %>
另一种重复使用代码的强大方式就是模板继承,类似Python的类继承,在下方有详细讲解。
Web表单
处理CSRF
CSRF,全称为跨站请求伪造,为了防止这种攻击,在每次表单提交时都会附带一个独一无二的token。这个token在表单被验证之前先被验证。为了生成这个token,实现对表单的保护,一般需要为程序设置一个密钥。设置密钥的语法如下:
app = Flask(__name__)
app.config['SECERT_KEY'] = 'qi1.zone'
app.config用于配置Flask,通过SECRET_KEY配置的密钥,对整个Flask应用以及很多拓展应用都是有效的。
表单类
使用Flask-WTF时,每一个表单都是由一个继承自Form的类表示,这个类定义了表单的字段,每个字段都由一个对象表示。字段对象可附属一个或多个验证函数,用来验证字段值是否符合要求。如下面的代码:
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired
class NameForm(FlaskForm):
name = StringField('name', validators=[DataRequired()])
password = PasswordField('password', validators=[DataRequired()])
submit = Submit('提交')
首先导入必要的包,其中FlaskForm就是Flask表单类,我们所要使用的表单需要继承自这个类;StringField等就是字段类,这个表单我们共有三个字段:姓名,密码和提交,因此我们用到了对应的三个字段对象;最后就是验证函数,WTForms已经为我们预设好了一些基础的验证函数,这里我们选择字段必填,则应该使用DataRequired。然后就是自定义一个表单类继承自FlaskForm类,然后定义其中的成员变量,这些成员变量在表单被渲染后就会编程HTML代码中的各个字段。拿第一行来说,StringField就代表这个字段是普通文本行,第一个参数代表这个字段被渲染成HTML后那个输入框的标号,第二个参数为可选参数,即验证器。WTforms还支持许多HTML标准字段,如下表所示:
字段类型 | 说明 |
---|---|
StringField | 文本字段 |
TextAreaField | 多行文本字段 |
PasswordField | 密码文本字段 |
HiddenField | 隐藏文本字段 |
DateField | 文本字段,值为datatime.date形式 |
DateTimeField | 文本字段,值为datetime.datetime形式 |
IntegerField | 文本字段,值为整数 |
DecimalField | 文本字段,值为decimal.Decimal形式 |
FloatField | 文本字段,值为浮点数 |
BooleanField | 文本字段,值为True和False |
RadioField | 一组单选按钮 |
SelectField | 下拉列表 |
SelectMultipleField | 下拉列表,可选择多个值 |
FileField | 文件上传字段 |
SubmitField | 表单提交按钮 |
FormField | 把表单作为字段镶嵌到另一个表单中 |
FieldList | 一组指定类型的字段 |
WTForms内置的验证函数如下表所示:
字段类型 | 说明 |
---|---|
验证电子邮箱地址 | |
EqualTo | 比较两个字段的值,常用于输入密码和确认密码 |
IPAddress | 验证IPv4地址 |
Length | 验证输入字符串的长度 |
NumberRange | 验证输入的值在数字范围内 |
Optional | 无输入值时跳过其他验证函数 |
Required | 确保字段中有数据 |
Regexp | 使用正则表达式验证输入值 |
URL | 验证URL |
AnyOf | 确保输入值在可选值列表中 |
HTML模板渲染方式
Flask框架使用Jinja2渲染模板,在模板中使用{{ variable }}
来输出变量,而Flask-WTF模块属于Flask框架的拓展模块,因此它也是支持使用Jinja2渲染的。在使用render_template渲染模板时,需要传入form参数,即表单,就是上方创建的表单对象。
在模板中,使用{{ form.field.label }}
展示标签,其中field是表单的字段对象,使用{{ form.field }}
展示具体字段。
下面是一个使用Flask-WTF和WTForms实现使用Flask渲染web表单的例子:
<!--模板 login.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form method="post">
<!--首先需要加载token-->
{{ form.csrf_token }}
<!--左边是标签,右边是具体的字段 字段不需要加属性,Jinja2在渲染时会自动添加-->
{{ form.user.label }}:{{ form.user() }}<br/>
{{ form.password.label }}:{{ form.password() }}<br/>
{{ form.submit() }}
</form>
</body>
</html>
# form.py
from flask_wtf import FlaskForm # 表单类
from wtforms import StringField, PasswordField, SubmitField # 导入字段对象
from wtforms.validators import DataRequired, Length # 导入验证器对象
class LoginForm(FlaskForm): # 创建自己的表单类,继承自biao'dan'lei
user = StringField(label='user', validators=[DataRequired('请输入用户名')]) # 实例化字段对象,第一个参数是标签,第二个参数是验证器
password = PasswordField(label='输入密码',
validators=[DataRequired('请输入密码'), Length(6, 16, '密码位数必须大于6小于16')])
submit = SubmitField(label='登录')
#run.py
from flask import Flask, render_template, redirect, url_for
from form import LoginForm
app = Flask(__name__)
app.config['SECRET_KEY'] = 'qi1.zone' # 密钥
@app.route('/login', methods=['GET', 'POST']) # 注意请求方式要包含POST请求
def login():
form = LoginForm() # 实例化表单
if form.validate_on_submit(): # 若表单的数据都符合验证器的要求
user = form.user.data # 获取数据
password = form.password.data
if user == 'qi' and password == '123456': # 自己的判断条件
return '<h1>登录成功</h1>'
else:
return '登录失败'
# 同一缩进级别的if用于判断表单是否符合验证器的要求,若页面是第一次渲染则该方法返回false,就会跳到下方的代码,然后渲染模板
return render_template('login.html', form=form)
@app.route('/')
def index():
return redirect(url_for('login')) # 重定向
if __name__ == '__main__':
app.run(debug=True)
运行这段代码,浏览器输入URL,就可以看到登录页面,若账号密码均正确就会显示登录成功。同时若输入数据不符合要求则表单无法提交。
Flask的拓展框架部分请见我的博客。
Flask请求
利用Flask提供的Request对象可以获取URL中问号后面的参数值也可以获取通过POST请求提交的表单中的值。
Request请求对象中封装了大部分请求报文,可以从中获取参数值。请求解析和响应大部分是由Werkzeug处理的,Flask子类化Werkzeug的请求和响应对象,并添加了和程序有关的特定功能。
Request对象的常用属性和方法如下表:
属性或方法 | 说明 |
---|---|
form | 一个字典,存储请求提交的所有表单字段 |
args | 一个字典,存储通过URL查询字符传递的所有参数 |
values | 一个字典,form和args的合集 |
cookies | 一个字典,存储请求的所有cookie |
headers | 一个字典,存储请求的所有http首部 |
files | 一个字典,存储上传的所有文件 |
get_data() | 返回请求主题缓冲的数据 |
get_json() | 返回一个字典,包含解析请求主体后得到的JSON |
blueprint | 处理请求的Flask蓝图的名称 |
endpoint | 处理请求的Flask端点的名称 |
method | HTTP请求方法,可以是GET或POST |
scheme | URL方案(http或https) |
is_secure() | 通过安全的连接(https)发送请求时,返回True |
host | 请求定义的主机名,若客户端定义了端口号,则还包括端口号 |
path | URL的路径部分 |
query_string | URL的查询字符串部分,返回原始二进制值 |
full_path | URL的路径和查询字符串部分 |
url | 客户端请求的完整URL |
base_url | 比较url减少了查询字符串部分 |
remote_addr | 客户端的IP地址 |
environ | 请求的原始WSGI字典 |
- 获取GET请求参数
使用request.args.get(arg)
方法可以获取GET请求参数,其中arg为参数的名字。
如URL为127.0.0.1:5000?name=qi&password=123456
,则request.args.get('name')
的值就为'qi'
。
由于这部分很简单,这里不用代码演示了。
- 获取POST请求参数
使用request.args.post(args)
方法可以获取POST请求参数,或者使用request.form
对象获取表单数据。
我在测试时没有找到request.args.post这个方法,可能在最新版本的模块中被删除了。
如我们创建一个run.py,然后在这个文件中定义一个默认路由,随后判断请求是否是以POST形式提交的,若否就渲染模板index.html,若是则获取表单中提交的姓名和年龄,然后在页面中输出这两个变量的值,代码如下。
from flask import Flask, request, render_template
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
name = request.form['name']
age = request.form['age']
return f'姓名:{name}, 年龄:{age}'
else:
return render_template('index.html')
if __name__ == '__main__':
app.run()
然后我们创建一个模板文件index.html,在其中我们创建一个表单,让这个表单以post方式提交,接着创建姓名、年龄输入框和提交按钮,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
<form method="post">
姓名:<input type="text" name="name"><br/>
年龄:<input type="text" name="age"><br/>
<input type="submit">
</form>
</body>
</html>
运行run.py文件后访问127.0.0.1:5000,在姓名中输入qi,在年龄中输入20,再点击提交,就可以看到输出结果为“姓名:qi,年龄:20”了。
- 文件上传
在使用Web表单时,经常使用文件上传的功能。使用request.files对象就可以获取与表单文件相关的数据。
如我们实现一个用户上传图片功能。首先定义一个路由函数upload(),用于上传图片,然后创建一个模板文件upload_file.html,用于显示上传文件的页面,最后编写一个show_file()函数,用于显示图片。
上传文件的模板文件upload_file.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>上传图片</title>
</head>
<body>
<form action="" method="post" enctype="multipart/form-data">
上传文件:<input type="file" name="file"><br/>
<input type="submit">
</form>
</body>
</html>
表单的enctype属性值要设为multipart/form-data,用于上传文件。
接下来创建run.py文件,在里面定义upload()路由函数,若接收请求为POST时就上传图片,若为GET请求就上传图片。图片上传完成后将页面重定向至图片路径,直接读取图片。
import os.path
import uuid
from flask import Flask, request, render_template, redirect, url_for, send_from_directory
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads' # 配置文件的上传路径
ALLOWED_EXTENSIONS = {'jpg', 'png', 'jpeg'} # 配置允许上传的文件名后缀
def is_file_allowed(filename): # 定义文件后缀是否符合格式的函数
return '.' in filename and filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS
def random_file_name(filename): # 定义随机命名文件名的函数
ext = os.path.splitext(filename)[1]
new_filename = uuid.uuid4().hex + ext
return new_filename
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'GET': # 若请求为GET则渲染模板
return render_template('upload_file.html')
elif request.method == 'POST': # 若请求为POST则执行上传功能且重定向至图片
pic = request.files['file'] # 读取表单中的图片
if pic and is_file_allowed(pic.filename): # 若成功读取且文件有效
filename = random_file_name(pic.filename)
pic.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) # 保存图片
return redirect(url_for('uploaded_file', filename=filename)) # 重定向
else:
return '文件类型不支持'
@app.route('/uploads/<filename>') # 读取图片的路由
def uploaded_file(filename):
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
if __name__ == '__main__':
app.run()
请求钩子
我们有时会对请求进行预处理和后处理,这时可以使用Flask框架的请求钩子Hook,以注册在请求处理的不同阶段执行的处理函数。Flask的请求钩子是在执行视图函数前后执行的函数,用户可以在这些函数里做一些操作。Flask利用装饰器提供了以下四种钩子:
before_first_request:在处理第一个请求时执行,如链接数据库操作;
before_request:在每次请求前执行,如权限校验;
after_request:每次请求后调用,前提是没有未处理的异常抛出;
teardown_request:即使有未处理的异常抛出也在每次请求后调用。
需要注意before_first_request已被弃用,在Flask的以后版本会被删除,因此最好使用初始设置函数来替代。
如在运行程序之前需要创建数据库表,添加管理员用户,这些工作就可以由before_first_request来处理;再如网站需要记录用户最后在线的时间,就可以通过用户最后发送的请求时间来实现。为了避免在每个视图函数都添加最后在线的时间,可以仅在before_request来实现这段函数;再比如在视图函数中进行数据库操作时,需要将修改之后的数据提交到数据库中,就可以通过after_request来实现。
例:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
print('视图函数执行')
return 'index page'
# 在第一次请求之前运行.
@app.before_first_request
def before_first_request():
print('before_first_request')
# 在每一次请求前都会执行
@app.before_request
def before_request():
print('before_request')
# 在请求之后运行
@app.after_request
def after_request(response):
# response: 就是前面的请求处理完毕之后, 返回的响应数据,前提是视图函数没有出现异常
# response.headers["Content-Type"] = "application/json"
print('after_request')
return response
# 无论视图函数是否出现异常,每一次请求之后都会调用,会接受一个参数,参数是服务器出现的错误信息
@app.teardown_request
def teardown_request(error):
print('teardown_request: error %s' % error)
if __name__ == '__main__':
app.run(debug=True)
运行代码后两次访问127.0.0.1:5000后结果如下:
A:\flaskDemo\venv\Scripts\python.exe A:\flaskDemo\07\04\run.py
\* Serving Flask app 'run'
\* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
\* Running on http://127.0.0.1:5000
Press CTRL+C to quit
\* Restarting with stat
\* Debugger is active!
\* Debugger PIN: 897-591-349
before_first_request
before_request
视图函数执行
after_request
teardown_request: error None
127.0.0.1 - - [26/May/2023 22:43:36] "GET / HTTP/1.1" 200 -
before_request
after_request
teardown_request: error None
127.0.0.1 - - [26/May/2023 22:43:36] "GET /favicon.ico HTTP/1.1" 404 -
127.0.0.1 - - [26/May/2023 22:44:10] "GET / HTTP/1.1" 200 -
before_request
视图函数执行
after_request
teardown_request: error None
Flask响应
Flask响应是由Request对象完成的,但是一般情况下只处理请求即可。
处理404请求
在浏览器输入网址后Flask会先检测是否有与该路径匹配的路由,若没有则会返回404状态。默认的404页面是一串英文,我们可以将其改为自己的页面,代码如下:
from flask import Flask, render_template
app = Flask(__name__)
@app.errorhandler(404)
def page_not_found(e): # 这里需要传递一个参数:e,代表错误的对象
print(e)
return '404'
@app.route('/')
def index():
return 'index'
if __name__ == '__main__':
app.run()
处理其他请求
如处理302重定向请求,就需要将首部字段的Location设为重定向至的URL,示例代码:
@app.route('/index')
def index():
return '', 302, {'Location': 'https://qi1.zone'}
当访问127.0.0.1:5000时,会重定向至https://qi1.zone。但是在多数情况下,只需要返回主体即可。
响应格式
MIME类型
在HTTP响应中,数据可以以多种方式传输,最常见的方式就是返回一个HTML代码,然后浏览器将其解析并呈现出页面。Flask的默认返回类型也是HTML代码,这时我们可以通过设置MIME类型来改变返回页面的类型。MIME类型在首部的Content-Type字段中定义。以默认的HTML类型为例,Content-Type类型如下:
Content-Type: text/html; charset=utf-8
但是在特定的情况下,也会使用其他格式。此时可以通过Flask提供的make_response()生成响应对象,传入相应的主体作为参数,然后使用响应对象的mimetype属性设置MIME类型。示例代码如下:
from flask import Flask, make_response
app = Flask(__name__)
@app.route('/')
def index():
response = make_response('Hello World!')
response.mimetype = 'text/plain'
return response
if __name__ == '__main__':
app.run()
常用的格式有纯文本、HTML、XML和JSON,它们对应的MIME类型如下:
- 纯文本:text/plain
- HTML:text/html
- XML:application/xml
- JSON:application/json
下面将演示如何返回JSON数据。
JSON数据格式
JSON,JavaScript对象表示法,是一种流行的、轻量的数据交换格式。它由“键值对”组成,类似于Python的字典。
对于JSON格式,MIME类型为application/json,Python处理JSON的笔记请见Python基础部分笔记。在Flask框架中,我们可以不使用json模块的load()和dumps()方法,而是使用Flask提供的jsonify()方法处理JSON字符串。
使用json.dumps()返回:
@app.route('/')
def index():
data = {'name': '棋', 'age': 20}
response = make_response(json.dumps(data))
response.mimetype = 'application/json'
return response
使用jsonify()返回:
@app.route('/')
def index():
return jsonify(name='棋', age=20)
jsonify()函数既可以传入关键字参数,也可以传入普通参数,如:
jsonify({'name': '棋', 'age': 20})
Cookie和Session
HTTP是无状态协议,用户跳转到其他网页时浏览器不会保存数据,这对登录以及其他需要传递信息的工作来说非常不方便。因此Cookie就诞生了。Cookie是服务器为了存储某些数据而保存在浏览器的小型文本信息,浏览器会将此信息保存一段时间,并在下一次向同一个服务器发送请求时附带这些数据,以便于进行会话管理。
在Flask中,使用Response类提供的set_Cookie()方法就可以在响应中添加一个Cookie。首先使用make_response()生成一个响应对象,传入相应主体作为参数。这个方法返回的默认为内置的Response对象,内置的Response对象有如下属性和方法:
- headers:一个Werkzeug的headers对象,表示响应首部,可以像字典一样操作
- status:状态
- status_code:状态码,文本类型
- mimetype:MIME类型,仅包含内容类型部分
- set_cookie:用来设置Cookie
- get_json:解析为JSON数据
- is_json:判断是否为JSON数据
其中,set_cookie()方法支持使用多个参数来设置Cookie选项,如下:
- key:cookie的键/名称
- value:cookie的值
- max_age:cookie的保存时间,单位为秒。默认在用户会话结束(关闭浏览器)时过期
- expires:具体的过期时间,一个datetime对象或UNIX时间戳
- path:下载Cookie时只有指定的URL可用,默认为整个域名‘
- domain:设置Cookie可用的域名
- secure:若为True,代表只有通过HTTPS才可以使用
- httponly:若为True,将禁止客户端Javascript获取cookie
例:
# app.py
from flask import Flask, request, render_template, make_response, redirect, url_for
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired
app = Flask(__name__)
app.config['SECRET_KEY'] = 'qi1' # 设置表单密钥
class LoginForm(FlaskForm): # 表单模型
user = StringField('用户名', [DataRequired('请输入用户名')]) # 用户名字段
password = PasswordField('密码', [DataRequired('请输入密码')]) # 密码字段
submit = SubmitField('提交') # 提交按钮
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
user = request.form['user'] # 获取表单中user字段的值
password = request.form['password'] # 获取表单中password字段的值
if user == 'qi' and password == '123456':
response = make_response('登录成功')
response.set_cookie(key='user', value=user) # 设置cookie
return response
else:
return '登录失败'
else:
return render_template('login.html', form=LoginForm()) # 渲染表单
@app.route('/')
def index():
user = request.cookies.get('user')
if user is not None:
return f'欢迎{user}'
else:
return redirect(url_for('login'))
if __name__ == '__main__':
app.run()
<!-- login.html -->
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>登录</title>
</head>
<body>
{{ form.csrf_token }}
<form action="" method="post">
{{ form.user.label }}:{{ form.user }}<br/>
{{ form.password.label }}:{{ form.password }}<br/>
{{ form.submit }}
</form>
</body>
</html>
运行此代码后输入127.0.0.1:5000后,由于用户未登录,因此会重定向至127.0.0.1:5000/login。在用户名密码输入框内正确输入密码后就会显示登录成功,此时在输入127.0.0.1:5000后就会显示登录成功的用户信息。
Session对象
上面的例子我们在用户登录成功后添加的Cookie是以明文方式显示的,这样恶意用户就可以通过伪造Cookie的方式非法获取网页中的内容。这时我们就可以通过Flask提供的Session对象解决这个问题。Session指用户会话,或对话,它用于加密Cookie,默认情况下它会把数据存储在名为Session的Cookie中。Session需要密钥加密数据对数据进行签名以加密数据,因此需要设置一个密钥,这个密钥可以通过密码生成工具生辰复杂度较高的密钥。
在下面的例子我们将上面run.py中的代码做如下改动:
# app.py
from flask import Flask, request, render_template, make_response, redirect, url_for, session
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired
app = Flask(__name__)
app.config['SECRET_KEY'] = 'qi1' # 设置表单密钥
class LoginForm(FlaskForm): # 表单模型
user = StringField('用户名', [DataRequired('请输入用户名')]) # 用户名字段
password = PasswordField('密码', [DataRequired('请输入密码')]) # 密码字段
submit = SubmitField('提交') # 提交按钮
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
user = request.form['user'] # 获取表单中user字段的值
password = request.form['password'] # 获取表单中password字段的值
if user == 'qi' and password == '123456':
session['user'] = user
return '登录成功'
else:
return '登录失败'
else:
return render_template('login.html', form=LoginForm()) # 渲染表单
@app.route('/logout')
def logout():
session.pop('user')
return redirect(url_for('login'))
@app.route('/')
def index():
user = session.get('user')
if user is not None:
return f'欢迎{user}'
else:
return redirect(url_for('login'))
if __name__ == '__main__':
app.run()
模板进阶知识
模板上下文
Flask有上下文,Jinja2也有上下文。
通常情况下,在渲染模板时可以调用render_template()传入参数。此外,还可以使用set标签在模板中定义变量,如:
{% set navigation = [('/', 'Home'%), ('/about', 'About')] %}
也可以将一部分模板变量数据定义为变量,使用set和endset标签声明开始和结束。例如:
{% set navigation %}
<li><a href='/'>首页</a></li>
<li><a href='/about'>关于</a></li>
{% endset %}
Flask在模板上下文中也定义了一些内置对象,可以在模板中直接使用。这些内置对象如下表所示:
属性 | 说明 |
---|---|
config | 当前的配置对象 |
request | 当前的请求对象,在已激活的请求环境中使用 |
session | 当前的会话对象,在已激活的请求环境中使用 |
g | 与请求绑定的全局变量,在已激活的请求环境下可用 |
下面通过一个实例介绍如何使用Session。
# run.py
from flask import Flask, render_template, request, session
app = Flask(__name__)
app.config['SECRET_KEY'] = 'qi'
@app.route('/')
def index():
return render_template('index.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
user = request.form['user']
password = request.form['password']
if user == 'qi' and password == '123456':
session['user'] = user
return '登录成功'
else:
return '登录失败'
return render_template('login.html')
@app.route('/logout')
def logout():
session.clear()
return '退出成功'
if __name__ == '__main__':
app.run()
<!-- login.html -->
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>登录</title>
</head>
<body>
<form action="" method="post">
<label for="user">用户名:</label><input type="text" id="user" name="user"><br/>
<label for="password">密码:</label><input type="password" id="password" name="password"><br/>
<input type="submit" value="登录">
</form>
</body>
</html>
<!-- index.html -->
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>首页</title>
</head>
<body>
{% if session['user'] == 'qi' %}
欢迎{{ session['user'] }}登录
{% else %}
请先登录
{% endif %}
</body>
</html>
模板过滤器
内置过滤器
在前面已经介绍了一些简单的过滤器,如safe、trim等,Jinja2中还有很多过滤器,它们的功能如下表:
过滤器 | 说明 |
---|---|
default(value, default_value='u', boolean=False) | 设置默认值,默认值作为参数传入,别名为d |
escape(e) | 转义HTML文本,别名为e |
fisrt(q) | 返回序列的第一个元素 |
last(q) | 返回序列的最后一个元素 |
length(object) | 返回变量的长度 |
random(q) | 返回序列中的随机元素 |
max(value, case_sensitive=False, attribute=None) | 返回序列中的最大值 |
min(value, case_sensitive=False, attribute=None) | 返回序列中的最小值 |
unique(value, case_sensitive=False, attribute=None) | 返回序列中不重复的值 |
wordcount(s) | 计算单词数量 |
tojson | 将变量值转换为JSON格式 |
truncate(s, length=255, killwords=False, end='...', leeway=None) | 截取字符串,可用于显示文章摘要,length参数设置截止的长度,killwords参数设置是否截取单词,end参数设置结尾的符号 |
如,模板中的Article.title变量如下:
Article.title='你好,这里是棋,很高兴认识你。目前我是一名大二学生,在努力学习后端,励志做一名优秀的后端工程师。我非常喜欢计算机,只要有我感兴趣的东西我都会学习下去。目前已经独立或合作做了很多大小项目,并且有很多收获。未来的路还有很长要走,离毕业还有很久,我会继续努力下去。'
使用truncate()方法可以截取Article.title变量,示例代码如下:
{{ Article.title|truncate(10) }}
运行结果如下:
你好,这里是棋...
自定义过滤器
除此之外,还可以自定义过滤器。有两种方法可以自定义过滤器:使用Flask应用对象的add_template_filter方法、一种是应用app.template_filter装饰器来实现自定义过滤器。
下面给出两个例子:
- 使用add_template_filter方法
# run.py
from flask import Flask, render_template
app = Flask(__name__)
content = '你好,这里是棋,很高兴认识你。目前我是一名大二学生,在努力学习后端,励志做一名优秀的后端工程师。我非常喜欢计算机,只要有我感兴趣的东西我都会学习下去。目前已经独立或合作做了很多大小项目,并且有很多收获。未来的路还有很长要走,离毕业还有很久,我会继续努力下去。'
def count(s):
return len(s)
app.add_template_filter('count', count)
@app.route('/')
def index():
return render_template('index.html', content=content)
if __name__ == '__main__':
app.run()
<!-- index.html -->
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>首页</title>
</head>
<body>
全文字数:
{{ content|count }}
<hr/>
{{ content }}
</body>
</html>
- 使用app.template_filter装饰器
只需要将上方的Python代码改为如下形式:
from flask import Flask, render_template
app = Flask(__name__)
content = '你好,这里是棋,很高兴认识你。目前我是一名大二学生,在努力学习后端,励志做一名优秀的后端工程师。我非常喜欢计算机,只要有我感兴趣的东西我都会学习下去。目前已经独立或合作做了很多大小项目,并且有很多收获。未来的路还有很长要走,离毕业还有很久,我会继续努力下去。'
@app.template_filter
def count(s):
return len(s)
@app.route('/')
def index():
return render_template('index.html', content=content)
if __name__ == '__main__':
app.run()
这两段代码的运行结果相同,都是先显示字数,然后显示全文内容。
模板
局部模板
在Web程序中,通常会为每一个页面编写一个独立的模板,比如主页、管理、设置。这些模板通常可以直接在视图函数中渲染并作为HTML主体。这种模板称为主模板或全局模板。除此之外,还会用到很多页面共有的模板,如导航栏、侧边栏、页脚等。这种模板称为局部模板或次模板。
为了和普通模板区分开来,局部模板的命名通常以一个下画线开始,如,定义一个_left_menu.html文件作为后台页面的主模板,然后在每个页面中使用include标签引用该模板,代码如下:
{% include '_left_menu.html' %}
模板继承
模板继承类似于Python的类继承。Jinja2允许用户天添加一个父模板,将网页的导航栏、页脚等公共部分放在单独的HTML代码中,而每一个继承父模板的子模板在渲染时会一并渲染父模板并将它作为子模板的一部分。使用这种方法可以避免编写相同的HTML代码。
在Jinja2中,使用关键字extends实现模板的继承。
例:
run.py
# run.py
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/login')
def login():
return render_template('login.html')
if __name__ == '__main__':
app.run()
这个代码非常简单,主要是在根路由下渲染index.html,在login路由下渲染login.html,注意看下方父模板和子模板的构造。
base.html:
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
{% block content %}
{% endblock %}
{% include 'foot.html' %}
</body>
</html>
在这个模板中,在最后使用了include指令将页脚包含在基模板中,内容部分使用block指令包含,命名为content,详细的页面在子模板中编写,在渲染时名字为content的占位符就会被具体内容替代。
index.html:
{% extends 'base.html' %}
{% block content %}
主页内容
{% endblock %}
index.html就是base.html的子模板,使用extends指令声明此页面继承自base.html基模板。然后在block中写出具体的内容。
login.html
{% extends 'base.html' %}
{% block content %}
登录页面
{% endblock %}
运行run.py,在浏览器中输入127.0.0.1:5000和127.0.0.1:5000/login时,除了显示它们自己的页面以外,由于它们都包含了foot.html,因此页脚都包含在了每一页。
消息闪现
如用户在登录时,用户名输入错误或密码错误时,页面的特定位置会显示错误信息。Flask提供了一个flash()函数,它可以闪现给用户需要的信息。
flash()函数的语法如下:
flash(massage, category)
参数message代表消息内容,category代表消息类型,用于对不同的消息内容分类处理。
通常在视图函数中使用flash()函数,传入消息内容即可闪现一条消息。注意,闪现不是在用户窗口弹出一条消息。实际上,通过flash()函数发送的消息会存储在Session中,需要在模板中使用全局函数get_flashed_messages()获取消息列表,并将其显示出来。
通过flash()函数发送的消息会存储在Session中,因此需要将程序设置一个密钥。
如我们通过以下例子将用户登录成功或失败的消息闪现出来。
run.py
from flask import Flask, render_template, flash, request, url_for, redirect
app = Flask(__name__)
app.config['SECRET_KEY'] = 'qi1'
@app.route('/', methods=['GET','POST'])
def index():
if request.method == 'POST':
if request.form['user'] == 'qi' and request.form['password'] == '123456':
flash('登录成功', 'success')
else:
flash('登录失败', 'error')
return render_template('index.html')
if __name__ == '__main__':
app.run()
run.py中,检测若方法为POST则验证表单中的数据,若数据错误则闪现错误的信息,若数据正确则闪现正确的信息,并设定它们对应的类型。
index.html
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>登录</title>
<style>
.success {
color: black;
}
.login {
color: red;
}
ul, li{
margin: 0;
}
</style>
</head>
<body>
<form action="" method="post">
<label for="user">账号</label><input type="text" id="user" name="user"><br/>
<label for="password">密码</label><input type="text" id="password" name="password"><br/>
{% with messages = get_flashed_messages(with_categories=True) %}
<ul>
{% if messages %}
{% for message, category in messages %}
<li class={{ category }}>{{ message }}</li>
{% endfor %}
{% endif %}
</ul>
{% endwith %}
<input type="submit" value="登录">
</form>
</body>
</html>
在index.html中,表单是必不可少的。在显示结果方面, 先使用with语句与get_flashed_messages(with_categories=True)
获取所有闪现的消息,加with是限定其作用域。然后它会返回一个列表。若这个列表中存在消息,就遍历这个消息,并将它们输出。
自定义404页面
使用app.errorhandler()装饰器就可以将一个函数注册为返回404页面的视图函数。
如自定义一个404.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>页面未找到</title>
<style>
*{
margin: 0;
padding: 0;
}
.container{
display: flex;
width: 100%;
height: 100vh;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<div class="container">
<div>
<h1>页面未找到</h1>
<p>你要的资源未找到,回到主页上看看吧</p>
<p><a href="#">回到主页</a></p></div>
</div>
</body>
</html>
然后创建一个run.py:
from flask import Flask, render_template
app = Flask(__name__)
@app.errorhandler(404)
def error_page(e):
return render_template('404.html')
@app.route('/')
def index():
return '首页'
if __name__ == '__main__':
app.run()
运行run.py,然后在浏览器中输入不存在的URL,就会返回404.html了。
使用Flask-SQLAlchemy管理数据库
扩展Flask-SQLAlchemy集成了SQLAlchemy,它简化了连接数据库服务器、管理数据库操作会话等各类操作,让Flask中的数据处理变得更轻松。
连接数据库服务器
DBMS通常以服务器形式运行在操作系统中,要连接数据库服务器,首先要为程序指定数据库URI。一些数据库管理系统的URI格式如下表所示:
数据库引擎 | URI |
---|---|
MySQL | mysql://username:password@hostname/database |
Postagres | postgresql://username:password@hostname/database |
SQLite(Unix) | sqlite:////absolute/path/database |
SQLite(Windows) | sqlite:///c:/absolute/path/database |
在这些URI中,hostname表示MySQL所在MySQL服务器所在的主机,可以是本地主机也可以是远程主机;数据服务器上可以托管多个数据库,因此database表示使用的数据库名,username和password分别代表用户名和密码。
如使用SQLAlchemy连接本地MySQL的URI如下:
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://root:root@localhost/db?charset=utf8mb4'
URI的各部分意义如下:
- 数据库:MySQL
- 数据库驱动:pymysql
- 数据库名:root
- 数据库密码:root
- 数据库名称:db
- 字符集:utf8mb4
定义数据模型
在连接数据库模式之后,创建数据表之前,要先创建数据模型。处理数据表中字段和面向对象语言中对象属性之间关系的模式叫做ORM。在Flask-SQLAlchemy中,一个数据表对应一个类,一个字段代表类的一个属性。
以用户和权限模型为例,代码如下:
class Role(db.Model):
__tablename__ = 'tb_role'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
def __repr__(self):
return f'<Role {self.name}'
class User(db.Model):
__tablename__ = 'tb_user'
id = db.Column(db_Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
def __repr__(self):
return f'User {self.username}'
类变量tablename定义在数据库中使用的表名,其余的类变量都是该模型的属性,被定义为db.Column类的实例。db.Column类构造函数的第一个参数为数据库列和模型属性的类型,最常用的列类型以及在模型中使用Python类型如下表所示:
类型名 | 对应的Python类型 | 说明 |
---|---|---|
Integer | int | 普通整数,一般是32位 |
SmallInteger | int | 取值范围最少的整数,一般是16位 |
BigInteger | int或long | 不限制精度的整数 |
Float | float | 浮点数 |
Numbric | decimal.Decimal | 浮点数 |
String | str | 变长字符串 |
Text | str | 变长字符串,对较长或不限长度的字符串做了优化 |
Unicode | unicode | 变长Unicode字符串 |
UnicodeText | unicode | 变长Unicode字符串,对较长或不限长度的做了优化 |
Boolean | bool | 布尔值 |
Date | datetime.date | 日期 |
Time | datetime.time | 时间 |
DateTime | datetime.datetime | 时间和日期 |
Interval | datetime.datedeta | 时间间隔 |
Enum | str | 一组字符串 |
PickleType | 任何Python对象 | 自动化使用Pickle序列化 |
LargeBinary | str | 二进制文件 |
最常用的SQLAlchemy列选项如下表所示:
选项名 | 说明 |
---|---|
primary_key | 设为True,表示该列为表的主键 |
unique | 设为True,表示该列不允许出现重复 |
index | 设为True,表示为该列创建索引 |
nullable | 设为True,表示该列不能为空值 |
default | 为该列定义默认值 |
定义关系
在关系数据库中,数据模型间的关系包含一对一、一对多、多对多关系。常用的SQLAlchemy关系如下表:
选项名 | 说明 |
---|---|
backref | 在关系的另一个模型中添加反向引用 |
primaryjoin | 明确指定两个模型之间的反向引用,只需要在模棱两可的关系间指定 |
lazy | 指定如何加载相关记录,可选值有select,表示首次访问时按需加载;immediate,表示源对象加载后加载;joined,表示加载记录,但使用连结;subquery,表示立即加载,但使用子查询;noload,表示永不加载;dynamic,表示不加载记录,但提供加载记录的查询 |
uselist | 若设为False,不使用列表,而使用标量值 |
order_by | 指定关系中记录的排序方式 |
secondary | 指定多对多关系中表的名字 |
secordaryjoin | SQLAlchemy无法自行决定时,指定多对多关系中的二级连结条件 |
在上面用户和角色的例子中,一个user只能分配一个role,一个role可以分给多个user,所以是一对多的关系,在user表中设置外键引用role表中的主键。示例代码如下:
class Role(db.Model):
__tablename__ = 'tb_role'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
# 新增:
users = db.relitionship('User', backref=True, lazy='dynamic')
def __repr__(self):
return f'<Role {self.name}'
class User(db.Model):
__tablename__ = 'tb_user'
id = db.Column(db_Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
# 新增:
role_id = db.Column(db.Integer, db.ForeignKey('tb_role.id'))
def __repr__(self):
return f'User {self.username}'
除一对多关系外,还有其他的关系类型。一对一关系可以用前面的一对多关系表示,但调用db.relitionship()时要将uselist设为False,把“多”变成“一”。多对一关系也可以使用一对多关系表示,对调两张表即可,或者把外键和db.relationship()都放在“多”那一侧。最复杂的关系是多对多,需要建立第三个表,即关系表。
以用户收藏课程为例,一个user可以收藏多个course,而一个course可以被多个user收藏,所以它们是多对多的关系。定义一个多对多关系模型,示例代码如下:
class User(db.Model, UserMixin):
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
username = db.Column(db.String(64), nullable=False)
# secondary:在多对多关系,指定关联表的名称
favorites = db.relationship('Course', secondary='collections', backref=db.backref('user', lazy='dynamic'), lazy='dynamic')
class Course(db.Model):
course_id = db.Column(db.BigInteger, nullable=False, primary_key=True)
# 创建一个收藏的中间表
collections = db.Table('collections', db.Column('user_id', db.Integer, db.Foreign_key('user_id')), db.Column('course_id', db.BigInteger, db.ForeignKey('course.course.id')))
上述代码中,在User表中定义了favorites属性,并在relationship()方法中添加了两个属性:
- secondary='collections':指定中间表为collections。
- backref=db.backref('user',lazy='dynamic'):设置backref,添加反向引用,所以在Course表中不需要在使用relationship设置外键。
在中间表collections中需要设置user_id和course_id列属性,并且都设置成外键。
数据库操作
数据库最常见的操作就是增删改查,下面介绍这四个操作
创建数据
在查询内容之前,必须先插入数据。将数据插入数据库的过程分为如下三步:
- 创建Python对象,
- 将其添加到会话中,
- 提交会话。
这里的会话不是Flask会话,而是Flask-SQLAlchemy,其本质上是数据库事务的增强版本。下面介绍如何新增一个用户,示例代码如下:
from models import User
me = User('admin', 'admin@admin.com')
db.session.add(me)
db.session.commit()
在将对象添加至会话之前,SQLAlchemy基本上不打算将其添加到事务中。此时,仍然可以放弃更改。add()方法可以将用户对象添加到会话中,但是不会提交到数据库。在使用commit()方法之后,会话才能被提交至数据库。
读取数据
添加完数据后,就可以从数据库中查询数据了。使用模型类提供的query属性,然后调用各种过滤方法及查询方法,即可从数据库中查询数据。
通常,一个完整的查询代码如下:
<模型类>.query.<过滤方法>.<查询方法>
例如,查询User表中用户名为qi的用户信息,示例代码如下:
User.query.filter(username='qi').get()
上面的示例中,filter()是过滤方法,get()是查询方法。在Flask-SQLAlchemy中,常用的过滤方法如下:
过滤器 | 说明 |
---|---|
filter() | 把过滤器添加到原查询上,返回一个新查询 |
filter_by() | 把等值过滤器添加到原查询上,返回一个新查询 |
limit() | 使用指定的值显示原查询返回的查询结果数量,返回一个新查询 |
offset() | 偏移原查询返回的结果,返回一个新查询 |
order_by() | 根据指定条件对原查询结果进行排序,返回一个新查询 |
group_by() | 根据指定查询条件对原查询进行分组,返回一个新查询 |
常用的查询如下:
方法 | 说明 |
---|---|
all() | 以列表形式返回查询的所有结果 |
first() | 返回查询的第一个结果,若无结果则返回None |
first_or_404() | 返回查询的第一个结果,若无结果则终止请求同时返回404响应 |
get() | 返回指定主键对应的行,若无结果则返回None |
get_or_404() | 返回指定主键对应的行,若无结果则终止请求并返回404响应 |
count() | 返回查询结果的数量 |
paginate() | 返回一个Paginate对象,包含指定范围内的结果 |
在实际的开发过程中,使用的数据查询方式比较多,下面介绍一些常用的查询方式:
根据主键查询。在get()方法中传递主键:
User.query.get(1)
精确查询。使用filter_by()方法设置查询条件:
User.query.filter_by(username='qi').first()
使用filter()方法设置查询条件:
User.query.filter(User.username='qi').first()
模糊查询:
users = User.query.filter(User.email.endsWith('@example.com')).all()
逻辑非查询:
users = User.query.filter(User.username != 'qi').all()
使用not_执行逻辑非查询:
from sqlalchemy import not_ users = User.query.filter(not_(User.username == 'qi')).all()
使用and_执行逻辑与查询
from sqlalchemy import and_ users = User.query.filter(and_(User.username == 'qi', User.email.endsWith('@example.com'))).all()
使用or_执行逻辑或查询
from sqlalchemy import or_ users = User.query.filter(or_(User.username == 'qi', User.username == 'Qi')).all()
查询结果排序:
User.query.order_by(User.username)
限制返回数目:
User.query.limit(10).all()
更新数据
更新数据非常简单,直接赋值给模型的字段属性即可,然后调用commit()方法。
例:
user = User.query.first()
user.username = 'guest'
db.session.commit()
删除数据
删除数据也非常简单,只需要将插入数据的add()换成delete()即可。例:
user = User.query.fitst()
db.session.delete()
db.session.commit()