使用FlaskScipt
pip install flask-script
使用它可以创建命令,在程序上下文中执行,这样能对Flask对象进行修改。
flask-script 是 Flask 的一个扩展,它能够创建指令,并且让这些指令在 Flask 的应用上下文中执行,可以达到修改 Flask 对象的目的。
除此之外,flask-script 还能够启动 Flask 开发环境服务器,和开启包含有应用上下文的 Python 指令行。
config.py文件
这个文件是应用程序的配置文件:
|
DEBUG为Ture进入调试模式,可以在代码修改后重新载入,不能用于生产环境,werkzeug默认启用pin码的身份验证,让调试环境下的攻击者更难利用调试器
Debugger pin code:146-867-947
manage.py文件
|
CLI是Command Line Interface的缩写,即命令行界面
main.py文件
|
app.config字典可以用来存储框架,扩展和程序本身的配置变量。这个对象还提供了一些方法,可以从文件或者环境中导入变量值。
通过manage.py运行命令行十分必要,因为一些Flask扩展只有在Flask应用对象被创建后才会被初始化。
程序和请求上下文
变量名 | 上下文 | 说明 |
---|---|---|
current_app | 程序上下文 | 当前激活程序的程序实例 |
g | 程序上下文 | 处理请求时用作临时存储的对象,每次请求都会重设这个变量 |
request | 请求上下文 | 请求对象,封装了客户端发出的HTTP请求中的内容 |
session | 请求上下文 | 用户会话,用于存储请求之间需要“记住”的值的词典 |
SQLAlchemy
pip install flask-sqlachemy
- 基于数据库抽象出数据模型,我们需要用SQLAlchemy的python包。
- 它在最底层包装了数据库操作接口,在最上层提供了对象关系映射(ORM)。
- ORM是在不同数据结构和系统类型的数据源之间传递和转换数据的技术。
- 这里,把他用来将数据传成Python对象的集合。
- 同时,python这样的语言,允许在不同的对象间建立引用,读取和设置他们的属性。
- 为了将SQLAlchemy绑定到我们的程序上下文中,我们用Flask SQLAlchemy,它在SQLAlchemy上提供了一层包装,这样就可以结合Flask的一些特性来方便的调用SQLAlchemy的功能。
需要一些特定的包,来作为SQLAlchemy与你选择的数据库之间的连接器。
创建uri来链接数据库
uri类似url,包含了SQLAlchemy创建连接所有信息。一般形式:
SQLALCHEMY_DATABASE_URI=datebasetype+driver://user:passeword@ip:port/db_name(数据库名)
如:config.py
补:sqlite是无需运行服务的sql数据库,所有数据都包含在一个文件中,而且支持python。
SQLite数据库不需要使用服务器,因此不用指定hostname、username和password。URL中的database是硬盘上文件的文件名。
创建数据模型
models.py
|
继承db.Model,这样我们得到了一个有三个字段的表,这个User类的属性值是db.Colum类的实力,每个属性都代表了这个数据库里的一个字段。
在db.Colum的构造函数里,第一个参数是可选的,对应的是实际数据库中的字段名,没有指定,默认为属性名。
如指定:username = db.Colum('user_name',db.String(255))
第二个参数告诉SQLALchemy把什么类型的python类型来处理:
- db.String和db.Text会接受python的字符串,转化为varchar和text类型的字段。
- db.Boolean接收python的True或False值,数据库支持则转,不支持转0,1.
- db.Date,db.DateTime,db.Time使用了python中datetime原生包中的同名类。
- db.Integer和Float会接受python中的任意数值类型。括号参数说明限制该字段的储存长度。
primary_key告诉SQLAlchem这个字段做主键索引,每个模型类必须有个一个主键才能正常工作。
设置表名,默认是该类的小写作为表名,在该类下添加:__tablename__='table_name'
可以更改,或将已经存在的名。
- init(): 其实我们可以省略定义 class User 的构造器. 这样的话 SQLAlchemy 会自动帮我们创建构造器, 并且所有定义的字段名将会成为此构造器的关键字参数名. EXAMPLE:
|
- repr(): 该方法返回一个对象的 字符串表达式. 与 str() 不同, 前者返回的是字符串表达式, 能被 eval() 处理;后者返回的是字符串, 不能被 eval() 处理得到原来的对象, 但与 print 语句结合使用时, 会被默认调用. 与 repr() 类似, 将对象转化为便于供 Python 解释器读取的形式, 返回一个可以用来表示对象的可打印字符串.
|
在数据库中根据模型创建表
在manager.py文件:
在控制台创建表:python manager,py shell
db.create_all()
SQLAlchemy的CRUD
在 manager shell 中完成:
- create增添数据
add: 把数据添加到会话对象中 (数据状态为待保存)
commit: 将会话对象中的数据提交 (数据被写入数据库中)
|
- retrieve 读取数据
通过 Model.query 方法对数据进行查询. Model.query == db.session.query(Model)
两种写法是等效的. 区别在于前者使用的是 flask_sqlalchemy.BaseQuery object, 后者使用的是 sqlalchemy.orm.query.Query object . 但两者本质上都是一个 Query 对象.
读取数据有两种情况:
获取一条记录:
|
Query的过滤器
在查询数据时, 可以根据一定的条件集合来获得过滤后的数据. SQLAlchemy 提供了过滤器 query.filter_by()
和 query.filter()
, 过滤器接受的参数就是过滤条件, 有下面几种形式:
- 字段键值对, EG.
username='fanguiju'
- 比较表达式, EG.
User.id > 100
- 逻辑函数, EG.
in_/not_/or_
|
可以将 query.filter()
内置的逻辑函数 in_/not_/or_
结合使用来实现更复杂的过滤.
两个过滤器的区别:
实质区别在于filter_by只能使用等号(不是==),应该更快,可以走索引
filter可以使用>,<取一定范围信息。用==。
获取多条记录:
|
分页函数:
pagination(): 是专门设计来实现分页功能的函数, 所以必须由 flask_sqlalchemy.BaseQuery object 来调用
|
分页模板:
|
- update 更新数据
|
如上述例子, 先定位到你希望更新的记录, 然后通过 Query 对象的 update()
传递要更新内容. 注意: 更新的内容必须是 Dict 数据类型.
需要注意的是: 就如使用原生 SQL 指令来更新记录一样, 如果没有指定要更新具体的哪一条记录的话, 会将该字段所在列的所有记录值一同更新, 所以切记使用过滤条件来定位到具体需要更新的记录.
而且 update()
会自动的添加 User 的实例化对象到 session 中, 所以直接 commit 就可以写入到数据库了.
具体更新可有两种方法:
|
- delete 删除数据
|
model间的关系
one to many 一对多
一个用户可以有博客,每篇博客之对应一个作者,它们之间的关系为一对多
class User(db.Model):__tablename__ = 'users'id = db.Column(db.String(45), primary_key=True)username = db.Column(db.String(255))password = db.Column(db.String(255))# Establish contact with Post's ForeignKey: user_id#会在 SQLAlchemy 中创建一个虚拟的列,该列会与 Post.user_id (db.ForeignKey) 建立联系posts = db.relationship('Post',backref='users',lazy='dynamic')class Post(db.Model):__tablename__ = 'posts'id = db.Column(db.String(45), primary_key=True)title = db.Column(db.String(255))text = db.Column(db.Text())publish_date = db.Column(db.DateTime)# Set the foreign key for Post,user_id 字段是 posts 表的外键user_id = db.Column(db.String(45), db.ForeignKey('users.id'))
如果你没有在父表类指定 __tablename__
属性,那么这一条语句我们应该这么写:
|
但是一般不建议写成这样,因为在 SQLAlchemy 初始化期间, User 对象可能还没有被创建出来,所以同时也建议在定义 models class 的时候应该指定 __tablename__
属性。
注意:表名长时用下划线,eg:user_others
不然外键这会报错`
- db.relationship
会在 SQLAlchemy 中创建一个虚拟的列,该列会与 Post.user_id
(db.ForeignKey) 建立联系。
第一个参数是关联的数据模型的类。
backref
:用于指定表之间的双向关系,如果在一对多的关系中建立双向的关系,这样的话在对方看来这就是一个多对一的关系。
lazy
:指定 SQLAlchemy 加载关联对象的方式。
lazy=subquery
: 会在加载 Post 对象后,将与 Post 相关联的对象全部加载,这样就可以减少 Query 的动作,也就是减少了对 DB 的 I/O 操作。但可能会返回大量不被使用的数据,会影响效率。lazy=dynamic
: 只有被使用时,对象才会被加载,并且返回式会进行过滤,如果现在或将来需要返回的数据量很大,建议使用这种方式。Post 就属于这种对象。
to use:
|
- many to many 多对多
一个博客和tag标签之间的关系是多对多:
多对多关系会在两个类之间增加一个关联表。 这个关联的表在 relationship()
方法中通过 secondary 参数来表示。通常的,这个表会通过 MetaData 对象来与声明基类关联, 所以这个 ForeignKey 指令会使用链接来定位到远程的表:
|
backref:声明表之间的关系是双向,帮助手册 help(db.backref)
。需要注意的是:在 one to many 中的 backref 是一个普通的对象,而在 many to many 中的 backref 是一个 List 对象。
实际上 db.Table 对象对数据库的操作比 db.Model 更底层一些。后者是基于前者来提供的一种对象化包装,表示数据库中的一条记录。 posts_tags 表对象之所以使用 db.Table 不使用 db.Model 来定义,是因为我们不需要对 posts_tags (self.name)进行直接的操作(不需要对象化),posts_tags 代表了两张表之间的关联,会由数据库自身来进行处理。
to use:
|
数据库迁移
工具Alembic可根据我们的SQLAlchemy模型的变化自动创建数据库迁移记录,保存了我们数据库结构变化的历史信息。
不然通过db.drop_all()和db.create_all()这样的方式会摧毁旧数据。
让我们升级或者降级到某个已保存的版本。这些历史文件本身就是python程序文件。
我们不会直接使用Alembic,而是会使用Flask-Migrate,这是为SQLAlchemy专门创建的一个扩展,并且可以跟Flask Script一起使用。
pip:pip install Flask-Migrate
添加到manage.py中:
|
通过app对象和SQLAlchemy的实例初始化了Migrate对象,通过命令来调用。
运行下面命令可以看到可用命令列表:$python manage.py db
开始跟踪我们的数据库变更:$python manage.py db init
上面这个命令会在项目目录里面创建一个叫migrations的文件夹,所有的记录文件都会被保存在里面。我们可以进行首次迁移:python manage.py db migrate -m"initial migration
上面这个命令会让Alembic扫描我们所有的SQLAlchemy对象,找到在此之前没有被记录过的所有表和列,-m参数来提交保存信息,通过提交保存信息寻找所需的迁移记录版本是最容易的方法。
每个迁移记录文件都被保存在migrations/version/文件中。
执行下面命令,把迁移记录应用到数据库上,并改变数据库的结构:
每次改变表结构的时候更新就好了。原有数据还不会清除。
python manage.py db upgrade
要返回以前的版本通过history命令找到版本号:python manage.py db history
再把版本好给downgrade命令:pyhon manage.py db downgrade 7ded34bc4fb
同git一样,每个迁移记录都由一个哈希值来表示。可以将迁移记录和git提交记录对应起来。
补充
- 回滚
db.session.rollback()
- 分组查询
db.session.query(User).group_by(User.name).all()
- 常用的SQLAlchemy 查询执行器:
对于给定的查询还可以检查SQLAlchemy生成的原生SQL查询,并将查询对象转换为一个字符串:
str(User.query.filter_by(role=user_role))
WTForm
基础
Flask提供的请求对象能提供用于处理web表单的信息,如request.form能获取post请求中提交的表单数据。WTForm是一个服务端表单检验库,它可对常见的表单类型进行输入合法性验证。
Flask WTForm是基于WTForm的Falsk扩展,增加了一些特性:JinjiaHTML渲染,预防跨域请求伪造(CSRF)和SQL注入攻击。
安装:pip install Flask-WTF
为了让WTForm的安全工作能够正常工作,我们需要一个密钥,来生成加密的签名。
这个密钥可以在多个扩展中使用。
在config.py里的Config对象:
|
WTForm 由三部分组成:
- 字段:对输入的初步检查
- 检验器:在字段上加的函数,用来确保输入字符的限制条件。
- 表单:一个python类,
forms.py加入如下内容from flask_wtf import Form # 最新改为FlaskFromfrom wtforms import StringField,SubmitFieldfrom wtforms.validators import DataRequiredclass NameForm(FlaskForm): #使用Flask-WTF时,每个web表单都继承一个From类。name = StringField('What is your named',validators=[DataRequired(method="添加内容不能为空")]) #这里自定义错误内容,会在后面的模板文件中显示-errorssubmit = SubmitField('Submit')
Form基类由Flask-WTF扩展定义,从flask_wtf中导入。
字段和验证函数可以直接从wtforms包中导入。
字段类的第一个参数为输入框标题,如果是按钮就是按钮名字,第二个参数为绑定到该字段的检验器列表,由 wtforms.validators 提供
支持的html字段
字段名称 | 说明 |
---|---|
StringField | 文本字段 |
TextAreaField | 多行文本字段 |
PasswordField | 密码文本字段 |
HiddenField | 隐藏文本字段 |
DateField | 文本字段,值为datetime.date格式 |
DateTimeField | 文本字段,值为datetime,datetime格式 |
IntegerField | 文本字段,值为整数 |
DecimalField | 值为decimal.Decimal |
FloatField | 文本字段,值为浮点数 |
BooleanField | 复选框,值为True和False |
RadioField | 一组单选框 |
SelectField | 下拉列表 |
SelectMultipleField | 下拉列表,可以选多个值 |
FileField | 文件上传字段 |
SubmitField | 表单提交按钮 |
FormField | 把表单作为字段嵌入另一个表单 |
FieldList | 一组指定类型的字段 |
表单验证函数
如上文的参数validators,它定义了该数据的验证器列表,如DataRequired()代表了输入值不能为空。
| 验证函数 | 说明 |
| ———— | ———————- |
| DataRequired | 字段不能为空 |
| Emai | 验证电子邮箱 |
| EqualTo | 比较两个字段的值,常用与输入两次密码进行确认 |
| IPAddress | 验证IPv4的网络地址 |
| Length | 验证输入字符串的长度 |
| NumberRange | 验证输入的值在数字范围内 |
| Optional | 无输入值时跳转其他验证函数 |
| Regexp | 使用正则表达式验证输入值 |
| URL | 验证URL |
| AnyOf | 确保输入值在可选值列表中 |
| NoneOf | 确保输入值不在可选值列表中 |
自定义检验器
定义一个函数,接受表单对象和字段对象为参数,不符合要求抛出一个WTForm.ValidationError:
视图函数中处理表单
|
methods参数告诉函数注册为POST和GET的请求提交。
get提交的数据以字符串的形式附加到URL中,大多数以post提交。
validate_on_submit函数,如果数据能被所有验证函数接受,返回True。否则false。
模板接收表单
|
这里的form就是我们传过来的那个参数。
label ,是定义表单类中国要显示的名字,form.name是定义的输入框,后面的可以定义控件的样式,’邮箱’是默认显示内容
但是这种方式太过于繁琐,我们更推荐用Flask-Bootstrap
|
如果validate_on_submit()没有响应,记得加:
在csrf保护中会讲
重定向和用户会话session
当用户刷新时,浏览器会有提示框,因为刷新页面时浏览器会重新发送之前已经发送过的最后一个请求。
解决方案:不让Web程序把POST请求作为浏览器发送的最后一个请求——重定向。、
并在刷新后记住上次填的内容,用到session。
- session[‘name’]存储了同一个会话的中文本框中的内容
- redirect()是个辅助函数,用来生成http重定向响应,参数是重定向的URL
- url_for(),当然redirect(url_for(‘index’))也可以写成redirect(‘/‘),但是推荐使用url_for()生成URL,因为这个函数使用URL映射生成URL,从而保证URL和定义的路由兼容,而且修改路由名字后依然后依然可用。``url_for()* session.get('name')直接从会话中中读出name参数的值,也可以使用session['name']读取,但是推荐使用session.get('name'),使用get()获取字典中键对应的值以避免未找到键的异常情况,因为对于不存在的键,get()会返回默认值None.#### flush消息用户提交了一项有错误的登陆表单后,服务器发回的相应重新渲染了登陆表单,并在表单上显示一个消息,提示用户用户名或密码错误。这是Flask的核心特性,flash函数可以实现这种效果。```pythonfrom flask import Flask,render_template,session,redirect,url_for,flash@app.route('/',methods=['GET','POST'])def index():form=NameForm()if form.validate_on_submit():old_name=session.get('name') # 和上次的名字做比较if old_name is not None and old_name!=from.name.data:flash('你已经改变了名字')session['name']=fomr.name.data #存储上这次的名字return redirect(url_for('index'))return render_template('index.html',form=form,name=session.get('name'))
这个示例中,每次提交的名字会储存在用户会话中的名字进行比较,而会话中存储的名字是前一次在这个表单中提交的数据。如果两个名字不一样,就会调用flash()函数,再发给客户端的下一个相应中显示一个消息。
我们要将flush消息显示出来:
|
在模板中使用循环是因为在之前的请求循环中每次调用flash()函数都会生成一个消息,所以可能有多个消息在排队,get_flashed_messages()函数获取的消息在下次调用时不会再次返回,只显示一次就消失。
CSRF保护
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,CSRF则通过伪装来自受信任用户的请求来利用受信任的网站。
用flask_wft实现的表单已经免受CSRF的威胁,,尽管如此没有包含表单的试图,仍需要保护。
为了能够让所有的视图函数受到 CSRF 保护,你需要开启 CsrfProtect
模块:
|
可惰性加载:
|
|
但是如果模板中没有表单,你仍然需要一个 CSRF 令牌:
|
无论何时未通过 CSRF 验证,都会返回 400 响应。你可以自定义这个错误响应:
|
CSRF 保护的大部分功能都能工作(除了 form.validate_on_submit()
,所以我们在用的时候,记得在渲染模板时加上: