django提供了:
django-admin快速创建项目工程目录
manage.py 管理项目工程
orm模型(数据库抽象层)
admin后台管理站点
缓存机制
文件存储系统
用户认证系统
flask提供了:
啥也没提供…
蛋,flask有丰富的扩展包可以满足各种需求
Flask-SQLalchemy:操作数据库;
Flask-migrate:管理迁移数据库;
Flask-Mail:邮件;
Flask-WTF:表单;
Flask-script:插入脚本;
Flask-Login:认证用户状态;
Flask-RESTful:开发REST API的工具;
Flask-Bootstrap:集成前端Twitter Bootstrap框架;
Flask-Moment:本地化日期和时间;
中文文档:http://docs.jinkan.org/docs/flask/
英文文档:https://flask.palletsprojects.com/en/0.12.x/,已更新到1.1
目前很多扩展包使用Python2,因此创建基于Python2的虚拟环境
同样的,一个环境一堆事,建立新的虚拟环境是非常必要的
mkvirtualenv flask_py # 默认创建py3的
deactivate
rmvirtualenv flask_py # 删除
which python # python执行目录
mkvirtualenv -p /usr/bin/python2.7 flask_py2 # 不能sudo
cd .virtualenv/flask_py2/
# 在bin下拷贝py2的解释器
# 在lib下用python2.7的包,达到隔离的目的
配置虚拟环境
# 可以使用 pip list 查看已安装包
# 也可将现有的环境复制一份清单
pip freeze > packages.txt
pip install -r packages.txt
安装
pip install Flask # 可以参考官方文档 1.1.2
对比Django学习
Hello World:hello.py
# coding:utf-8
# 导入Flask类,尽量不要使用 * 导入,有歧义
from flask import Flask
# Flask类接收一个参数__name__
# __name__代表当前模块名称,这里就以hello.py文件所在目录为总目录
# 从而定位同级static和templates文件夹
app = Flask(__name__) # 可以传任意字符串,默认也是将当前文件作为启动文件
# 装饰器的作用是将路由映射到视图函数index
# 视图也不需要request,动态路由直接传re匹配的参数名即可
# 路径和参数都交给route处理=Django的urls=装饰器+application
@app.route('/')
def index():
return 'Hello World'
# Flask应用程序实例的run方法启动自带WEB服务器
# 项目上线再替换
if __name__ == '__main__':
app.run() # 没有manage.py管理脚本
127.0.0.1:5000
abc
是python内置模块哦关于__name__
# test.py
# 以此文件直接运行
print(__name__) # 输出 __main__
# 若导入到其他模块
# 输出 test
参数配置
from flask import Flask
app = Flask(__name__,
# 默认值是/static
static_url_path = "/python", # 访问的是静态文件,但使用/python/...
static_folder = "static", # 默认
template_folder = "templates", # 默认
)
# 配置参数并使用,三种方式
app.config.from_pyfile('config.cfg') # 从文件拿,还是以当前文件所在目录查找
app.config.from_object('app.Config') # 从对象拿,自定义class
app.config['DEBUG'] = True # 直接操作字典对象(内置配置) Debuger is active
class Config(object):
DEBUG = True # 开启调试模式
# 视图函数拿取配置参数
@app.route('/')
def index():
pring(app.config.get('DEBUG')) # 字典的get()方法
return 'Hello World'
# 方式二
from flask import current_app
@app.route('/')
def index():
pring(current_app.config.get('DEBUG')) # 字典的get方法
return 'Hello World' # 可以直接返回响应体
运行服务器:需要配上host,不然防火墙过不去
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000) # 服务器绑定IP,任何都可访问
按照路由——视图——模板——模型的顺序学习
查看当前路由配置
print(app.url_map)
限定路由方法
@app.route('/post_get', methods=['POST','GET']) # 方法不对报错 405
def post_get():
return 'both post and get method are allowed '
# 和Django相同,如果路由相同(方法也同),先定义的会覆盖后定义的,按顺序访问
# 如果多路由一视图:
@app.route('/h1')
@app.route('/h2')
def hello():
return "hello world"
重定向,配合反向解析 url_for
from flask import Flask, redirect
@app.route('/login')
def login():
# 类似于reverse(),通过name属性找到路由名称,即是路由名称改变也不会影响
url = url_for("index") # 传递视图函数名称
return redirect(url) # 跳转报 302
动态路由(获取参数)
# 类似Django的转换器
@app.route('/user/' ) # 还支持 float、path(接受 /)
def hello_itcast(id):
return 'hello Roy %d' %id
# 默认使用字符串规则
@app.route('/user/' ) # 后面的参数会以字符串形式被接收,名为id,不接受 /
自定义转化器
# 自定义转换器获取路由参数
from werkzeug.routing import BaseConverter
class RegexConverter(BaseConverter):
# 外部传入regex即可
def __init__(self, url_map, regex):
# 父类初始化,交给flask操作
super(RegexConverter, self).__init__(url_map)
# 将正则表达式的参数保存到对象属性中,flask就会使用这个pattern进行正则匹配
self.regex = regex
# 自定义转换器添加到flask应用中;url_map有点像request,对象
app.url_map.converters['re'] = RegexConverter # 例如还有 IntConverter...
@app.route("/mobile/" )
def mobile(numbers):
return numbers
re_path
regex
写死:class Mobile(BaseConverter):
def __init__(self, url_map, regex):
# 父类初始化,交给flask操作
super(RegexConverter, self).__init__(url_map)
# 将正则表达式的参数保存到对象属性中,flask就会使用这个pattern进行正则匹配
self.regex = r'1[34578]\d{9}'
converters
字典中获取类对象,绕了一圈得到匹配规则,提取参数# 这是基类定义
class BaseConverter(object):
"""Base class for all converters."""
regex = "[^/]+"
weight = 100
def __init__(self, map): # 需要传递map
self.map = map
def to_python(self, value):
return value
def to_url(self, value):
if isinstance(value, (bytes, bytearray)):
return _fast_url_quote(value)
return _fast_url_quote(text_type(value).encode(self.map.charset))
to_python
方法中,直接返回了匹配得到的值,我们在这里可以做点别的!class Mobile(BaseConverter):
def __init__(self, url_map, regex):
super(RegexConverter, self).__init__(url_map)
self.regex = r'1[34578]\d{9}'
def to_python(self, value):
# return value
return "abcd" # 这只是举例说明,最后匹配的结果要经过这里返回
to_url
方法呢?之前使用url_for
反向解析,目的是得到路由进行重定向,传入的是视图函数名称# 如何把参数也传过去呢?对应mobile视图
@app.route("/mobile/" )
def mobile(numbers):
return numbers
@app.route("/login")
def login():
# def url_for(endpoint, **values):
url = url_for("mobile", numbers=13219510963)
# 这里url_for先去找视图函数,拿着url会调用to_url方法,返回最后的url
return redirect(url)
class Mobile(BaseConverter):
def __init__(self, url_map, regex):
super(RegexConverter, self).__init__(url_map)
self.regex = r'1[34578]\d{9}'
def to_url(self, value):
# return value # 原本返回:/mobile/13219510963
return "18813008122" # 就不会返回传入的numbers参数了
以上便是完整的路由匹配的内容
路由参数是框架学习的重难点,后面会通过项目具体总结一下,各种参数的获取和传递
动态路由获取参数后,如何传递给视图函数呢?
在flask中,使用全局的request
传递参数,也是对象
from flask import Flask, request
参数都可通过此对象的属性获取,区别就在于参数的传递方法和类型了:
key=value&
的形式,也可以包含文件,flask会将其处理成类似字典的形式MultiDict
,类似Django中的TypeDict
data
属性不能拿出表单数据,可以拿其他请求体中的数据,都搞成字符串args
专门获取查询字符串参数,即url中用?key=value&
的参数
前端先定义模板携带参数,然后后端提供数据
app = Flask(__name__)
@app.route('/index', methods=['GET','POST'])
def index():
name = request.form.get('name') # 尽量不要直接用['']获取字典值
age = request.form.get('age')
names = request.form.getlist('name') # 重复键名
city = request.args.get('city')
print("data:%s"%request.data)
return "name=%s, age=%s, namelist=%s,city=%s"%(name.age,names,city)
为了避免修改代码测试,使用postman工具模拟请求
还是使用request对象的属性
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST': # 不用方法对应不同逻辑,也是常见策略
f = request.files['the_file'] # 表单的name属性值
f.save('/var/www/uploads/uploaded_file.txt')
# 上面这么写有点low了
@app.route('/upload', methods=['POST'])
def upload_imgs():
file = request.files.get('img')
if file is None:
return "未上传..." # 健壮了是不是
# 打开文件
f = open('./demo.png', 'wb') # 打开写入空间
data = file.read()
f.write(data)
f.close()
# 直接使用文件对象保存
# file.save('./demo.png')
return "上传成功"
深入解析with
函数
# 使用with可以自动捕获异常,上面就可以写成
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
file = request.files.get('img')
if file is None:
return "未上传..."
with open("./demo.png", "wb") as f: # 一般是在读取时使用,更有可能异常
data = file.read()
f.write(data)
# with为什么能捕获异常?实则是open类的功劳
class OpenFiles(object):
def __enter__(self): # 刚进入with语句时
print("enter...")
def __exit__(self, exc_type, exc_val, exc_tb):
# 离开with语句时调用
print("异常类型:%s" % exc_type)
print("异常提示:%s" % exc_val)
print("追踪信息:%s" % exc_tb)
self.close() # with自动关闭打开的文件
with OpenFiles("file.png", "wb") as of:
print("自定义with使用的对象...")
a = 1/0
print("异常结束...")
abort()
函数,异常处理
from flask import Flask, abort, Response
# 终止当前视图函数的运行, 并传递信息
@app.route("/login")
def login():
if name != 'roy' or password != '123456':
# 返回给前端信息
abort(400) # 必须是标准状态码
res = Response("login failed")
abort(res) # 返回响应体信息,不能直接传字符串
@app.errorhandler(404)
def error(err): # 必须接收一个参数,方便如下自定义吧,不然还不如直接在外面print了
return '您请求的页面不存在,请确认后再次访问!%s'%err
返回自定义响应信息,之前数据流是从前向后,现在从后向前
可以直接return响应体、状态码、响应头等信息,也可以使用make_response
对象
# 使用元祖,返回自定义响应信息
@app.route('/index')
def index():
# 元祖的两个元素组成键值对
# 响应体 状态码 响应头
return "index~", 400, [('name', 'roy'), ('prov', 'NingXia')]
# {'name':'roy', 'prov':'NingXia'} 用字典也可以
# 自定义状态码并附加说明信息: "888 special"
from flask import Flask, make_response
# 构造响应头信息
@app.route('/index')
def index():
resp = make_response("success") # 响应体
resp.headers["sample"] = "value"
resp.status = "404 not found"
return resp
python字典与json字符串互化:
u
在字符串前面是转成Unicode编码(py2遗留问题)import json
# 直接返回json格式数据,响应头中的content-type还是text/html
def index():
# request拿到数据,处理...
......
data = {
"name":"roy",
"age":18
}
json_str = json.dumps(data)
return json_str, 200, {
"Content-Type":"application/json"}
# 这么多事让我做?类似于JsonResponse
from flask import jsonify
def index():
# request拿到数据,处理...
......
data = {
"name":"roy",
"age":17
}
return jsonify(data) # 以json返回
make_response
添加响应头,而非request对象from flask import Flask, make_response, request
@app.route('/set_cookie')
def set_cookie():
resp = make_response("success") # 通过返回设置,添加响应头:Set-Cookie
resp.set_cookie("name","roy") # 关闭浏览器失效
resp.set_cookie("age", "18", max_age=3600) # 而不是request对象了
return resp
@app.route('/get_cookie')
def get_cookie():
cookie = request.cookies.get("name") # 获取是request
return cookie
@app.route('/del_cookie')
def del_cookie(): # 无法真正删除,只能设置过期
resp = make_response("del success")
resp.delete_cookie("age")
return resp
session
模块from flask import Flask, session
app.config['SECRET_KEY'] = 'fdnavnrNOVFNONOnnce147r1qNFDAIN' # 随机设置密钥
@app.route('/login')
def login():
session['name'] = 'roy'
session['age'] = 18
return "login... "
@app.route('/get_session')
def get_session():
name = session.get('name')
return name
if __name__ = '__main__':
app.run(debug=True)
session_id
,session信息保存在后端数据库,但是flask不一样上下文是指当前用户所处的环境,例如request是全局对象,当同一时刻有多个用户请求时存在竞争,此时的request代表哪个用户呢?
怎么结合具体的用户请求呢?线程编号(一个线程对应一个键值)
每个用户对应一个线程编号,可以理解成request是线程内全局变量,线程间的局部变量
请求上下文(request context) :request和session都属于请求上下文对象
应用上下文(application context):current_app和g都属于应用上下文对象
app = Flask(__name__)
,关系到后端环境g
类似一个空对象,可以自己设置属性名称存点东西;哪里不能存,非朝这里存?from flask import g
@app.route('/login')
def login():
g.username = 'roy'
index()
return "OK..."
def index():
name = g.username # 在一次请求的多个视图函数之间传递变量
print("fucking...")
特点是每次请求之前都会清空保存的属性
hook
请求钩子类似于css中的伪类选择器before和after,或者说中间件
请求钩子是通过装饰器的形式实现,Flask支持如下四种:
# before_first_request:在处理第一个请求前运行。
@app.before_first_request
# before_request:在每次请求前运行
@app.before_request
# after_request(response):如果没有未处理的异常抛出,在每次请求后运行
@app.after_request
# teardown_request(response):在每次请求后运行,即使有未处理的异常抛出
@app.teardown_request
request.path
获取请求路径:例如127.0.0.1:5000/index
得到 /index
@app.teardown_request
def handle_teardown_request(response):
path = request.path
if path in [url_for("index"), url_for("login")]:
print("钩子中判断视图逻辑:index")
else:
print("未包含相关请求路径")
return response
需要安装扩展包 pip install Flask-Script
# manager_test.py
from flask import Flask
from flask_script import Manager
app = Flask(__name__)
manager = Manager(app) # 管理当前应用
@app.route('/')
def index():
return '床前明月光'
if __name__ == "__main__":
manager.run()
此时需要使用命令行启动此文件,类似Django中的manage.py
python manager_test.py runserver # 还支持shell,但不需要再像ipython导入了
和Django中类似,在模板中新建index.html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<p>{
{name}}p>
<p>{
{age}}p>
<p>{
{dicts.city}}p>
<p>{
{dicts["county"]}}p>
<p>{
{lists[0]}}p>
body>
html>
使用render_template
渲染
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/index')
def index():
data = {
"name":"flask",
"function":"zhuangB",
"dicts":{
"city":"BJ", "county":"HD"},
"lists":[1,2,3,4,5]
}
return render_template("index.html", **data) # 可以直接平铺在这
# return render_template("index.html", name="flask",function="zhuangB")
类似的,提供以下转义
// safe:把转义禁用;
<p>{
{
'hello' | safe }}</p>
// capitalize:把变量值的首字母转成大写,其余字母转小写;
<p>{
{
'hello' | capitalize }}</p>
// lower:把值转成小写;
<p>{
{
'HELLO' | lower }}</p>
// upper:把值转成大写;
<p>{
{
'hello' | upper }}</p>
// title:把值中的每个单词的首字母都转成大写;
<p>{
{
'hello' | title }}</p>
// trim:把值的首尾空格去掉;
<p>{
{
' hello world ' | trim }}</p>
// reverse:字符串反转;
<p>{
{
'olleh' | reverse }}</p>
// format:格式化输出;
<p>{
{
'%s is %d' | format('name',17) }}</p>
// striptags:渲染之前把值中所有的HTML标签都删掉;
<p>{
{
'hello' | striptags }}</p>
支持链式使用过滤器
<p>{
{
“ hello world “ | trim | upper }}</p>
列表过滤器
// first:取第一个元素
<p>{
{
[1,2,3,4,5,6] | first }}</p>
// last:取最后一个元素
<p>{
{
[1,2,3,4,5,6] | last }}</p>
// length:获取列表长度
<p>{
{
[1,2,3,4,5,6] | length }}</p>
// sum:列表求和
<p>{
{
[1,2,3,4,5,6] | sum }}</p>
// sort:列表排序
<p>{
{
[6,2,3,1,5,4] | sort }}</p>
xss指注入恶意指令代码到网页
如果此时提交内容如下:
<script>
alert("Hello Attack")
</script>
注:Chrome是自动防范xss攻击的,可在火狐测试
自定义的过滤器名称如果和内置的过滤器重名,会覆盖内置的过滤器
有两种自定义方式
add_template_filter
函数注册def filter_double_sort(ls): # 必须传参啊
return ls[::2] # [0:end:2]
# 参数:过滤器函数, 模板中使用的过滤器名称
app.add_template_filter(filter_double_sort,'step_2')
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<p>{
{lists | step_2}}p>
body>
html>
template_filter
装饰器@app.template_filter('ver_3') # 传递过滤器名称
def filter_double_sort(ls):
return ls[::-3]
前端会对提供的数据进行校验,但是数据可以伪造,因此无论前端是否校验,后端都要校验数据
对表单数据的校验有特定的方式,因此可以抽象化
使用Flask-WTF
表单扩展,可以帮助进行CSRF验证,帮助我们快速定义表单模板,而且可以在视图中验证表单的数据,
即后端定义前端模板,前端提交后反过来进行验证,并进行相应处理(后-前-后)
安装环境pip install Flask-WTF
<form method="post">
{
{ form.csrf_token }}
{
{ form.us.label }}
<p>{
{ form.us }}p>
{
{ form.ps.label }}
<p>{
{ form.ps }}p>
{% for msg in form.ps.errors %}
<p>{
{ msg }}p>
{% endfor %}
{
{ form.ps2.label }}
<p>{
{ form.ps2 }}p>
{% for msg in form.ps2.errors %}
<p>{
{ msg }}p>
{% endfor %}
<p>{
{ form.submit }}p>
{% for x in get_flashed_messages() %}
{
{ x }}
{% endfor %}
form>
定义表单的模型类,一般是数据库ORM,这里是表单模型,后台定义表单渲到前端
from flask import Flask,render_template, redirect,url_for,session,request,flash
# 导入wtf扩展的表单类
from flask_wtf import FlaskForm
# 导入自定义表单需要的字段,有支持的标准HTML字段
from wtforms import SubmitField,StringField,PasswordField
# 导入wtf扩展提供的表单验证器,有提供常用验证器
from wtforms.validators import DataRequired,EqualTo
app = Flask(__name__)
app.config['SECRET_KEY']='anfojac13rCAWac'
#自定义表单模型类
class Login(Flask Form): # 以后中文属性前面都要加u
us = StringField(label=u'用户:',validators=[DataRequired(u"用户名不能为空")])
ps = PasswordField(label=u'密码',validators=[DataRequired(u"密码不能为空")])
ps2 = PasswordField(label=u'确认密码',validators=[DataRequired(),EqualTo('ps',u'两次密码不一致')])
submit = SubmitField(u'提交') # 提交后还是到这里来
# 定义根路由视图函数,生成表单对象,获取表单数据,进行表单数据验证
@app.route('/register',methods=['GET','POST'])
def register():
form = Login() # 实例化,从视图函数把表单模板渲染出来
# 表单提交,如果验证通过:
if form.validate_on_submit():
name = form.us.data
pswd = form.ps.data
pswd2 = form.ps2.data
print name,pswd,pswd2 # 后端打印
session['username'] = name
return redirect(url_for('index'))
else:
if request.method=='POST': # 通过请求体提交的数据才刷新(删除已输入)
flash(u'信息有误,请重新输入!')
return render_template('index.html',form=form)
@app.route('/index')
def index():
username = session.get('username')
return "Hello, %s"%username
if __name__ == '__main__':
app.run(debug=True)
WTForms支持的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 | 一组指定类型的字段 |
WTForms常用验证器:
验证函数 | 说明 |
---|---|
DataRequired | 确保字段中有数据 |
EqualTo | 比较两个字段的值,常用于比较两次密码输入 |
Length | 验证输入的字符串长度 |
NumberRange | 验证输入的值在数字范围内 |
URL | 验证URL |
AnyOf | 验证输入值在可选列表中 |
NoneOf | 验证输入值不在可选列表中 |
{% macro input() %}
<input type="text"
name="username"
value=""
size="30"/>
{% endmacro %}
{
{ input() }}
{% macro input(name='define',value='',type='text',size=20) %}
<input type="{
{ type }}"
name="{
{ name }}"
value="{
{ value }}"
size="{
{ size }}"/>
{% endmacro %}
{
{ input(value='name',type='password',size=40)}}
{% macro input() %}
<input type="text" name="username" placeholde="Username">
<input type="password" name="password" placeholde="Password">
<input type="submit">
{% endmacro %}
{% import 'macro.html' as func %}
{% func.input() %}
Web应用中普遍使用的是关系数据库,把所有的数据都存储在表中,使用结构化的查询语言。关系型数据库的列定义了表中表示的实体的数据属性
Flask本身不限定数据库的选择,你可以选择SQL或NOSQL的任何一种
也可以选择更方便的SQLALchemy
,类似于Django的ORM
SQLAlchemy实际上是对数据库的抽象,让开发者不用直接和SQL语句打交道,而是通过Python对象来操作数据库,在舍弃一些性能开销的同时,换来的是开发效率的较大提升
安装扩展:
pip install flask-sqlalchemy # 这只是一个将python模型类转化成sql语句的工具
pip install flask-mysqldb # 仍然需要拿着sql操作数据库
flask-mysqldb
相当于对Python2中使用的MySQL-Python
进行封装,都是数据驱动数据库设置
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/flask_test'
# 自动跟踪数据库
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
SQLAlchemy支持的字段类型,对应MySQL的字段要求
类型名 | python中类型 | 说明(mysql) |
---|---|---|
Integer | int | 普通整数,一般是32位 |
SmallInteger | int | 取值范围小的整数,一般是16位 |
BigInteger | int或long | 不限制精度的整数 |
Float | float | 浮点数 |
Numeric | decimal.Decimal | 普通整数,一般是32位 |
String | str | 变长字符串 |
Text | str | 变长字符串,对较长或不限长度的字符串做了优化 |
Unicode | unicode | 变长Unicode字符串 |
UnicodeText | unicode | 变长Unicode字符串,对较长或不限长度的字符串做了优化 |
Boolean | bool | 布尔值 |
Date | datetime.date | 时间 |
Time | datetime.datetime | 日期和时间 |
LargeBinary | str | 二进制文件 |
同样,完整性约束不能少:
选项名 | 说明 |
---|---|
primary_key | 如果为True,代表表的主键 |
unique | 如果为True,代表这列不允许出现重复的值 |
index | 如果为True,为这列创建索引,提高查询效率 |
nullable | 如果为True,允许有空值,如果为False,不允许有空值 |
default | 为这列定义默认值 |
试着定义一个模型类
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
#设置连接数据库的URL
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/flask_test'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
#设置每次请求结束后会自动提交数据库中的改动
# app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
#查询时会显示原始SQL语句
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app) # 得到数据库操作对象
class Role(db.Model):
# 自定义表名,一般对表名都是有制定规范的
__tablename__ = 'roles'
# 定义列对象
id = db.Column(db.Integer, primary_key=True) # Column也代表真实数据
name = db.Column(db.String(64), unique=True)
users = db.relationship('User', backref='role') # 方便一查多
# flask没有Django的 user_set语法
# backref='role' 相当于给User添加了一个属性,让User查Role的时候不再是得到id,而是直接得到对应的行值;(非必要)
#repr()方法显示一个可读字符串
def __repr__(self):
return 'Role:%s'% self.name
# 注意定义格式,该背的,就背一背
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True, index=True) # 给name字段加索引
email = db.Column(db.String(64),unique=True) # 可以在唯一字段加索引
pswd = db.Column(db.String(64))
role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) # 多端(一个角色多个User),多查一
def __repr__(self):
# query.get()时显示的信息格式
# 类似__str__()
return 'User:%s'%self.name
if __name__ == '__main__':
db.drop_all() # 第一次建库才做,清除所有数据
# 建表
db.create_all()
# 实例化对象插入新数据
ro1 = Role(name='admin')
ro2 = Role(name='user')
# db.session记录对象任务(类似于git中的add,到暂存区)
db.session.add_all([ro1,ro2])
# 提交任务执行
db.session.commit() # 由于建立了跟踪,ro1/ro2的id被同步
us1 = User(name='wang',email='[email protected]',pswd='123456',role_id=ro1.id)
us2 = User(name='zhang',email='[email protected]',pswd='201512',role_id=ro2.id)
us3 = User(name='chen',email='[email protected]',pswd='987654',role_id=ro2.id)
us4 = User(name='zhou',email='[email protected]',pswd='456789',role_id=ro1.id)
db.session.add_all([us1,us2,us3,us4])
db.session.commit()
app.run(debug=True)
SQLAlchemy可能因为MySQL的版本问题出现隔离级别错误,需要修改其源代码:base.py
常用的SQLAlchemy查询执行器
方法 | 说明 |
---|---|
all() | 以列表形式返回查询的所有结果 |
first() | 返回查询的第一个结果,如果未查到,返回None |
first_or_404() | 返回查询的第一个结果,如果未查到,返回404 |
get() | 返回指定主键对应的行,如不存在,返回None |
get_or_404() | 返回指定主键对应的行,如不存在,返回404 |
count() | 返回查询结果的数量 |
paginate() | 返回一个Paginate对象,它包含指定范围内的结果 |
count()
和first()
非常常用常用的SQLAlchemy查询过滤器
过滤器 | 说明 |
---|---|
filter() | 把过滤器添加到原查询上,返回一个新查询 |
filter_by() | 把等值过滤器添加到原查询上,返回一个新查询 |
limit | 使用指定的值限定原查询返回的结果 |
offset() | 偏移原查询返回的结果,返回一个新查询 |
order_by() | 根据指定条件对原查询结果进行排序,返回一个新查询 |
group_by() | 根据指定条件对原查询结果进行分组,返回一个新查询 |
查询,方法有两类:flask-mysql和sqlalchemy
# 类.query查询,flask-mysqldb的方法
us = User.query.all()
ro1 = us[0]
ro1.name
User.query.get(1) # 拿到id=1的数据,对应的显示信息在类中的__repr__魔术方法指定
User.query.filter_by(name='wang').all()
# db.session查询,SQLAlchemy的方法
db.session.query(Role).all()
# 或查询
User.query.filter(or_(User.name=='wang', User.email.endswith('163.com'))).all
# 同样的还有and_ not_
# 跳过两条,从记录第一条开始跳
User.query.offset(2).all()
# 如果这样写呢?
User.query.all().offset(2)
User.query.order_by(User.id.asc()).all() # desc()
# 分组查询
db.session.query(User.role_id, func.count(User.role.id)).group_by(User.role_id).all()
# 关联查询
# 一查多,建立了relationship之后
Role.users[0].name
# 多查一
u = User.query.get(1)
# 没有反向引用
Role.query.get(u.role_id)
# 若backref,直接查询这个角色下的所有用户
u.role
db.session.query
,接上过滤器,真香!label('xxx')
给查询出来的值添加常量名,可以用对象. 的方式调用查询更新
# 链式操作
User.query.filter_by(User.name='zhou').update({
'name':'yang', 'email':'[email protected]'})
查询删除
u = User.query.get(3)
db.session.delete(u)
一般在执行更新和删除之前,都要先进行查询验证,看条件是否正确,防止误操作
图书添加删除
分别建立作者和书名类:
#coding=utf-8
from flask import Flask,render_template,redirect,url_for
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
#设置连接数据
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/test1'
#设置每次请求结束后会自动提交数据库中的改动
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
#设置成 True,SQLAlchemy 将会追踪对象的修改并且发送信号。这需要额外的内存, 如果不必要的可以禁用它。
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
#实例化SQLAlchemy对象
db = SQLAlchemy(app)
#定义模型类-作者
class Author(db.Model):
__tablename__ = 'author'
id = db.Column(db.Integer,primary_key=True)
name = db.Column(db.String(32),unique=True)
email = db.Column(db.String(64))
au_book = db.relationship('Book',backref='author')
def __str__(self):
return 'Author:%s' %self.name
#定义模型类-书名
class Book(db.Model):
__tablename__ = 'books'
id = db.Column(db.Integer,primary_key=True)
info = db.Column(db.String(32),unique=True)
leader = db.Column(db.String(32))
au_book = db.Column(db.Integer,db.ForeignKey('author.id'))
def __str__(self):
return 'Book:%s,%s'%(self.info,self.lead)
表单界面:使用WTF,从后端到前端
from flask import Flask,render_template,url_for,redirect,request
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from wtforms.validators import DataRequired
from wtforms import StringField,SubmitField
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@localhost/test1'
# app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.config['SECRET_KEY']='s'
db = SQLAlchemy(app)
# 创建表单类,用来添加信息
class Append(Form):
au_info = StringField(validators=[DataRequired()])
bk_info = StringField(validators=[DataRequired()])
submit = SubmitField(u'添加')
@app.route('/',methods=['GET','POST'])
def index():
# 创建表单对象
form = Append()
# 查询所有作者和书名信息
author = Author.query.all()
book = Book.query.all()
# -----------------------------------------
if form.validate_on_submit(): # 验证通过,返回数据插入数据库
# 获取表单输入数据
wtf_au = form.au_info.data
wtf_bk = form.bk_info.data
# 把表单数据存入模型类
db_au = Author(name=wtf_au)
db_bk = Book(info=wtf_bk)
# 提交会话
db.session.add_all([db_au,db_bk])
db.session.commit()
#添加数据后,再次查询所有作者和书名信息
author = Author.query.all()
book = Book.query.all()
return render_template('index.html',author=author,book=book,form=form)
else:
if request.method=='GET':
render_template('index.html', author=author, book=book,form=form)
# ----------------------------------------
return render_template('index.html',author=author,book=book,form=form)
# 使用ajax异步请求
# 删除作者
@app.route('/delete_author' )
def delete_author(aid):
#精确查询需要删除的作者id
au = Author.query.filter_by(id=aid).first()
db.session.delete(au)
#直接重定向到index视图函数
return redirect(url_for('index'))
# 删除书名
@app.route('/delete_book' ) # /delete_book{
{book_id}}
# 前端还可以使用 /delete_book/{
{book_id}}
# 也可以 /delete_book?book_id={
{book_id}},要指明methods=['GET'],后端使用request.args.get('book_id')
def delete_book(bid):
#精确查询需要删除的书名id
bk = Book.query.filter_by(id=bid).first()
db.session.delete(bk)
#直接重定向到index视图函数
return redirect(url_for('index'))
if __name__ == '__main__':
db.drop_all()
db.create_all()
#生成数据
au_xi = Author(name='我吃西红柿',email='[email protected]')
au_qian = Author(name='萧潜',email='[email protected]')
au_san = Author(name='唐家三少',email='[email protected]')
bk_xi = Book(info='吞噬星空',lead='罗峰')
bk_xi2 = Book(info='寸芒',lead='李杨')
bk_qian = Book(info='飘渺之旅',lead='李强')
bk_san = Book(info='冰火魔厨',lead='融念冰')
#把数据提交给用户会话
db.session.add_all([au_xi,au_qian,au_san,bk_xi,bk_xi2,bk_qian,bk_san])
#提交会话
db.session.commit()
app.run(debug=True)
展示界面
<h1>玄幻系列h1>
<form method="post">
{
{ form.csrf_token }}
<p>作者:{
{ form.au_info }}p>
<p>书名:{
{ form.bk_info }}p>
<p>{
{ form.submit }}p>
form>
<ul>
<li>{% for x in author %}li>
<li>{
{ x }}li><a href='/delete_author{
{ x.id }}'>删除a>
<li>{% endfor %}li>
ul>
<hr>
<ul>
<li>{% for x in book %}li>
<li>{
{ x }}li><a href='/delete_book{
{ x.id }}'>删除a>
<li>{% endfor %}li>
ul>
除了上面的方式,页面时上面添加下面展示,可以使用ajax异步请求
<a href="javascript:;" book_id="{
{book.id}}">a>
<script type="text/javascript" src="js/jquery-1.12.4.min.js">script>
<script type="text/javascript">
$("a").click(function(){
var data = {
book_id : $(this).attr('book_id')
};
var del_id = JSON.stringify(data) // 转化为json数据格式
$.ajax({
url : '/delete_book',
type : 'post',
data : del_id,
contentType : 'application/json',
dataType : 'json',
success : function(data){
if(data.code == 0){
// 返回code
alert('OK');
location.href = '/'
}
}
})
})
script>
相应的视图函数要改变:使用request.get_json()
获取参数
@app.route('/delete_book', methods=['POST']) # ajax的POST请求
def delete_book():
data = request.get_json() # 获取前端请求参数
bk_id = data.get('book_id')
# 精确查询需要删除的书名id
bk = Book.query.filter_by(id=bk_id).first()
db.session.delete(bk)
#直接重定向到index视图函数
return redirect(url_for('index'))
在开发过程中,需要修改数据库模型,而且还要在修改之后更新数据库
最直接的方式就是删除旧表,但这样会丢失数据
更好的解决办法是使用数据库迁移框架,类似Django中的migration,它可以追踪数据库模式的变化,然后把变动应用到数据库中
在Flask中可以使用Flask-Migrate
扩展,来实现数据迁移(来了来了扩展它又来了)
Flask-Migrate提供了一个MigrateCommand类,可以附加到flask-script的manager对象上管理
# 安装
pip install flask-migrate
模型类:database.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate,MigrateCommand
from flask_script import Shell,Manager
app = Flask(__name__)
manager = Manager(app)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:[email protected]:3306/Flask_test'
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db = SQLAlchemy(app)
# 第一个参数是Flask的实例,第二个参数是Sqlalchemy数据库实例
migrate = Migrate(app,db) # 会自动将Migrate对象塞到app中维护,不接收也可以
# manager是Flask-Script的实例
# 这条语句在flask-Script中添加一个名为db命令
manager.add_command('db',MigrateCommand)
#定义模型Role
class Role(db.Model):
# 定义表名
__tablename__ = 'roles'
# 定义列对象
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
def __repr__(self):
return 'Role:'.format(self.name)
#定义用户
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
def __repr__(self):
return 'User:'.format(self.username)
if __name__ == '__main__':
manager.run()
创建迁移仓库,下面这个命令会创建migrations文件夹,所有迁移文件都放在里面
python database.py db init # db 是自定义命令
创建自动迁移脚本,类似Django中的makemigrations
,更新数据库
python database.py db migrate -m 'initial migration' # -m是说明信息
python database.py db upgrade
查询日志,回退版本
python database.py db history # 可以查出版本号
python database.py db downgrade 版本号
Flask-Mail
通过包装了Python内置的smtplib包,可以用在Flask程序中发送邮件from flask import Flask
from flask_mail import Mail, Message
app = Flask(__name__)
#配置邮件:服务器/端口/传输层安全协议/邮箱名/密码
app.config.update(
DEBUG = True,
MAIL_SERVER='smtp.qq.com',
MAIL_PROT=465,
MAIL_USE_TLS = True,
MAIL_USERNAME = '[email protected]',
MAIL_PASSWORD = 'xxxxxxxxx', # QQ密码
)
mail = Mail(app)
@app.route('/')
def index():
# sender 发送方,recipients 接收方列表
msg = Message("This is a test message ",sender='[email protected]', recipients=['[email protected]', '[email protected]'])
#邮件内容
msg.body = "Flask test mail"
#发送邮件
mail.send(msg)
print "Mail sent"
return "Sent Succeed"
if __name__ == "__main__":
app.run()
创建蓝图对象
# Blueprint必须指定两个参数,admin表示蓝图的名称,__name__表示蓝图所在模块
from flask import Blueprint
admin = Blueprint('admin', __name__) # __name__方便蓝图定位文件
可以定义具体的视图函数了
@admin.route('/')
def index():
return 'admin_index'
Flask的实例化应用中(app)注册该蓝图,使用其视图函数
app.register_blueprint(admin, url_prefix='/admin') # 一般在另一个文件,先import admin
# 添加了前缀 ,那么路径就变成:/admin/index,而不是/index
# 懂了没有?
效果:
案例
from flask import Blueprint,render_template
#创建蓝图
logins = Blueprint('login',__name__)
@logins.route('/')
def login():
return render_template('login.html')
from flask import Blueprint,render_template
#创建蓝图,第一个参数指定了蓝图的名字。
users = Blueprint('user',__name__)
@users.route('/')
def user():
return render_template('user.html')
from flask import Flask
# 导入蓝图对象
from login import logins
from user import users
app = Flask(__name__)
app.register_blueprint(logins,url_prefix='/login')
app.register_blueprint(users,url_prefix='/user')
@app.route('/')
def hello_world():
return 'Hello World!'
if __name__ == '__main__':
print(app.url_map) # 打印出全局url信息
app.run(debug=True)
之前说过蓝图的效果类似于Django中的应用,我们把每个蓝图组单独建立一个文件夹,搞成包
搞成包需要添加__init__.py
文件,这个文件会让包在被导入时执行,就在这里定义蓝图吧
from flask import Blueprint
app_cart = Blueprint("app_cart", __name__)
from .views import get_cart # 让蓝图知道有这个视图,不然主目录无法使用
在蓝图组的包中建立views.py文件定义视图
from . import app_cart # . 代表本文件所在包
@app_cart.route('/get_cart')
def get_cart():
return "cart"
使用方式
def fibo(x):
if x == 0:
resp = 0
elif x == 1:
resp = 1
else:
return fibo(x-1) + fibo(x-2)
return resp
assert fibo(5) == 5
但不能到处断言测试一个个函数吧,可以启动服务器,用postman模拟请求,也可以使用爬虫模块发出请求(万能方式)
最常用的,是python中的单元测试类,封装了类似postman的功能,有测试客户端
想怎么测试,就在类里面定义函数,函数名必须以 test_
为前缀
import unittest
class TestClass(unittest.TestCase): # 类名随便起
# 该方法会首先执行,方法名为固定写法
def setUp(self):
# 一般把实例化测试客户端放在这
self.client = app.test_client()
# 该方法会在测试代码执行完后执行,方法名为固定写法
def tearDown(self):
pass
断言常用情景:
assertEqual # 如果两个值相等,则pass
assertNotEqual # 如果两个值不相等,则pass
assertTrue # 判断bool值为True,则pass
assertFalse # 判断bool值为False,则pass
assertIsNone # 不存在,则pass
assertIsNotNone # 存在,则pass
前面的登录测试都是自定义json返回信息,测试时loads出返回数据(字典)
如果有未json格式化的错误呢?将无法定位;因此需要打开测试模式,方便定位其他bug
测试之前的图书案例数据库:看数据能否成功插入
import unittest
from author_book import *
#自定义测试类,setUp方法和tearDown方法会分别在测试前后执行。以test_开头的函数就是具体的测试代码
class DatabaseTest(unittest.TestCase):
def setUp(self):
# 打开测试模式
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@localhost/test1'
self.app = app
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
#测试代码
def test_append_data(self):
au = Author(name='itcast')
bk = Book(info='python')
db.session.add_all([au,bk])
db.session.commit()
author = Author.query.filter_by(name='itcast').first()
book = Book.query.filter_by(info='python').first()
# 断言数据存在
self.assertIsNotNone(author)
self.assertIsNotNone(book)
运行测试的py文件,看是否OK
当我们执行程序时,使用flask自带的服务器,在生产环境中,自带的服务器无法满足性能要求
这里采用Gunicorn
做WSGI容器,来部署flask程序
wsgi
是什么吗?它是为Python语言定义的Web服务器和Web应用程序(框架)之间的一种简单而通用的接口(协议)我的部署方式: nginx + Gunicorn + flask
安装Gunicorn
pip install gunicorn
$ gunicorn -h
测试程序
# main.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'hello world Flask Gunicorn
'
if __name__ == '__main__':
app.run(debug=True)
运行
#-*- coding: UTF-8 -*-
$ gunicorn -w 4 -b 192.168.43.129:5000 -D --access-logfile ./logs/log.txt main:app
我们可以使用一台服务器开启多个端口,模拟多态服务器的场景
$ gunicorn -w 4 -b 192.168.43.129:5001 -D --access-logfile ./logs/log1.txt main:app
无论是Gunicorn还是nginx都是运行在服务器上的软件而已
安装,使用命令安装的nginx相关文件如下:(ubuntu系统)
$ sudo apt-get install nginx
# 回顾Linux命令
$ find /usr -name 'nginx'
$ which nginx
/etc/nginx
下,并且每个虚拟主机安排在 /etc/nginx/sites-available下/usr/sbin/nginx
/var/log/nginx
中/etc/init.d/
目录下创建了启动脚本nginx/var/www/nginx-default
(有的版本 默认的虚拟主机的目录设置在了/var/www/html
, 请参考/etc/nginx/sites-available里的配置)启动
#启动
sudo /etc/init.d/nginx start
#查看
ps aux | grep nginx
# 重启
sudo /etc/init.d/nginx reload
配置负载均衡
# 先将之前的配置备份
sudo cp nginx.conf nginx.conf.django
# 在http{}中添加:
# 这里就是轮流转发请求的IP和端口
upstream flask{
# 这个是内网IP,因为在虚拟机里,在本地测试用的,上线肯定不行
server 192.168.43.129:5000;
server 192.168.43.129:5001;
}
server {
# 监听80端口
listen 80;
# 本机
server_name localhost;
# 默认请求的url
location / {
#请求转发到gunicorn服务器
proxy_pass http://flask;
#设置请求头,并将头信息传递给服务器端
proxy_set_header Host $host;
# 给后端服务器请求的具体来源
proxy_set_header X-Real-IP $remote_addr;
}
}
http://localhost/index
访问,什么鬼?nginx默认页面必须用127.0.0.1浏览器访问nginx,默认发送到nginx的80端口,nginx只需监听本地的80端口即可
所有请求都将发送到nginx服务器,进行转发
访问不同视图函数,通过查看日志可以发现请求是轮流转发(轮询算法)到不同业务服务器端口的
以上即使用一台服务器完成nginx——Gunicorn的请求均衡配置