Flask框架

一、 简介

Flask是一个非常小的PythonWeb框架,被称为微型框架;只提供了一个稳健的核心,其他功能全部是通过扩展实现的;意思就是我们可以根据项目的需要量身定制,也意味着我们需要学习各种扩展库的使用。

二、 概要

1)安装: pip install flask
2)组成:WSGI系统、调试、路由
3)模板引擎:Jinja2(由Flask核心开发者人员开发)
4)使用到装饰器:以@开头的代码方法

三、 知识点(附代码)

1. Flask基础入门

1)路由route的创建:

route装饰器详解

  • 通过创建路由并关联函数,实现一个基本的网页:
from flask import Flask

# 用当前脚本名称实例化Flask对象,方便flask从该脚本文件中获取需要的内容
app = Flask(__name__)

#程序实例需要知道每个url请求所对应的运行代码是谁。
#所以程序中必须要创建一个url请求地址到python运行函数的一个映射。
#处理url和视图函数之间的关系的程序就是"路由",在Flask中,路由是通过@app.route装饰器(以@开头)来表示的
@app.route("/")
#url映射的函数,要传参则在上述route(路由)中添加参数申明
def index():
    return "Hello World!"

# 直属的第一个作为视图函数被绑定,第二个就是普通函数
# 路由与视图函数需要一一对应
# def not():
#     return "Not Hello World!"

# 启动一个本地开发服务器,激活该网页
app.run()
  • 通过路由的methods指定url允许的请求格式:
from flask import Flask

app = Flask(__name__)

#methods参数用于指定允许的请求格式
#常规输入url的访问就是get方法
@app.route("/hello",methods=['GET','POST'])
def hello():
    return "Hello World!"
#注意路由路径不要重名,映射的视图函数也不要重名
@app.route("/hi",methods=['POST'])
def hi():
    return "Hi World!"

app.run()
  • 通过路由在url内添加参数,其关联的函数可以接收该参数:
from flask import Flask

app = Flask(__name__)

# 可以在路径内以/<参数名>的形式指定参数,默认接收到的参数类型是string

'''#######################
以下为框架自带的转换器,可以置于参数前将接收的参数转化为对应类型
string 接受任何不包含斜杠的文本
int 接受正整数
float 接受正浮点数
path 接受包含斜杠的文本
########################'''

@app.route("/index/<int:id>",)
def index(id):
    if id == 1:
        return 'first'
    elif id == 2:
        return 'second'
    elif id == 3:
        return 'thrid'
    else:
        return 'hello world!'

if __name__=='__main__':
    app.run()
  • 除了原有的转换器,我们也可以自定义转换器(pip install werkzeug):
from werkzeug.routing import BaseConverter #导入转换器的基类,用于继承方法
from flask import Flask

app = Flask(__name__)

# 自定义转换器类
class RegexConverter(BaseConverter):
    def __init__(self,url_map,regex):
        # 重写父类定义方法
        super(RegexConverter,self).__init__(url_map)
        self.regex = regex

    def to_python(self, value):
        # 重写父类方法,后续功能已经实现好了
        print('to_python方法被调用')
        return value

# 将自定义的转换器类添加到flask应用中
# 具体过程是添加到Flask类下url_map属性(一个Map类的实例)包含的转换器字典属性中
app.url_map.converters['re'] = RegexConverter
# 此处re后括号内的匹配语句,被自动传给我们定义的转换器中的regex属性
# value值会与该语句匹配,匹配成功则传达给url映射的视图函数
@app.route("/index/<re('1\d{10}'):value>")
def index(value):
    print(value)
    return "Hello World!"

if __name__=='__main__':
    app.run(debug=True)
2)endpoint的作用
  • 说明:每个app中都存在一个url_map,这个url_map中包含了url到endpoint的映射;
  • 作用:当request请求传来一个url的时候,会在url_map中先通过rule找到endpoint,然后再在view_functions中根据endpoint再找到对应的视图函数view_func
from flask import Flask

app = Flask(__name__)

# endpoint默认为视图函数的名称
@app.route('/test')
def test():
    return 'test success!'
# 我们也可以在路由中修改endpoint(当视图函数名称很长时适用)
# 相当于为视图函数起别名
@app.route('/hello',endpoint='our_set')
def hello_world():
    return 'Hello World!'

if __name__ == '__main__':
    print(app.view_functions)
    print(app.url_map)
    app.run()
  • 可以通过view_functions查看到当前endpoint视图函数的对应情况;
  • 可以通过url_map查看当前urlendpoint的绑定情况;
# view_functions
{'static': <function Flask.__init__.<locals>.<lambda> at 0x00000230CC2A7DC0>, 'test': <function test at 0x00000230CC30FD30>, 'our_set': <function hello_world at 0x00000230CC30FDC0>}
# url_map
Map([<Rule '/hello' (OPTIONS, HEAD, GET) -> our_set>,
 <Rule '/test' (OPTIONS, HEAD, GET) -> test>,
 <Rule '/static/<filename>' (OPTIONS, HEAD, GET) -> static>])
 * Serving Flask app 'endpoint_test' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
  • 值得注意的是,endpoint相当于给url起一个名字,view_functions内存储的就是url的名字到视图函数的映射,且endpoint在同一个蓝图下也不能重名:
from flask import Flask

app = Flask(__name__)

@app.route('/test',endpoint='Test')
def test():
    return 'None'
def Test():
    return 'World!'
    
if __name__ == '__main__':
    print(app.view_functions)
    print(app.url_map)
    app.run()
  • 通过view_functions可以看到,即使修改endpoint为其他视图函数名,依然是绑定其正下方的视图函数,说明endpoint作用于url:
{'static': <function Flask.__init__.<locals>.<lambda> at 0x000002056C378CA0>, 'Test': <function test at 0x000002056C3E0C10>}
3)request对象的使用
  • 什么是request对象
  • render_template():可以用于呈现一个我们编写的html文件模板
  • request.method用于获取url接收到的请求方式,以此返回不同的响应页面
#request:包含前端发送过来的所有请求数据

from flask import Flask,render_template,request

# 用当前脚本名称实例化Flask对象,方便flask从该脚本文件中获取需要的内容
app = Flask(__name__)

@app.route("/",methods=['GET','POST'])
#url映射的函数,要传参则在上述route(路由)中添加参数申明
def index():
    if request.method == 'GET':
        # 想要html文件被该函数访问到,首先要创建一个templates文件,将html文件放入其中
        # 该文件夹需要被标记为模板文件夹,且模板语言设置为jinja2
        return render_template('index.html')
    # 此处欲发送post请求,需要在对应html文件的form表单中设置method为post
    elif request.method == 'POST':
        name = request.form.get('name')
        password = request.form.get('password')
        return name+" "+password

if __name__=='__main__':
    app.run()
4)请求钩子before/after_request

想要在正常执行的代码的前、中、后时期,强行执行一段我们想要执行的功能代码,便要用到钩子函数——用特定装饰器装饰的函数


下面将介绍Flask内,四种常用的钩子:

  1. before_request:在每一次请求之前调用;
  • 该钩子函数表示每一次请求之前,可以执行某个特定功能的函数;
  • 执行顺序是先绑定的先执行
  • 并且先执行 flask app 的 before_request, 再执行 blueprint 的 before_request;
  • 一般用于检验用户权限、请求是否合法等场景;
from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello World!'

@app.before_request
def before_request_a():
    print('I am in before_request_a')
 
@app.before_request
def before_request_b():
    print('I am in before_request_b')
 
 
if __name__ == '__main__':
    app.run()
 
# 打印结果 -=-=-=-=-=-=-=-=-=-=-=-=-=
I am in teardown_request_a
I am in teardown_request_b
  1. before_first_request:与before_request的区别是,只在第一次请求之前调用;
  • 该钩子函数表示第一次请求之前可以执行的函数;
  • 执行顺序同样也是先绑定的先执行
# 代码替换视图函数hello_world后,if main前
@app.before_first_request
def teardown_request_a():
    print('I am in teardown_request_a')

@app.before_first_request
def teardown_request_b():
    print('I am in teardown_request_b')

# 打印结果 -=-=-=-=-=-=-=-=-=-=-=-=-=
I am in teardown_request_a
I am in teardown_request_b
  1. after_request每一次请求之后都会调用;
  • 该钩子函数表示每一次请求之后,可以执行某个特定功能的函数,这个函数接收response对象,所以执行完后必须归还response对象
  • 执行的顺序是先绑定的后执行
  • 被触发的前提是没有异常抛出,或者异常被 errorhandler捕获并处理;
  • 一般可以用于产生csrf_token验证码等场景;
# 代码替换视图函数hello_world后,if main前
@app.after_request
def after_request_a(response):
    print('I am in after_request_a')
    # 该装饰器接收response参数,运行完必须归还response,不然程序报错
    return response
 
@app.after_request
def after_request_b(response):
    print('I am in after_request_b')
    return response
    
# 打印结果 -=-=-=-=-=-=-=-=-=-=-=-=-=
I am in teardown_request_b
I am in teardown_request_a
  1. teardown_request每一次请求之后都会调用;
  • 该钩子函数接收一个参数,该参数是服务器出现的错误信息;
  • 执行顺序也是先绑定的后执行
  • 只有在请求上下文被 pop 出请求栈的时候才会直接跳转到teardown_request;
  • 所以在被正常调用之前,即使某次请求有抛出错误,该请求也都会被继续执行, 并在执行完后返回 response;
# 代码替换视图函数hello_world后,if main前
@app.teardown_request
def teardown_request_a(exc):
    print('I am in teardown_request_a')
 
@app.teardown_request
def teardown_request_b(exc):
    print('I am in teardown_request_b')
 
# 打印结果 -=-=-=-=-=-=-=-=-=-=-=-=-=
I am in teardown_request_b
I am in teardown_request_a
5)redirect重定向
  • 什么是redirect重定向
  • 在flask 中,重定向是通过flask.redirect(location, code=302)这个函数来实现的,location表示需要重定向的url, 应该配合url_for函数来使用, code表示采用哪个重定向,默认是302,即临时性重定向, 可以修改为301来实现永性重定向;
from flask import Flask,redirect,url_for

app = Flask(__name__)

@app.route('/index')
def index():
    # redirect重定位(服务器向外部发起一个请求跳转)到一个url界面;
    # url_for给指定的函数构造 URL;
    # return redirect('/hello') 不建议这样做,将界面限死了
    return redirect(url_for('hello'))

@app.route('/hello')
def hello():
    return 'this is hello fun'

if __name__ == '__main__':
    app.run()
6)返回json数据给前端
  • 方法一:

使用:make_response方法和json库共同完成

from flask import Flask,make_response,json

app = Flask(__name__)

@app.route("/index")
def index():
   data = {
       'name':'张三'
   }
   # json.dumps 将一个python数据结构转化为json
   # json.dumps 序列化时对中文默认使用的ascii编码.想输出真正的中文需要指定ensure_ascii=False
   # 生成一个response响应对象,而不是直接return来返回响应对象,便于执行更多的后续操作
   response = make_response(json.dumps(data,ensure_ascii=False))
   # 修改数据的MIME标准类型为json(在发送数据前会先发送该类型)
   response.mimetype = 'application/json'
   return response

if __name__=='__main__':
    app.run()
  • 方法二:

使用:jsonify库实现,减少代码行数

from flask import Flask,jsonify

app = Flask(__name__)
# 在Flask的config是一个存储了各项配置的字典
# 该操作是进行等效于ensure_ascii=False的配置
app.config['JSON_AS_ASCII'] = False

@app.route("/index")
def index():
   data = {
       'name':'张三'
   }
   return jsonify(data)

if __name__=='__main__':
    app.run()
  • 运行结果:

对象类型如图

7)abort函数的使用
  • 什么是abort()
  • 使用类似于python中的raise函数,可以在需要退出请求的地方抛出错误,并结束该请求;
  • 我们可以使用errorhandler()装饰器来进行异常的捕获与自定义:
from flask import Flask,render_template,request,abort

app = Flask(__name__)

@app.route("/",methods=['GET','POST'])
def index():
    if request.method == 'GET':
        return render_template('index.html')
    elif request.method == 'POST':
        name = request.form.get('name')
        password = request.form.get('password')
        if name == 'zhangsan' and password == '123456':
            return 'login sucess'
        else:
            # abort的用法类似于python中的raise,在网页中主动抛出错误
            abort(404)
            return None

# 自定义错误处理方法,将404这个error与Python函数绑定
# 当需要抛出404error时,将会访问下面的代码
@app.errorhandler(404)
def handle_404_error(err):
    # return "发生了错误,错误情况是:%s"%err
    # 自定义一个界面
    return render_template('404.html')

if __name__ == '__main__':
    app.run()
  • 自定义的404界面:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!-- 注意图片文件需要放在一个静态文件夹static里 -->
<img src="../static/error404.jpg" alt="" width="1428px" height="57px">
</body>
</html>
  • errorhandler()也可以传入异常类,用于捕获除flask异常列表内的其他异常:点我查看
8)url_for实现反转
  • url_for是实现url反转的工具,即视图函数 → \rightarrow url
  • 静态文件引入:url_for('static', filename='文件路径')

静态文件需要存储在当前工程下的static目录内。

  • 定义路由:url_for('模块名.视图名',变量=参数)
  • 参数一中的视图名实质指的是endpoint,url_map中存储了url到endpoint的映射,只是默认情况下endpoint与视图函数名相同;
  • 如果在装饰器中修改了endpoint,则url_for只能使用endpoint设置的名字来反转url;
  • 在单模块程序下我们可以省略模块名,但当使用了蓝图(buleprint)后,参数一就必须使用"蓝图模块名.视图名",因为不同蓝图下的视图函数可以重名。

2. Flask高级视图

1)add_url_rule的初登场
  • 欲实现url与视图函数的绑定,除了使用路由装饰器@app.route,我们还可以通过add_url_rule(rule,endpoint=None,view_func=None)方法,其中:
  • rule:设置的url
  • endpoint:给url设置的名称
  • view_func:指定视图函数的名称
  • 因此,我们可以这样用:
def my_test():
    return '这是测试页面'
app.add_url_rule(rule='/test',endpoint='test',view_func=my_test)
  • 验证一下上述用法:
from flask import Flask,url_for

app = Flask(__name__)

@app.route('/',endpoint='index')
# 底层其实是使用add_url_rule实现的
def hello_world():
    return 'Hello World!'

def my_test():
    return '这是测试页面'
app.add_url_rule(rule='/test',endpoint='test',view_func=my_test)

# 请求上下文只有在发送request请求时才会被激活,激活后request对象被设置为全局可访问
# 其内部封装了客户端发出的请求数据报文
# 此处是主动生成一个临时的测试请求上下文
with app.test_request_context():
    print(url_for('test')) # 输出结果为/test

if __name__ == '__main__':
    app.run(debug=True)
2)类视图的引入
  • 之前我们所定义的视图都是通过函数来实现的,所以称之为视图函数,但其实视图还可以由类来实现,即类视图
  • 标准类视图
  • 定义时需要继承flask的views.View这一基类;
  • 每个类视图内必须包含一个dispatch_request方法,每当类视图接收到请求时都会执行该方法,返回值的设定和视图函数相同;
  • 视图函数可以通过@app.route和app.add_url_rule来进行注册(映射到url),但类视图只能通过app.add_url_rule来注册,注册时view_func不能直接使用类名,需要调用基类中的as_view方法来为自己取一个“视图函数名
  • 采用类视图的最大优势,就是可以把多个视图内相同的东西放在父类中,然后子类去继承父类;而类视图不方便的地方,就是每一个子类都要通过一个add_url_rule来进行注册。
  • 下面将创建一个网站包含三个页面,每个页面中都展示相同的对联广告,py文件如下:
from flask import Flask,render_template,views

app = Flask(__name__)

# 定义父视图类继承基类View
class Ads(views.View):
    def __init__(self):
        super(Ads, self).__init__()
        # 实例属性
        self.context={
            'ads':'这是对联广告!'
        }

# 定义子视图类继承父类并实现工程
class Index(Ads):
    def dispatch_request(self):
        # 字典传参方式==不定长的关键字传参
        return render_template('class_mould/index.html',**self.context)
class Login(Ads):
    def dispatch_request(self):
        # 字典传参方式==不定长的关键字传参
        return render_template('class_mould/login.html',**self.context)
class Register(Ads):
    def dispatch_request(self):
        # 字典传参方式==不定长的关键字传参
        return render_template('class_mould/register.html',**self.context)

# 注册我们创建的类视图,as_view给类视图起名
app.add_url_rule(rule='/',endpoint='index',view_func=Index.as_view('index'))
app.add_url_rule(rule='/login/',endpoint='login',view_func=Login.as_view('login'))
app.add_url_rule(rule='/register/',endpoint='register',view_func=Register.as_view('register'))

if __name__=='__main__':
    print(app.view_functions)
    app.run(debug=True)
  • 首页index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
这是首页!{{ ads }}
</body>
</html>
  • 登录页面login.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
这是登录页面!{{ ads }}
</body>
</html>
  • 注册页面register.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
这是注册页面!{{ ads }}
</body>
</html>
  • 可以通过调用app.view_functions来查看当前的endpoint绑定情况,发现已经变为as_view转化后的类视图:
{'static': <function Flask.__init__.<locals>.<lambda> at 0x0000024163C46D30>, 'index': <function View.as_view.<locals>.view at 0x0000024164B58CA0>, 'login': <function View.as_view.<locals>.view at 0x0000024164BCB280>, 'register': <function View.as_view.<locals>.view at 0x0000024164BCB310>}
  • 基于方法的类视图
  • 当我们需要根据不同请求来实现不同逻辑时,用视图函数需要在内部对请求方法做判断,但我们使用方法类视图就可以通过重写其内部方法简单实现;
  • Flask除了基本类视图,还为我们提供了另一种类视图flask.views.MethodView,在其内部编写的函数方法即是http方法的同名小写映射
from flask import Flask,render_template,request,views

app = Flask(__name__)

@app.route('/')
def hello_world():
    return render_template('index.html')

# 定义LoginView类
class LoginView(views.MethodView):
    # 定义get函数
    def get(self):
        return render_template("index.html")
    # 定义post函数
    def post(self):
        username = request.form.get("username")
        password = request.form.get("password")
        if username == 'admin' and password == 'admin':
            return "用户名正确,可以登录!"
        else:
            return "用户名或密码错误,不可以登录!"

# 注册类视图
# 未设置endpoint,则endpoint默认为as_view设置的类视图名
app.add_url_rule('/login',view_func=LoginView.as_view('loginview'))

if __name__ == '__main__':
    print(app.url_map)
    app.run(debug=True)
  • 模板文件index.html为:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--action中可以指定表单提交的目标url或文件-->
<!--login指向我们给类视图绑定的url:'/login'-->
<form action="login" method="post">
    USERNAME:
    <input type="text" name="username">
    <br>
    PASSWORD:
    <input type="password" name="password">
    <br>
    <!--提交按钮-->
    <input type="submit" name="submit">
</form>
</body>
</html>
3)装饰器的自定义与使用
  • 装饰器本质上是一个python函数,他可以让其他函数在不需要做任何代码变得的前提下增加额外的功能,其传入参数一般是函数对象(如视图函数),返回值也是一个函数对象;
  • 装饰器主要用于有切面需求的场景,如插入日志、性能测试、事务处理等与函数功能无关的操作,对于这些需要多次重用的代码,我们将其放置在装饰器里,就可以无需在每个函数中反复编写;
  • 如我们要在新闻页面前插入登录操作,我们可以这样实现:
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello World!'

# 定义装饰器函数
def user_login(func):
    def inner():
    	# 替代登录操作
        print('登录操作!')
        # 执行传入的函数对象
        func()
    # 此处如果return inner(),那么返回的是inner函数的执行结果
    # 而使用return inner,则返回的是inner函数
    return inner

# 定义新闻页面视图函数news
def news():
    print('这是新闻详情页!')
# 将news函数作为参数传给装饰器函数
show_news=user_login(news)
# 因为user_login返回inner函数,所以show_news()==inner()
show_news()
# 打印出show_news的真实函数名(为inner)
print(show_news.__name__)

if __name__ == '__main__':
    app.run(debug=True)
  • 上述代码的运行逻辑是这样的:首先我们将新闻页面函数作为一个参数传给装饰器,装饰器将我们需要插入的登录操作与我们的视图函数包装成一个inner函数对象并返回,最后执行该对象便可以实现在新闻页面显示前执行登录操作;
  • 其中登录操作并不是新闻页面函数的功能,且访问每一个新闻页面都应当先执行该操作,固我们将其放置在定义的装饰器中,需要添加该功能的函数使用该装饰器即可;运行结果如下:
登录操作!
这是新闻详情页!
inner
  • 当然上述的写法和我们平时调用装饰器的方法不太一样,我们将其变为标准的装饰器形式:
@user_login
# 定义函数news,该函数将自动被传给装饰器做参数
def news():
    print('这是新闻详情页!')
# 此时相当于已经执行完news=user_login(news)
news()
print(news.__name__)

# show_news=user_login(news)
# show_news()
# print(show_news.__name__)
  • 用标准格式替换后得到运行结果相同,news的函数名也已经变为inner:
登录操作!
这是新闻详情页!
inner
  • 刚才我们展示的是不含参数的函数使用装饰器,对于带参数的函数我们同样也可以使用装饰器,这里要先回顾Python的可变参数:

def func(*args,**kwargs) :

  • *:代表元组,长度不限;
  • **:代表键值对,个数不限;
  • *args:指用元组传参,元组内包含不定个数的位置参数;
  • **kwargs:指用字典传参,字典内包含不定个数的关键字参数(键值对);
  • 对于函数传参的演示略过,直接展示带参数的函数如何使用装饰器:
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello World!'

# 定义装饰器函数
def user_login(func):
    # inner函数接收参数
    def inner(*args,**kwargs):
        print('登录操作!')
        # 执行传入函数时使用inner接收到的参数
        func(*args,**kwargs)
    return inner

# 不带参的不受影响
@user_login
def news():
    print(news.__name__)
    print('这是新闻详情页!')
news()

# 带参的定义时预声明接收的参数
@user_login
def news_list(*args):
    # 获取元组args的第一个元素
    page=args[0]
    print(news_list.__name__)
    print('这是新闻列表页的第'+str(page)+'页!')
# 传递给args的元组即为(5,)
news_list(5)

if __name__ == '__main__':
    app.run(debug=True)
  • 运行后可以看到上述的两个视图函数名都已经变为inner,说明打包成功:
登录操作!
inner
这是新闻详情页!

登录操作!
inner
这是新闻列表页的第5页!
  • 上述结果也反映出代码存在一定的问题,就是不管我们定义的视图函数名称是news还是news_list,最终执行时都变为了inner。为了解决这一问题,我们可以使用functools.wraps方法来保留原函数的属性与名称,通俗一点理解就是“不换外包装”;
  • 方法的导入:from functools import wraps
  • 在自定义的装饰器下方添加一行@wraps(<形参名>)即可;
from functools import wraps

# 定义装饰器函数
def user_login(func):
    @wraps(func)
    # inner函数接收参数
    def inner(*args,**kwargs):
        print('登录操作!')
        # 执行传入函数时使用inner接收到的参数
        func(*args,**kwargs)
    return inner
  • 替换修改后的装饰器,运行结果如下,可以看到原视图函数名称被保留:
登录操作!
news
这是新闻详情页!

登录操作!
news_list
这是新闻列表页的第5页!
4)蓝图的使用
  • 上述类视图、装饰器分别通过继承、包装的方式减少了单个flask程序文件里重复代码的出现,实现了程序的优化;
  • 但是这样处理后的文件内,不同功能的代码块(类视图、视图函数)仍然混杂在一起。如果要制作一个非常大型的程序项目,这样不仅会让代码阅读变得十分困难,而且不利于后期维护;
  • 为了解决这一问题,我们需要引入蓝图(flask.Blueprint),用于实现程序功能的模块化;
  • 导入方法:from flask import Blueprint
  • 当接收到请求时,Flask会遍历Flask对象下(已注册)的各蓝图对象,比对蓝图对象中记录的url,比对成功则映射到该url绑定的视图函数并返回响应
  • 主路由视图函数:创建flask对象,并为拓展模块中的蓝图对象提供注册入口
from flask import Flask
from flask学习 import news,products

app = Flask(__name__)
@app.route('/')
def hello_world():
    return 'hello my world !'

# 将对应模块下的蓝图对象注册到app中
app.register_blueprint(news.new_list)
app.register_blueprint(products.product_list)

if __name__ == '__main__':
    app.run(debug=True)
  • Blueprint对象的工作方式与Flask对象类似,但其不是一个单独的应用;
  • 每拓展一个蓝图对象,就要在主路由文件下添加一行注册代码;
  • 蓝图对象内记录了当前模块下的所有视图函数,固视图函数不可与蓝图对象同名;
  • 在蓝图内需要通过蓝图对象来定义路由和调用其他装饰器,由蓝图对象定义的路由处于休眠状态,在蓝图被注册时才成为程序的一部分。
  • 分路由视图函数:创建蓝图对象,实现功能拓展
  • 模块一:news.py
from flask import Blueprint

# 实例化蓝图对象,参数一类似于蓝图对象的名称
# 一个app下的蓝图对象不可重名
new_list = Blueprint('news',__name__)

# 蓝图对象的使用和app类似
# 一个蓝图下的视图函数名、endpoint不可重复
@new_list.route('/news')
def new():
    return '这是新闻模块!'
  • 模块二:products.py
from flask import Blueprint

# 实例化蓝图对象,参数一类似于蓝图对象的名称
# 一个app下的蓝图对象不可重名
new_list = Blueprint('products',__name__)

# 蓝图对象的使用和app类似
# 一个蓝图下的视图函数名、endpoint不可重复
@new_list.route('/products')
def product():
    return '这是产品模块!'
  • 如此一来,我们便将不同功能的视图函数定义在了不同的模块下,实现了程序的工程化。
5)url_prefix设置蓝图前缀
  • 一般在蓝图对象定义时添加,为当前蓝图下的所有视图函数添加统一的前缀,这样不同蓝图下的视图函数的url就不易发生重复;
  • 如下例添加前缀后,加载该新闻模块的url就变为"/index/news":
new_list = Blueprint('news',__name__,url_prefix='/index')

@new_list.route('/news')
def new():
    return '这是新闻模块!'
  • 此外,在主路由中注册蓝图时也可以为蓝图添加前缀,并且此次添加会覆写蓝图对象创建时添加的前缀;
  • 如下例中,注册后的新闻模块的url又变为了"/test/news":
app.register_blueprint(news.new_list,url_prefix='/test')
6)subdomain设置蓝图子域名

设置域名

  • 我们可以通过修改Flask对象的配置,来为我们的网站设置域名,使用的配置字段为’SERVER_NAME’,同时还要为该域名指定端口号
# 当前网站域名设置为example.com,端口号为5000
app.config['SERVER_NAME'] = 'example.com:5000'
  • 在设置了app.config['SERVER_NAME']的值后,访问网站应当使用我们设置的域名,同时我们还需要修改位于 C:\Windows\System32\drivers\etc 下的域名重定向文件 hosts
  • 在该文件下添加一条记录:127.0.0.1 example.com
  • 记录的左边是服务器ip地址(本地开发时使用localhost才为127.0.0.1),右边则是我们定义的域名
  • 下面展示一个实例,该实例中在hosts文件下插入的记录为:10.240.142.216 example.com(此时我们指定了服务器host不再为localhost,而是0.0.0.0),左边是本机ipv4地址;
  • 此时访问网站服务器时只能通过域名方式:http://example.com:5000/index,使用http://10.240.142.216:5000/index将返回404:;
from flask import Flask
import admin

app = Flask(__name__)

# 配置`SERVER_NAME`,设置主域名
app.config['SERVER_NAME'] = 'example.com:5000'
# 注册蓝图,指定了subdomain
app.register_blueprint(admin.bp)

@app.route('/index')
def index():
    return '通过域名访问!'

if __name__ == '__main__':
    app.run(host='0.0.0.0',debug=True)

域名访问

设置子域名

  • 此时我们在蓝图文件admin.py中注册蓝图对象,添加subdomain='admin'即为该蓝图设置子域名admin
  • 再在hosts文件中添加记录:10.240.142.216 admin.example.com后,我们便可以通过域名:http://admin.example.com:5000/ad来访问该蓝图(下一级域名在上一级左侧);
from flask import Blueprint
bp = Blueprint('admin',__name__,subdomain = 'admin')

@bp.route('/ad')
def admin():
    return 'Admin Page'

子域名访问

3. jinja2模板引擎

1)模板的导入与使用
  • Flask通过render_template来实现模板的渲染,要使用这个方法,我们需要导入from flask import rander_template,模板中注释需放在{# #}
  • 模板的第一个参数为指定的模板文件名称,如自定义的html文件,第二个(及后续)参数为可选项,用于向模板中传递变量。
from flask import Flask,render_template

app = Flask(__name__)

# 给前端模板传参
@app.route("/")
def index():
    data = {
        'name':'张三',
        'age':18,
        'mylist':[1,2,3,4,5,6,7]
    }
    # 以键值对的形式传参给模板index2.html
    # 左边是我们要在前端调用时使用的变量名称(形参:data);
    # 右边是我们给这个变量传的值(实参:字典data);
    return render_template('index2.html',data=data)

if __name__ == '__main__':
    app.run()
  • 前端html模板内需要在双括号{{ }}中使用该变量:
  • 如果想给该变量添加属性便于CSS修改格式,我们可以在变量后添加括号,并在括号内定义class、id等属性 → \rightarrow 点击查看
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
hello world
<br>
<!-- 对传入变量的使用并显示:在双括号内,和python中用法类似 -->
{{ data }}
<br>
{{ data['name'] }}
<br>
{{ data.name }}
<br>
mylist:{{ data.mylist }}
<br>
mylist[1]:{{ data.mylist[1] }}
<br>
count:{{ data.mylist[1]+data.mylist[2] }}
</body>
</html>
  • 如果有多个变量需要传递,我们可以不需要一个一个进行传参,直接使用**locals()替代我们在当前视图函数中定义的所有变量:
from flask import Flask,render_template

app = Flask(__name__)

# 给前端模板传参
@app.route("/")
def index():
	title='python键值对'	# 定义键值1
	author='li'			# 定义键值2
    return render_template('index2.html',**locals()) #渲染模型并传值

if __name__ == '__main__':
    app.run()
  • 在前段直接使用定义时的变量名就可以使用该变量,即{{ title }}{{ author }}
2)模板中的控制语句
  • jinja2模板引擎中也可使用if和for控制语句,但是语句需要放置在{% %}中;

  • if条件判断语句必须包含结束标签{% endif %},其他部分与python中类似,可以与比较运算符> >= < <= == !=结合使用,或与逻辑运算符and,or,not,()结合使用;

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
{% if name==1 %}
	<h1>恭喜你抽中了一等奖!</h1>
{% if name==2 %}
	<h1>恭喜你抽中了二等奖!</h1>
{% else %}
	<h1>恭喜你抽中了三等奖!</h1>
{% endif %}
</body>
</html>
  • for循环控制语句在模板内的用法也和python中类似,遍历的对象可以是字典、元组、列表等,但需要注意的是在模板中无法使用continue和break来对循环进行控制;
{% for 目标 in 对象 %}
	<p>目标</p>
{% endfor %}
  • for循环的内置常量:
  1. loop.index: 获取当前的索引值 从1开始
  2. loop.index0:获取当前的索引值 从0开始
  3. loop.first: 判断当前是否是第一次迭代, 是返回True否则返回False
  4. loop.last: 判断当前是否是最后一次迭代, 是返回True否则返回False
  5. loop.length: 序列的长度
<ul>
{% for item in list %}
	<li>{{ item }}</li>
	<li>当前的索引是:{{ loop.index }}</li>
	<li>当前的索引是:{{ loop.index0 }}</li>
	<li>当前是否是第一次迭代:{{ loop.first }}</li>
	<li>当前是否是最后一次迭代:{{ loop.last }}</li>
	<li>前序列的长度:{{ loop.length }}</li>
</ul>
3)过滤器的使用与自定义
  • 常用过滤器有哪些?
  • 可以在前端模板内{{ 内容 | 过滤器 }}的" | "后使用;
  • 可以使用add_template_filter(函数方法名,'过滤器名')来自定义过滤器;
# 自定义过滤器
def list_step(li):
    # 返回列表,步长为2
    return li[::2]

# 注册模板过滤器(filter)
# 参数1为该过滤器调用的函数,参数2为在前端中调用该过滤器使用的名称
app.add_template_filter(list_step,'li2')
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!-- 过滤器的使用 -->
<!-- 全大写 -->
{{ 'hello_world' | upper }}
<br>
<!-- 单词首字母大写 -->
{{ 'hello world' | title }}
<br>
<!-- 替换左边的内容为右边的内容 -->
{{ 'hello_world' | replace('hello','hi') }}
<br>
<!-- 调用自定义的过滤器 -->
mylist列表:{{ data.mylist | li2 }}
</body>
</html>
  • 此处我们是将列表作为过滤器的返回对象,也可以把字符串等作为过滤器的返回对象,给前端标签作为class、id等属性,再结合css可以达到一些特别的效果。
<span class="{{ 给过滤器的参数|过滤器名称 }}"></span>
4)宏的定义、调用与导入
  • 宏的定义是为了将前端模板中需要反复创建的模块变成一个方便调用的“函数”,这一操作类似于python中创建函数,也可以传参,但不能有返回值
  • 宏的定义以macro标志开始,以endmacro结束,同样需要在{% %}中进行。
<!-- 不带参数的宏定义, 像定义一个函数一样 -->
{% macro input1() %}
    <!-- 宏内执行的操作,生成一个input表单项 -->
    <label>表单项1:
        <input type="text" name='username' value=''>
        <br>
    </label>
{% endmacro %}

<!-- 带参数的宏定义,在括号添加参数和默认值 -->
{% macro input2(name, value='', type='text', size=30) %}
    <!-- 同样是宏内执行的操作,生成一个input表单项 -->
    <!-- 此处双括号内的参数,指向我们在定义时设定的参数,调用时没有传值就使用设定的默认值 -->
    <label>表单项2:
        <input type="{{ type }}" name="{{ name }}" value="{{ value }}" size="{{ size }}">
        <br>
    </label>
{% endmacro %}
  • 宏的调用同样类似于函数的调用,如果未采用关键字传参则要注意顺序。
<!-- 使用1, 相当于调用一个函数一样 -->
{{ input1() }}

<!-- 使用2 -->
{{ input2() }}   <!--name不指定, 则name="", 即和value一样也是空-->
<!-- 或者 -->
{{ input2('username') }}
<!-- 或者 -->
{{ input2('username', value='cheng', type='password', size=50) }}
  • 宏的导入可以使用语句import 模板文件名 as 别名from 模板文件名 import 宏名称,就像python中库和包的导入一样;
  • 我们将宏单独定义在一个html模板文件中后,就可以通过导入这个模板文件来调用里面的所有宏,导入过程同样在{% %}中进行,调用过程在{{ }}在进行。
<!-- 上述宏我们定义在了一个index3.html的文件中 -->
{% import 'index3.html' as index3 %}
	<div>
		<!-- 调用导入的宏模板文件中的宏,实现登录页面构建 -->
		<p>用户名:{{index3.input2('username')}}</p>
		<p>密码:{{index3.input2('password',type='password')}}</p>
		<p>登录:{{index3.input2('submit',type='submit',value='登录')}}</p>
	</div>
  • 也可以像这样:
<!-- 另一种导入方式 -->
{% from 'index3.html' import input2 %}
	<div>
		<!-- 此时直接调用input2即可 -->
		<p>用户名:{{input2('username')}}</p>
		<p>密码:{{input2('password',type='password')}}</p>
		<p>登录:{{input2('submit',type='submit',value='登录')}}</p>
	</div>
5)include的使用
  • include用于在一个模板的指定位置导入另一个模板的内容,区别于宏的调用,include更像从另一个模板“复制+粘贴”;
  • include同样在{% %}中使用,采用语句{% include 模块名 %},需要注意两点:
  • include是直接将目标模板中的所有内容直接“copy”在当前位置,所以被导入的模板如果有head和body部分也将被导入过来;
  • include和import都是在templates这个目录下搜索的,所以使用路径时不需要添加相对路径:上级目录 “ ../ ” 和当前目录 “ ./ ” ;

(PS:子模板与父模板的说法是为了更好理解)

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<!-- 可以视为父模板 -->
<body>
	<!-- 可以视为子模板1 -->
    {% include "common/header.html" %}
    <div class="content">
        中间的
    </div>
    <!-- 可以视为子模板2 -->
    {% include "common/footer.html" %}
</body>
</html>
  • 子模板——header.html内容如下:
<nav>
	<div class="top">
		这是顶部
	</div>
</nav>
  • 子模板——footer.html内容如下:
<footer>
	<div class="bottom">
		这是底部
		<!-- 说明:子模板中可以直接使用父模板的变量,不需要其他操作
		因为这一代码是被复制到父模板中去运行的 -->
		author:{{ name }}
	</div>
</footer>
  • 后端导入父模板:
from flask import Flask,render_template

app = Flask(__name__)

# 运行时直接将子模板需要的参数传给父模板
@app.route("/")
def index():
	name='时生'
    return render_template('include_test.html',name=name)

if __name__ == '__main__':
    app.run(debug=True)
6)set和with的使用
  • 我们在模板内需要使用到的变量,不仅可以通过后端传参,也可以由我们自己定义,这样更有利于前后端分离的实现;
  • set——自定义全局变量:由set定义的变量可以在模板内任意一个地方调用,甚至在子模板中也可以使用;
<!-- 定义普通变量并赋值 -->
{% set telephone=1234567890 %}
<!-- 定义列表变量并赋值 -->
{% set lis=[('produce.html','produce'),('index.html','index')] %}

<!-- 调用 -->
{{ telephone }}
{{ lis }}
  • with——自定义局部变量:with定义的变量只能在{% with %}{% endwith %}这个代码块间使用;
<!-- 定义一个普通变量 -->
{% with test=60 %}
	<!-- 内部可调用 -->
	{{ test }}
{% endwith %}
7)加载静态文件
  • 静态文件一般是我们在开发过程中用到的图片文件css文件js文件,在Flask工程中通常包含一个static文件目录,当需要调用静态文件是将会默认在该目录下进行查询,固不需要使用相对路径
  • 通常我们会在static文件目录下定义名为cssimagejs的文件夹分别存储这些静态文件;
  • 加载静态文件通常配合url_for函数使用(需要在双括号内调用),将模板标签的src、herf属性通过url_for(静态文件名称)设置为反转url要比使用相对路径更好
<head>
	<!-- 导入js文件 -->
	<script type="text/javascript" src="{{url_for('static',filename='js/jquery-3.5.1/jquery-3.5.1.js')}}"></script>
	<!-- 导入css文件 -->
	<link rel="stylesheet" href="{{url_for('static',filename='css/car.css')}}">
</head>
<body>
	<!-- 导入图片 -->
	<img alt="" src="{{ url_for('static',filename='image/car.jpg') }}"/>
</body>
  • 上述代码直接添加在html文件中需要的位置即可,我们以js为例验证导入是否成功(置于body内):
<script>
	// 该js文件是标准jq库复制粘贴来的
	if(jQuery){
		alert('jQuery已加载!');
	}
	else{
		alert('jQuery未加载!');
	}
</script>

结果截图

8)extends继承模板
  • 在include中,我们对于当前模板需要插入的代码块,可以在其他模板中定义,然后用include导入进来,外部模块是当前模块的补充
  • 而在extends中,我们当前的模板则是待装载的代码块,需要我们继承一个框架来搭载这些代码块,这时候就需要extend来导入框架(基类)模块了;

简单点说,如果把二者都按照父子模板来称呼,

  • include中我们在后端导入的父模板,将子模板插入到父模板中需要的地分;
  • extend中我们在后端导入子模板,将父模板的框架(页面布局)拿来显示子模板;
  • 父模板——基类模板father.html:

block后跟的是当前区块的名称,可以自己定,子模板内容中便可以依据名字插入到对应的位置;

<!DOCTYPE html>
<html lang="en">
<head>
	<!--除了装载部分,其他部分子模板一律安照当前父模板的定义显示-->
    <meta charset="UTF-8">
    <title>
    	<!--标题中子模板内容的装载位置-->
        {% block title %}
        {% endblock %}
        -我的网站
    </title>
</head>
<body>
	<!--主体中子模板内容的装载位置-->
    {% block body %}
        这是基类中的内容
    {% endblock %}
</body>
</html>
  • 子模板——son1.html:

在继承操作中,如果子模板实现了父模板的某个block,那么子模板中该block的代码就会覆写父模板中的代码,如果我们在子模板中仍然想保留父模板的代码,可以使用super()方法实现。

<!--继承的父类模板文件名称-->
{% extends "father.html" %}
<!--插入到父类代码的title区块的内容-->
{% block title %}
    网站首页
{% endblock %}
<!--插入到父类代码的body区块的内容-->
{% block body %}
	<!--保留父模板该block中原本的内容-->
    {{ super() }}
    <h4>这是网站首页的内容!</h4>
{% endblock %}
  • 子模板——son2.html:

如果我们在一个block中想要调用其他block的内容,则可以使用{{ self.其他block名称() }}方法实现。

<!--继承的父类模板文件名称-->
{% extends "father.html" %}
<!--插入到父类代码的title区块的内容-->
{% block title %}
    产品列表页
{% endblock %}
<!--插入到父类代码的body区块的内容-->
{% block body %}
    <h4>这是产品列表页的内容!</h4>
    取得网页标题的内容:
    <!--调用当前模板中其他block的内容 -->
    <h4>{{ self.title() }}</h4>
{% endblock %}
  • 后端导入两个子模板:
from flask import Flask,render_template

app=Flask(__name__)

@app.route('/son1')
def son1():
    return render_template('son_1.html')

@app.route('/son2')
def son2():
    return render_template('son_2.html')

if __name__ == '__main__':
    app.run(debug=True)

子模板1
子模板2


4. Flask数据交互

1)使用flask处理表单
  • 传统的前端通用表单,需要前后端共同完成操作,前端需要使用form标签来定义表单,而后端则需要使用request.form来获取post请求中的表单数据:
# 判断请求方式
if request.method == 'POST':
	# 获取表单中name为username的文本域提交的数据
	name = request.form.get('username')
	# 获取表单中name为password的文本域提交的数据
	password = request.form.get('password')
	return name+" "+password
  • 上述的方法既没有为表单提供保护措施,也不利于前后端分离的改进需求,固我们引入第三方扩展包:flask-wtfwtforms,来实现由后端单独完成的表单操作:
  • wtforms安装:pip install wtforms
  • flask-wtf安装:pip install Flask-WTFpip install flask-wtf
  • wtforms依照功能类别来说wtforms分别由以下几个类别:
    • Forms: 主要用于表单验证、字段定义、HTML生成,并把各种验证流程聚集在一起进行验证。
    • Fields: 包含各种类型的字段,主要负责渲染(生成HTML文本域)和数据转换。
    • Validator:主要用于验证用户输入的数据的合法性。比如Length验证器可以用于验证输入数据的长度。
    • Widgets:html插件,允许使用者在字段中通过该字典自定义html小部件。
    • Meta:用于使用者自定义wtforms功能(配置),例如csrf功能开启。
    • Extensions:丰富的扩展库,可以与其他框架结合使用,例如django。
  • Flask-WTF其实是对wtforms的简单集成,也能通过添加动态token令牌的方式,为所有Form表单提供免受CSRF(Cross-site request forgery——跨站请求伪造)攻击的技术支持

  • 我们可以采用以下方法来启用CSRF保护:

  1. 定义配置文件,再将配置文件中的配置语句通过app.config.from_object(<配置对象>)app.config.from_pyfile(<'配置文件名'>)导入到flask对象app中,这个配置对象可以是配置模块也可以是配置类
# config.py
CSRF_ENABLED = TRUE # 用于开启CSRF保护,但默认状态下都是开启的
SECRET_KEY = 'X1X2X3X4X5' # 用于生成动态令牌的秘钥
  • 其中SECRET_KEY用于建立加密令牌token,在我们编写程序时可以尽量定义的复杂一些;
from flask import Flask
from flask_wtf.csrf import CSRFProtect # 导入CSRFProtect模块
import config # 导入配置文件

app = Flask(__name__)
# 导入配置模块中的配置
app.config.from_object(config)
# 为当前应用程序启用WTF_CSRF保护,并返回一个CSRFProtect对象
csrf = CSRFProtect(app)
  1. 直接通过键值对的方式新增配置,即app.config['<配置名称>']=值添加配置到flask对象app中:
from flask import Flask
from flask_wtf.csrf import CSRFProtect # 导入CSRFProtect模块

app = Flask(__name__)
app.config['SECRET_KEY'] = 'ADJLAJDLA' # 用于生成动态令牌的秘钥
app.config['CSRF_ENABLED'] = True # 用于开启CSRF保护,但默认状态下都是开启的
# 为当前应用程序启用WTF_CSRF保护,并返回一个CSRFProtect对象
csrf = CSRFProtect(app)
  • 除了使用上述方法来配置CSRF保护,我们还需要用到flask_wtf与wtfroms来定义一个支持CSRF保护的后端表单,我们一般将其定义在一个类当中;
  • 该类需要继承基类:flask_wtf.FlaskFormflask_wtf.Form,二者完全相同,但Form即将被FlaskForm替换,推荐使用前者!
  • flask_wtf.FlaskForm继承自wtfroms.Form,是其子类
from flask import Flask,render_template,request
from flask_wtf.csrf import CSRFProtect
# 导入表单基类FlaskForm
from flask_wtf import FlaskForm
# 导入FlaskForm父类的表单字段组件(字符串文本域,密码文本域,提交按钮)
from wtforms import StringField,PasswordField,SubmitField
# 导入FlaskForm父类的表单验证组件(数据不为空,数据是否相同,数据长度)
from wtforms.validators import DataRequired,EqualTo,Length

app = Flask(__name__)
# 配置加密匙,后端为了保护网站加入的验证机制
# 不加会报错:RuntimeError: A secret key is required to use CSRF.
app.config['SECRET_KEY'] = 'ADJLAJDLA'
# app.config['CSRF_ENABLED'] = True # 可以省略
csrf = CSRFProtect(app)

# 定义表单模型类,继承FlaskForm
class Register(FlaskForm):
    # 定义表单中的元素,类似于html的form中定义input标签下的内容
    # label 用于点击后跳转到某一个指定的field框
    # validators 用于接收一个验证操作列表
    # render_kw 用于给表单字段添加属性,各属性以键值对的形式设置
    user_name = StringField(label='用户名:',validators=[DataRequired(message=u'用户名不能为空'),Length(6,16,message='长度位于6~16之间')],render_kw={'placeholder':'输入用户名'})
    # message中存放判断为错误时要返回的信息,EqualTo中第一个参数是要比较的field组件
    password = PasswordField(label='密码:',validators=[DataRequired(message=u'密码不能为空'),EqualTo('password2',message=u'两次输入需相同'),Length(6,16,message='长度位于6~16之间')],render_kw={'placeholder':'输入密码'})
    password2 = PasswordField(label='再次输入密码:', validators=[DataRequired(message=u'密码不能为空'),Length(6,16,message='长度位于6~16之间')],render_kw={'placeholder':'再次输入密码'})
    submit = SubmitField(label='提交')

@app.route('/',methods=['GET','POST'])
def register():
    # 实例化表单对象
    form = Register()
    if request.method == 'GET':
    	# 表单对象发送至前端
        return render_template('register.html',form=form)
    elif request.method == 'POST':
        # form.validate_on_submit() 等价于:request.method=='post' and form.validate()
        # form.validate() 用于验证表单的每个字段(控件),都满足时返回值为True
        if form.validate_on_submit():
            username = form.user_name.data
            password = form.password.data
            password2 = form.password2.data
            return 'login success'
        else:
            # flask的form使用一个字典来储存各控件的errors列表
            # print(type(form.errors))
            # 输出密码字段导致validate_on_submit为false的错误原因(两种方式)
            print(form.errors['password'])
            print(form.password.errors)
            return render_template('register.html',form=form)

if __name__ == '__main__':
    app.run()
  • 前端中使用后端定义的表单,同样也需要使用jinja2模板引擎,在{{ }}中调用我们传入的form对象,且表单开头需要使用form.csrf_tokenform.hidden_tag()语句添加动态令牌(用户不可见也不可编辑,用于验证):
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Flask_WTF</title>
    <style type="text/css">
        .div1 {
            height:450px;
            width:400px;
            border:1px solid #8A8989;
            margin:0 auto;
            padding: 10px;
        }
        .input {
            display: block;
            width: 350px;
            height: 40px;
            margin: 10px auto;
            }
        .button{
            background: #2066C5;
            color: white;
            font-size: 18px;
            font-weight: bold;
            height: 50px;
            border-radius: 4px;
        }
</style>
</head>
<body>
<div class="div1">
    <form action="" method="post">
        <!-- 启用CSRF验证,将token令牌置于表单内 -->
        <!-- 不添加该字段,后端验证会一直为False -->
        {{ form.csrf_token }}
        {{ form.username.label }}
        <!-- 可以在变量后添加括号,并在括号内设置变量的属性 -->
        {{ form.username(class='input',id='name',size=16) }}
        <!-- 错误展示 -->
        {% for e in form.username.errors %}
            <span style="color: red">{{ e }}</span>
        {% endfor %}
        <br>
        {{ form.password.label }}
        {{ form.password(class='input',id='pwd',size=16) }}
        <!-- 错误展示 -->
        {% for e in form.password.errors %}
            <span style="color: red">{{ e }}</span>
        {% endfor %}
        <br>
        {{ form.password2.label }}
        {{ form.password2(class='input',id='pwd2',size=16) }}
        <!-- 错误展示 -->
        {% for e in form.password2.errors %}
            <span style="color: red">{{ e }}</span>
        {% endfor %}
        <br>
        {{ form.submit(class='button') }}
    </form>
</div>
</body>
</html>
2)flash闪现的使用
  • 导入:from flask import flash
  • 后端的使用:flash("message"),message为消息内容;
  • 前端通过遍历get_flashed_messages()获取flash消息内容;
  • 示例代码(部分):
# --------------视图函数------------------
@app.route('/login/', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template("flash.html")
    else:
        username = request.form.get('username')
        password = request.form.get('password')
        # user = User.query.filter(User.username == username, User.password == password).first()
        user = User.query.filter(User.username == username).first()
        if user and user.check_password(password):
            session['user_id'] = user.id
            session['user_name'] = user.username
            session.permanent = True
            return redirect(url_for("index"))
        else:
            flash('用户名或密码不正确,请检查!')
            return render_template('flash.html')

# ---------------前端使用-----------------
<div class="warning">
    {% for message in get_flashed_messages() %}
        <div class="alert alert-warning alert-dismissible" role="alert">
            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span>
            </button>
            <strong>Warning!</strong> {{ message }}
        </div>
    {% endfor %}
</div>
  • 效果展示(还需配置CSS等):flash文本显示
3)Flask实现文件上传
  • 文件上传指的是客户端将文件上传(post请求发送)到服务器,服务器端进行保存的操作;
  • 这一部分涉及到的库和拓展知识过多,将解释放在代码注释中,直接上代码再做简单的流程分析;
  • 对于单文件的上传,主要用到flask_wtf.file库下的上传字段类:FileField,以及检验组件:FileRequiredFileAllowed
  • 后端应用程序文件"flask_file.py":
from flask import Flask,render_template,redirect,url_for
from flask import send_from_directory,session,flash
from flask_wtf import FlaskForm
from wtforms import SubmitField
from flask_wtf.file import FileField, FileRequired, FileAllowed
from flask_wtf.csrf import CSRFProtect


app = Flask(__name__)
app.config['SECRET_KEY'] = 'XASXA#@216.WDFAW'
csrf = CSRFProtect(app)

# 自定义表单类
class UploadForm(FlaskForm):
    # flask_WTF中提供的上传字段,label仍是字段的标签
    # validators接收的验证列表中:
    # FileRequired()用于验证是否包含文件对象,可以设置参数message
    # FileAllowed()用于验证文件的类型(后缀名),参数一为允许的文件后缀名的列表,参数二为可选的message
    photo = FileField(label='Upload Image', validators=[FileRequired(), FileAllowed(['jpg','jpeg','png','gif'])])
    # 采用了wtforms提供的提交字段,flask_WTF中似乎不包含该字段
    submit = SubmitField()

import os
# 验证文件大小,可以通过修改配置,设置请求报文的最大长度
# 接收到超过长度的文件,服务器会中断连接,并返回413错误响应
app.config['MAX_CONTENT_LENGTH'] = 1*1024*1024
# root_path获取当前程序文件所处的目录,使用path.join将其与uploads连接形成上传路径
# 将形成的路径写入到flask程序的配置当中,上传到服务器的文件都会保存在当前目录下的uploads目录(需要手动预先创建)中
app.config['UPLOAD_PATH'] = os.path.join(app.root_path, 'uploads')

# 导入 通用唯一识别码 库
import uuid
# 自定义文件重命名方法
def random_filename(filename):
    # os.path.splitext将文件路径与扩展名(文件类型标识)分开
    # 这里ext获取了文件扩展名用于拼接生成新的文件名
    ext = os.path.splitext(filename)[1]
    # 将生成的随机uuid与扩展名结合,形成新的文件名
    new_filename = uuid.uuid4().hex + ext
    # 返回新的文件名
    return new_filename

# 文件展示
@app.route('/uploaded-images')
def show_images():
    # 响应显示文件的模板
    return render_template('upload_file/uploaded.html')

# 获取文件路径
@app.route('/uploads/<path:filename>')
def get_file(filename):
    # send_from_directory可以生成对应文件的下载链接
    # 参数一是所有文件的存储目录(即uploads目录),参数二是该目录下要下载的文件名
    return send_from_directory(app.config['UPLOAD_PATH'], filename)

# 主程序
@app.route('/upload', methods=['GET', 'POST'])
def upload():
    # 实例化我们自定义的表单类UploadForm
    form = UploadForm()
    if form.validate_on_submit():
        # 使用表单对象form.photo的data属性即可获取到上传的文件
        f = form.photo.data
        # 处理文件名,这里采用自定义的random_filename方法来实现
        filename =random_filename(f.filename)
        # 服务器端使用save方法来保存接收到的文件
        # 读取配置中上传文件夹的路径,与文件名拼接后形成完整存储路径
        f.save(os.path.join(app.config['UPLOAD_PATH'], filename))
        # 使用flash通知用户文件上传成功
        flash('Upload success.')
        # 保存文件名到session,采用列表是为了后续拓展为多文件上传
        session['filenames'] = [filename]
        # 上传成功后显示图片,重定向到对应视图函数
        return redirect(url_for('show_images'))
    # 响应上传文件的模板,并把表单对象作为参数传递
    return render_template('upload_file/upload.html', form = form)
  • 前端上传文件模板"upload.html":
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>文件上传</title>
</head>
<body>
{% block content %}
    <!-- 当表单中包含上传文件字段时,需要给表单添加enctype属性 -->
    <!-- 并且enctype属性需要设置为"multipart/form-data" -->
    <!-- 这一步操作是告诉浏览器将上传的文件作为数据发送给服务器,否则浏览器只会把文件名作为表单数据提交给服务器 -->
    <form method="post" enctype="multipart/form-data">
        {{ form.csrf_token }}
        {{ form.photo.label }}<br>
        {{ form.photo }}<br>
        {{ form.submit }}<br>
    </form>
{% endblock %}
</body>
</html>
  • 前端上显示文件模板"uploaded.html":
<!--  继承操作 -->
{% extends 'upload_file/upload.html' %}
<!--  待装载部分 -->
{% block content %}
    <!-- 如果session中记录的已上传的文件不为空 -->
    {% if session.filenames %}
        <!-- 从session中读取文件名 -->
        {% for filename in session.filenames %}
            <!-- 定义一个超链接,其href指向的url为视图函数get_file的执行结果,点击后在新窗口打开该链接 -->
            <a href="{{ url_for('get_file', filename=filename) }}" target="_blank">
                <!-- 定义该超链接的显示方式为img图片,图片链接通过get_file视图函数获得 -->
                <img alt="" src="{{ url_for('get_file', filename=filename) }}">
            </a>
        {% endfor %}
    {% endif %}
{% endblock %}
  • 创建流程分析如下:
  1. 创建表单类UploadForm,表单类中实例化了我们需要的两个字段类:上传文件字段类FileField与提交字段类SubmitField
  • 上传字段内包含两种验证组件:
验证器说明
FileRequired(message=None)验证是否包含文件对象
FileAllowed( upload_set,message=None)用来验证文件类型,upload _set参数用来传入包含允许的文件后缀名列表
  • 出于安全考虑,必须对上传的文件类型进行限制。如果用户可以上传HTML文件,而且我们同时提供了视图函数获取上传后的文件,那么很容易导致XSS攻击;
  • 使用HTML5中的accept属性也可以在客户端实现简单的类型过滤,即:
    <input type="file" id="profile_pic" name="profile_pic" accept=".jpg, .jpeg, .png, .gif">
  • 当用户单击文件选择按钮后,打开的文件选择窗口会默认将accept属性之外的文件过滤掉(实质上只是不显示,没有真的过滤掉),但用户还是可以选择设定之外的文件,所以仍然需要在服务器端验证。
  1. 修改配置文件,设置服务器端的文件保存路径以及允许接收的最大文件大小;
  • 客户端上传的文件被保存在服务器的指定文件夹下,该文件夹的绝对路径存储在配置文件的自定义配置变量UPLOAD_PATH中,我们可以通过app.config['UPLOAD_PATH'] = os.path.join(app.root_path, '<文件夹名>')来定义存储路径;
  • 服务器允许接收的最大报文长度存储在配置文件的自定义变量MAX_CONTENT_LENGTH中;
  1. 实例化表单类UploadForm,并将该对象传入模板"upload.html";
@app.route('/upload', methods=['GET', 'POST'])
def upload():
    form = UploadForm()
    return render_template('upload.html',form = form)
  1. 在模板文件upload.html中渲染上传表单,注意表单需要添加属性enctype="multipart/form-data"
{% block content %}
    <!-- 当表单中包含上传文件字段时,需要给表单添加enctype属性 -->
    <!-- 并且enctype属性需要设置为"multipart/form-data" -->
    <!-- 这一步操作是告诉浏览器将上传的文件作为数据发送给服务器,否则浏览器只会把文件名作为表单数据提交给服务器 -->
    <form method="post" enctype="multipart/form-data">
        {{ form.csrf_token }}
        {{ form.photo.label }}<br>
        {{ form.photo }}<br>
        {{ form.submit }}<br>
    </form>
{% endblock %}
  1. 此时已经可以实现在客户端上传文件了,上传的文件要在服务器端被保存,首先需要对文件进行接收,即获取文件数据;
  • 如果是前端直接定义的上传字段,如步骤1中的示例,则需要在后端使用request对象的files属性来获取,该属性对应一个ImmutableMultiDict字典对象,其中存储了上传字段name属性(键)到文件对象(值)的映射;
  • 所以我们可以通过request.files.get(<name属性值>)request.files[<name属性值>]这两种方法来获取文件对象;

  • 而在上面代码中我们使用的是flask_WTF内的上传字段类FileField,flask_WTF会自动帮我们获取请求中的文件对象,固我们只需要通过上传字段类的data属性便可以获取客户端上传的文件,即f = 自定义表单对象.上传字段对象.data
  1. 获取了文件(FileStorage对象)后,我们需要对文件名进行简单的操作后才可以进行下一步;
  • 对于确定来源安全的文件,我们可以直接使用原文件名,原文件名可以通过FileStorage对象的filename属性获取,即filename = f.filename

  • 但要支持用户上传文件时,则必须对文件名进行处理,因为攻击者可能会在文件名中加入恶意路径。
  • 比如,如果恶意用户在文件名中加入表示上级目录的../(比如 ../../../home/username/.bashrc../../etc/passwd),那么当服务器端保存文件时,如果表示上级目录的../数量正确,就会导致服务器端对应的系统文件被覆盖或篡改,还有可能执行恶意脚本。
  • 对于这种情况,我们可以使用Werkzeug库提供的secure_filename(<文件名>)函数对文件名进行过滤,传递文件名作为参数,它会过滤掉文件名中的非ASCII字符,返回 “安全的文件名”;
>>> from werkzeug import secure_filename

>>> secure_filename('sam!@$%^&.jpg')
'sam.jpg'

>>> secure_filename('sam图片.jpg')
'sam.jpg'
  • 这种过滤方法存在一个弊端,就是如果文件名完全由非ASCII字符组成,那么过滤后将会得到一个空文件名
>>> secure_filename('图像.jpg')
'jpg'

  • 为了避免出现这种情况,更好的做法是使用统一的处理方式对所有上传的文件重新命名
  • 上面代码中定义的random_filename(filename)方法就是利用python内置的uuid模块来生成随机字符串,然后用该字符串为文件重命名;
  1. 在处理完文件名后,就可以对文件进行保存了,文件保存的路径我们已经在步骤二中进行了配置,指定的保存文件夹需要手动创建在当前目录下;
  • 要保存文件,只需要调用FileStorage对象的save()方法即可,该方法需要传入包含目标文件夹绝对路径文件名在内的完整保存路径;
  • f.save(os.path.join(app.config['UPLOAD_PATH'],filename))
  1. 文件保存后,通过flash来提示用户文件上传成功了(upload.html中未编写get_flashed_messages()所以不显示),并将文件名以列表的形式保存到session中;
# 使用flash通知用户文件上传成功
flash('Upload success.')
# 保存文件名到session,采用列表是为了后续拓展为多文件上传
session['filenames'] = [filename]
  1. 我们想要显示上传后的图片,则编写一个视图函数,在该视图函数内重定向到新的模板,在该模板中渲染出我们接收到的文件;
  • 上述代码中的show_images视图函数便返回uploaded.html模板
  • 另外定义一个get_file视图函数用于返回获取上传文件的url
  • 在uploaded.html模板中通过session获取文件名filename(右),再将获取到的文件名作为url变量filename(左),通过url_for传给视图get_file来获取文件url,最后将该url作为<img>标签的src属性值(上述代码中传输的文件为图片),便可以看到上传的文件了;
<img alt="" src="{{ url_for('get_file', filename=filename) }}">
  • 对于多文件的上传,在新版本的Flask-WTF发布后,可以和上传单个文件相同的方式处理表单;
  • 比如可以使用Flask-WTF提供的MultipleFileField类来创建多文件上传的字段,使用相应的验证器对文件进行验证;
  • 在视图函数中,可以继续使用form.validate_on_submit()来验证表单,并通过form.上传字段对象名.data来获取字段的数据:包含所有上传文件对象(werkzeug.datastructures.FileStorage)的列表
  • 此时可以通过for循环来统一修改文件名,并保存到session中;

多文件上传处理通常会使用JavaScript库在客户端进行预验证,并添加进度条来优化用户体验。

4)Cookie的使用

Cookie也被称作Cookies,它是一种让网站的服务器端可以把少量数据存储在客户端的硬盘或内存中,而后续又可以从客户端中读取该数据的技术。

  • 在网站中,http请求是呈无序状态
  • 无序状态是指协议对于事务处理没有记忆能力,同一个服务器上你新打开的网页和之前打开的网页之间没有任何联系,你的当前请求和上一次请求究竟是不是一个用户发出的,服务器也无从得知;
  • 为了解决这一问题,就出现了Cookie技术
  • 当用户访问服务器并登录成功后,服务器向客户端返回一些数据(Cookie);
  • 客户端将服务器返回的Cookie数据保存在本地,当用户再次访问服务器时,浏览器自动携带Cookie数据给服务器,服务器便知道访问者的身份信息了;
  • 值得注意的是,浏览器发送的是当前本地保存的所有作用在当前域且在有效期内的cookie
  • 也就是说由上一次访问当前网站某一板块时服务器发送的旧cookie如果未过期,在访问其他板块的新请求中也会被携带,一同发送出去。这就是为什么有些时候我们抓包获取到了cookie,但用它测试连接时却发现这个cookie可有可无;
  • 单个Cookie数据大小一般规定不超过3KB。

设置cookie

  • Cookie一般通过Response对象set_cookie()方法来设置,其基本语法如下:
# 前两个参数必须设置,后续参数则为可选参数
set_cookie(key,value[,max_age,expires,path,domain,secure,httponly,samesite])
  • set_cookie()的参数说明如下:
参数描述
key(或name)必需项,规定cookie的名称,字符串
value必需项,规定cookie的内容,字符串
max_age可选项,规定cookie的失效时间(单位:秒),
与expires同时设置时,其优先级更高
expires可选项,规定cookie的有效期,使用具体日期
path可选项,规定cookie在当前web下那些目录有效,
默认情况下为所有目录"/"有效
domain可选项,规定cookie作用的有效域(如:127.0.0.1)或域名(如:baidu.com),
默认情况下只作用于设置该cookie的域
secure可选项,规定cookie是否只能通过安全的HTTPS连接传输,默认关闭
httponly可选项,规定cookie是否只能被HTTP连接访问,默认关闭
即是否拒绝JavaScript访问Cookie
samesite可选项,规定cookie是否只能被附加在同一站点(客户端)的请求中
  • 每添加一个cookie,就调用一次Response.set_cookie()方法,示例代码如下:
from flask import Flask,request,Response

app = Flask(__name__)

@app.route('/')
def set_cookie():
    """
    实例化Response对象时可以传入三个参数:
    data:需要生成响应的数据。
    status:响应的状态码。
    平时我们请求接口的时候,如果成功状态码是200,Flask中这个状态码可以修改,在反爬虫中用处很大。
    如我将数据成功返回,而状态码是401,那么其他人爬虫的时候就不会爬这些数据。
    headers:设置请求头。
    """
    resp = Response("设置Cookie!")
    # 前两个参数默认传给key和value
    resp.set_cookie("username","zhangsan",path='/static')
    resp.set_cookie("password","123456",max_age=3600)
    # 将Response对象作为响应return
    return resp

if __name__ == '__main__':
    app.run(debug=True)
  • 可以通过下面的方法查看上例我们设置的cookie:
  1. 通过url左侧的网站信息图标来查看:网站信息图标
  • 可以看到第一个cookie中我们设置的name、value、path都生效了:username_cookie
  • 可以看到第二个cookie中我们设置的name、value、max_age都生效了:password_cookie
  • 比较可以发现,如果没设置有效期或失效时长,则默认到期时间为浏览器会话结束时。
  1. 通过开发者工具查看,查看network(网络)捕获到的请求的响应头即可:响应头查看
  • 除此之外,我们也可以通过修改响应对象Response的表单头headers来设置cookie:
@app.route('/h')
def set_cookie_h():
    resp = Response("设置Cookie!")
    # 通过添加表头的键值对来实现cookie设置
    # 第一项为key=value,需要同时设置这两个参数,后续项则用分号分隔
    resp.headers['Set-Cookie'] = " testname = lisi; Expires = SUN,01-Nov-2021 05:10:12 GMT; Max-Age=3600; path=/ "
    return resp
  • 上例中设置完的cookie如下,可以看到我们设置的expires被max_age给取代了:headers添加cookie

查看cookie

  • 在响应报文中设置了Cookies后,客户端在向服务器端发送一次请求(如登录操作)后,浏览器便会获取并保存该域名对应的cookie,这时我们可以通过request.cookies方法来查看我们发送的请求报文中携带的cookie;
  • 该方法返回的是一个字典对象,所以我们想要查看指定keyvalue,可以使用:request.cookies.get('<key>')request.cookies['<key>']
@app.route('/g')
def get_cookie():
    # 因为我们为key=username的cookie设置的工作目录为子一级的static,
    # 所以工作在当前目录下的程序无法获取该cookie的value,返回为None
    username=request.cookies.get('username')
    # 这两个cookie都可以直接获取value
    password=request.cookies.get('password')
    testname=request.cookies.get('testname')
    return password #---- 响应对象为:123456 ----
    # return testname #---- 响应对象为:lisi ----

删除cookie

  • 对于已经设置的cookie,我们可以通过Response对象delete_cookie()方法来删除,括号内填入需要删除的cookie名称(key):
@app.route('/d')
def delete_cookie():
    resp = Response('删除Cookie!')
    resp.delete_cookie('username')
    return resp
  • 通过运行结果可以看出,所谓的删除cookie是指执行:将cookie的value置为空,有效期expires置为1970年1月1日等操作;删除cookie

设置cookie作用域

  • Cookie默认只能在主域名下使用,如果想要在子域名(蓝图页面)下也能使用Cookie,我们需要在子域名下调用主域名的Cookie,设置子域名的方法见上文
  • 主域名文件下设置cookie:
from flask import Flask,Response
import admin

app = Flask(__name__)

# 配置`SERVER_NAME`,设置主域名
app.config['SERVER_NAME'] = 'example.com:5000'
# 注册蓝图,指定了subdomain
app.register_blueprint(admin.bp)

# @app.route('/index')
# def index():
#     return '通过域名访问!'

@app.route('/')
def set_cookie():
    resp = Response("设置Cookie!")
    # domain设置为 .example.com 是允许 example.com 域名下的各子域名使用该cookie
    resp.set_cookie("username", "zhangsan",domain='.example.com')
    return resp

if __name__ == '__main__':
    app.run(host='0.0.0.0',debug=True)
  • 子域名文件下调用主域名的cookie:
from flask import Blueprint,request
bp = Blueprint('admin',__name__,subdomain='admin')

# @bp.route('/ad')
# def admin():
#     return 'Admin Page'

@bp.route('/')
def get_cookie():
    username = request.cookies.get('username')
    # 如果前一个对象为None,则将后一个对象作为响应
    return username or '没有获取到name值'
5)Session的使用

上文中已经介绍了cookie的使用,在介绍session使用之前,我们首先要了解到session是基于cookie实现的,也就是说二者之间是存在紧密联系的,下面我们就先来对cookie和session做一个具体的分析:

1. 为什么需要session和cookie呢?

  • 源于web系统的发展和变迁:
  • web1.0:强调的是资源的共享
    http协议是一种无状态的协议。
  • web2.0:强调的是交互
    交互意味着是有多步操作,请求和请求之间是有依赖存在的;
    引入了session和cookie机制是来实现状态的记录。
  • web3.0: 强调的是双赢。

2. Session和cookie的特征

  • session和cookie都是由服务器生成的,都是用来存储特定的值(键值对应);
  • session是存储在服务器的,而cookie是会返回给客户端的。
    • 返回形式:置于响应信息头 —— set-cookie
  • 客户端(浏览器)在发送请求的时候,会自动将存活、可用的cookie封装在请求头中和请求一起发送。
    • 发送形式:置于请求信息头 —— Cookie
  • cookie和session都是有其生命周期的;
  • cookie的生命周期,一般来说,cookie的生命周期受到两个因素的影响
    • cookie自身的存活时间,是服务器生成cookie时设定的;
    • 客户端是否保留了cookie。客户端是否保留cookie,只对客户端自身有影响,对其它封包工具是没有影响的。
  • session的生命周期,一般来说,session的生命周期也是受到两个因素的影响:
    • 服务器对于session对象的保存的最大时间的设置。
    • 客户端进程是否关闭。客户端进程关闭,只对客户端自身有影响,对其它封包工具是没有影响的。
  • cookie和session都是有其作用域的。

3. 为什么session比cookie安全?

  • cooke是存储在客户端的,是(用户)可见的,是(用户)可以改变的;
  • session是存储在服务器端的,是(用户)不可见的,是(用户)不可改变的。

经过上文对session和cookie的比较分析后,我们再把目光放回到这部分的主角session身上,来看看session是怎么工作的:

  • 当客户端第一次请求session对象时候,服务器会为客户端创建一个session,并将通过特殊算法算出一个session的ID,用来标识该session对象;
  • sessionID是一次会话的key,如果客户端的一次请求没有携带sessionID,那么服务器端就会为这次会话创建一个session用于存储内容,每个session都有唯一的sessionID;
  • session被创建之后,就可以调用session相关的方法往session中增加内容了,而这些内容只会保存在服务器中,发到客户端的只有sessionID
  • 值得注意的是:sessionID并不是session值,sessionID会以类似于cookie的方式返回给客户端;
  • sessionID是服务器用来识别、操作存储session值的对象的。一般来说,在服务器端,session的存储方式有文件方式与数据库方式,sessionID就是用来指向这个文件或数据库的标识;
  • 当客户端再次发送请求的时候,会将这个sessionID带上,服务器接受到请求之后就会依据sessionID找到相应的session,从而再次使用已经创建的session;
  • 服务端在创建了session的同时,会为该session生成唯一的sessionID,而sessionID会在随后的请求中会被用来重新获得已经创建的session
  • 既然sessionID是通过特殊算法来生成的,那么计算过程就涉及到了秘钥SECRET_KEY来完成加密与解密,我们可以通过自定义字符串系统随机生成等方式来配置秘钥app.config['SECRET_KEY']

设置session

  • 服务器端接收到请求会自动创建session,我们可以通过session['name']='value'方法来对session内的值进行设置;
  • name即key,是我们设置的变量名称;value则是变量的值;
from flask import Flask,session

# 设置session
@app.route('/')
def set_session():
    # 将 username=zhangsan 存储在session中
    # session的存储与获取都是字典方式
    session['username'] = 'zhangsan'
    return 'Session设置成功!'

获取session的值

  • 在设置了session值后,我们有两种方法来获取我们设置的值,类似于字典的值的获取,推荐使用第二种
    result = session['name'] → \rightarrow 如果内容不存在会报错;
    result = session.get('name') → \rightarrow 如果内容不存在会返回None;
# 获取session
@app.route('/g')
def get_session():
    # 通过字典方式获取session中的username值
    username = session.get('username')
    return username or 'Session为空!'

删除session的值或清空session所有内容

  • 也是与字典中的操作类似:
    ①删除单条session值,可以采用session.pop('name')方法;
    ②清空整个session内容,则采用session.clear()方法;
# 清除session
@app.route('/d')
def del_session():
    # 删除session中username的这条记录
    session.pop('username')
    # 清空session
    # session.clear()
    return 'Session被删除!'

指定session的过期时间

  • 如果没有指定session的过期时间,则默认是浏览器关闭(会话结束)后session就自动结束;
  • 如果使用session.permanent = True语句将session的permanent属性置为True,则session的有效期将被延长至31天
  • 如果想要为session指定过期时间,那么就需要修改配置文件中的’PERMANENT_SESSION_LIFETIME’为一个timedelta对象,之后再使用session.permanent = True语句,操作方法如下:
from datetime import timedelta
# 配置session有效期为7天
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)

from flask import session
# 将有效期设为启用
session.permanent = True

  • 最终得到一个完整的服务器端session操作过程,代码如下:
from flask import Flask,session
from datetime import timedelta

app = Flask(__name__)

import os
# 使用os库下的urandom随机生成秘钥
app.config['SECRET_KEY'] = os.urandom(24)
# 配置session有效期为7天
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)

# 设置session
@app.route('/')
def set_session():
    # 将 username=zhangsan 存储在session中
    # session的存储与获取都是字典方式
    session['username'] = 'zhangsan'
    # 将有效期设为启用
    session.permanent = True
    return 'Session设置成功!'

# 获取session
@app.route('/g')
def get_session():
    # 通过字典方式获取session中的username值
    username = session.get('username')
    return username or 'Session为空!'

# 清除session
@app.route('/d')
def del_session():
    # 删除session中username的这条记录
    session.pop('username')
    # 清空session
    # session.clear()
    return 'Session被删除!'

if __name__ == '__main__':
    app.run(debug=True)
  • 执行上述代码可以得到如下结果:
    sessionID
    抓包sessionID

  • 可以看到sessionID以cookie的形式被发送到了客户端;

  • 当再次发送请求时,也会在请求头内加上搭载了sessionID的cookie;

  • 所以说session是基于cookie(ID以cookie形式发送)实现的;
  • 该“cookie” 的:key = ‘session’,value = 加密得到的sessionID值。

5. Falsk访问数据库

1)初识Flask-SQLAlchemy

什么是SQLAlchemy?

  • SQLAlchemy是一个基于Python实现的ORM (Object Relational Mapping,对象关系映射)框架。该框架建立在DB API (数据库应用程序接口系统) 之上,使用关系对象映射进行数据库操作。简言之便是将类和对象转换成SQL,然后使用数据API (接口) 执行SQL 并获取执行结果。
  • 它的核心思想于在于将关系数据库表中的记录映射成为对象,以对象的形式展现,程序员可以把对数据库的操作转化为对对象的操作
  • 安装:pip install flask-sqlalchemy
  • flask数据库入门:点击跳转
  • flask数据库配置:点击跳转
  • 如果映射所连接的是MySQL数据库,还要事先安装好PyMySQL:pip install pymsql

对象-关系的映射(ORM)实质

  • 有SQL语句如下:
# 建立表book
create table book('id' int(11) NOT NULL AUTO_INCREMENT, 'tiltle' varchar(50),'publishing_office' varchar(100),'isbn' varchar(4));
  • 有book对象如下:
# 使用SQLALchemy创建表book
class Book(db.Model):
    __tablename__='book'
    id = db.Column(db.Integer, primary_key = True,autoincrement = True)	#定义id字段
    title = db.Column(db.String(50),nullable = False)	#定义title字段
    publishing_office = db.Column(db.String(100),nullable = False)	#定义出版社字段
    isbn = db.Column(db.String(100),nullable = False) #定义isbn号字段
    storage_time = db.Column(db.DateTime, default = datetime.now) # 入库时间字段
  • Flask-SQLALchemy的ORM框架便可以实现将操作数据库转变为操作对象,一个book表被抽象成了一个Book类,一个表中的id、tiltle、publishing_office、isbn、storage_time字段被抽象成一个类的五个属性,而该表的一条数据记录就抽象成该类的一个实例化对象,不用再写烦琐的底层SQL语句了。
  • 二者的映射可以概括如下表:
面向对象概念面向关系概念
实例对象表的行(记录)
类属性表的列(字段)

为什么使用ORM

  • 当需要实现一个应用程序时,如果不使用ORM,我们可能会写特别多的数据访问层的代码,从数据库保存、删除、读取对象信息,但这些代码都是重复的。如果使用ORM则能够大大减少重复性的代码
  • 对象关系映射(Object Relational Mapping,ORM),主要实现程序对象到关系数据库数据的映射,具有以下特点:
  • 简单:ORM以最基本的形式建模数据。比如 ORM会将MySQL 的一张表映射成一个类(模型),表的字段就是这个类的成员变量(属性)。
  • 精确:ORM 使所有的 MySQL 数据表都按照统一的标准精确地映射成一个类,使系统在代码层面保持准确统一。
  • 易懂:ORM使数据库结构文档化,程序员可以把大部分精力用在Web功能的开发和实现上,而不需要花费时间和精力在底层数据库驱动上。
  • 易用:ORM包含对持久类对象进行CRUD操作的API,例如:create()、update()、save()、load()、find()、find_all()和 where()等,也就是将SQL查询全部封装成了编程语言中的函数,通过函数的链式组合生成最终的SQL语句。通过这种封装避免了不规范、冗余、风格不统一的SQL语句,可以避免很多人为 Bug,方便编码风格的统一和后期维护。
  • 综上所述,使用ORM框架的最大优点是解决了重复读取数据库的问题,使程序员高效开发成为可能。最大的不足之处在于会牺牲程序的执行效率,特别是处理多表联查、where条件复杂之类的查询时,ORM 的语法会变得复杂。
2)初始化数据库配置
  • 要使用SQLAlchemy连接数据库,必须要进行必要的初始化配置后才能实现,数据库配置文件一般要求独立成一个文件,便于管理和移植;
  • 配置文件:config.py
USERNAME = 'root' #设置登录账号
PASSWORD = '123456' #设置登录密码
HOST = '127.0.0.1' #设置主机地址
PORT = '3306' #设置端口号
DATABASE ='flaskdb' #设置访问的数据库

# 创建URI(统一资源标志符)
'''
SQLALCHEMY_DATABASE_URI的固定格式为:
'{数据库管理系统名}://{登录名}:{密码}@{IP地址}:{端口号}/{数据库名}?charset={编码格式}'
'''
DB_URI = 'mysql://{}:{}@{}:{}/{}?charset=utf8'.format(USERNAME,PASSWORD,HOST,PORT,DATABASE)

# 设置数据库的连接URI
SQLALCHEMY_DATABASE_URI = DB_URI
# 设置动态追踪修改,如未设置只会提示警告
SQLALCHEMY_TRACK_MODIFICATIONS = False
# 设置查询时会显示原始SQL语句
SQLALCHEMY_ECHO = True
  • 上述配置文件设置完后,在flask程序文件下导入该文件,再用app.config.from_object方法导入到flask对象内即可;

  • 当然上述配置也可以直接在flask程序文件内设置,但是不利于后期管理;

  • 在完成配置的设置与导入后,可以用下面的代码检测配置是否成功。如果SQLAlchemy配置成功,程序运行将会不报错并正常返回一个服务器url:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import flask学习.config # 导入配置文件

app = Flask(__name__)
# 导入配置文件至flask对象
app.config.from_object(flask学习.config)

'''1. 直接用键值对插入配置:(使用 localhost 替代 127.0.1:3306)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:123456@localhost/flaskdb?charset=utf8'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.config['SQLALCHEMY_ECHO'] = True
'''

'''2. 定义配置类后导入:(使用 localhost 替代 127.0.1:3306)
class Config:
    SQLALCHEMY_DATABASE_URI = 'mysql://root:123456@localhost/flaskdb?charset=utf8'
    SQLALCHEMY_TRACK_MODIFICATIONS = True
    SQLALCHEMY_ECHO = True

app.config.from_object(Config)
'''

# 初始化一个SQLAlchemy对象
db = SQLAlchemy(app)
# 测试数据库连接是否成功(create_all将我们定义的所有表类映射为数据库下的表)
db.create_all()

@app.route('/')
def index():
    return 'index'

if __name__=='__main__':
    app.run(debug=True)
3)表模型的定义与数据库映射
  • SQLAlchemy允许我们依据数据库的表结构来构建数据模型,即用python类的方式定义一个表模型,再通过调用create_all()方法,就可以将我们定义的所有表模型全部映射为数据库下的表
  • 但要注意的是每次运行create_all()这行代码时都会将所有表创建一次,导致重复创建表的问题,可以在前面添加一行drop_all()代码来删除已经创建的表;
  • 在用类的方式定义表模型时,使用__tablename__='<表名>'来将字符串对象设为表名,使用<列名>=db.Column()来将Column类的实例对象设为表的字段;我们为字段(列)指定的数据类型和约束条件,作为Column实例化时的参数;
  1. SQLAlchemy常用的数据类型名称(必选参数):
类型名称Python类型描述
Integerint整型,通常为32位,映射到数据库中是int类型
SmallIntegerint短整型,通常为16位,映射到数据库中是int类型
BigIntegerint或long整型,精度不受限制,映射到数据库中是int类型
Floatfloat浮点数,映射到数据库中是float类型
Numericdecimal定点数
Stringstr可变长度字符串,映射到数据库中是varchar类型
Textstr可变长度字符串,适合大量文本
Unicodeunicode可变长度Unicode字符串
Booleanbool布尔值,映射到数据库中的是tinyint类型
Datedatetime.data日期类型
Timedatetime.time时间类型
Datetimedatetime.datetime日期时间类型
IntervalDatetime.timedata时间间隔
Enumstr字符列表
PickleType任意Python对象自动Pickle序列化
LargeBinarystr二进制
  1. SQLAlchemy可选的约束条件参数(加在数据类型后,一个或多个):
可选参数描述
Primary_key如果设置为True,该列为表的主键
unique如果设置为True,该列不允许有相同值
index如果设置为True,为提高查询效率,为该列创建索引
nullable如果设置为True,该列允许为空。设置为False,该列不允许为空值
default定义该列的默认值
  • 利用上述知识点,就可以实现SQLAlchemy创建表了,并可以尝试着进行插入记录的操作SQLAlchemy.session.add()
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import flask学习.config
from datetime import * # datatime库下的datatime类重名了

app = Flask(__name__)
app.config.from_object(flask学习.config)

# 初始化一个SQLAlchemy对象(该步要在导入config后执行)
# 实例化的同时将与数据库相关的配置读入
db = SQLAlchemy(app)
# 初始化app对象中与数据库相关的配置的设置,防止数据库信息泄露
db.init_app(app)

# 创建表模型类对象
class Book(db.Model):
    __tablename__='book'
    id = db.Column(db.Integer, primary_key = True,autoincrement = True)	#定义id字段
    title = db.Column(db.String(50),nullable = False)	#定义title字段
    publishing_office = db.Column(db.String(100),nullable = False)	#定义出版社字段
    isbn = db.Column(db.String(100),nullable = False) #定义isbn号字段
    storage_time = db.Column(db.DateTime, default = datetime.now) # 入库时间字段

if __name__ == '__main__':
    # 删除数据库下的所有上述定义的表,防止重复创建
    db.drop_all()
    # 将上述定义的所有表对象映射为数据库下的表单(创建表)
    db.create_all()

    # 向表中插入记录:实例化-插入-提交
    book1 = Book(id='001',title='人工智能导论',publishing_office ='高等教育出版社',isbn='9787040479843')
    db.session.add(book1)
    db.session.commit()
  • 运行后结果如下(pycharm展示):

SQLAlchemy创建表

4)数据的增、删、改、查操作

完整示例代码

  • 这里先展示一个包含增、删、改、查的flask程序文件,下文再对逐个操作进行分析(配置文件config.py见初始化配置部分):
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import flask学习.config
from datetime import * # datatime库下的datatime类重名了

app = Flask(__name__)
app.config.from_object(flask学习.config)

# 初始化一个SQLAlchemy对象(该步要在导入config后执行)
# 实例化的同时将与数据库相关的配置读入
db = SQLAlchemy(app)
# 初始化app对象中与数据库相关的配置的设置,防止数据库连接泄露
db.init_app(app)

# 创建表模型类对象
class Book(db.Model):
    __tablename__='book'
    id = db.Column(db.Integer, primary_key = True,autoincrement = True)	# 定义id字段
    title = db.Column(db.String(50),nullable = False)	                # 定义title字段
    publishing_office = db.Column(db.String(100),nullable = False)	    # 定义出版社字段
    price = db.Column(db.String(30), nullable=False)                    # 定义price号字段
    isbn = db.Column(db.String(50),nullable = False)                    # 定义isbn号字段
    storage_time = db.Column(db.DateTime, default = datetime.now)       # 入库时间字段

# 删除数据库下的所有上述定义的表,防止重复创建
db.drop_all()
# 将上述定义的所有表对象映射为数据库下的表单(创建表)
db.create_all()

# 添加数据的路由
@app.route('/add')
def add_record():
    book1 = Book(title='Python基础教程(第3版)', publishing_office ='人民邮电出版社', price = '68.30 ', isbn = '9787115474889')
    book2= Book(title='Python游戏编程快速上手第4版',publishing_office = '人民邮电出版社', price = '54.50', isbn = '9787115466419')
    book3 = Book(title='JSP+Servlet+Tomcat应用开发从零开始学',publishing_office = '清华大学出版社', price = '68.30', isbn = '9787302384496')

    db.session.add(book1)
    db.session.add(book2)
    db.session.add(book3)
    # 需要提交事务给数据库
    db.session.commit()
    return 'add success!'

# 查找数据的路由
@app.route('/query')
def query_record():
    # 查找id=1的第一个对象
    result = Book.query.filter(Book.id == '1').first()
    print(result.title)
    # 查找publishing_office=人民邮电出版社的全体对象
    result_list = Book.query.filter(Book.publishing_office == '人民邮电出版社').all()
    for books in result_list:
        print(books.title)
    return 'query success!'

# 修改数据的路由
@app.route('/edit')
def edit_record():
    # 查找id=1的第一个对象
    book1 = Book.query.filter(Book.id == '1').first()
    book1.price = 168
    # 需要提交事务给数据库
    db.session.commit()
    return 'edit success!'

# 删除数据的路由
@app.route('/delete')
def delete_record():
    # 查找id=9的第一个对象
    book2 = Book.query.filter(Book.id == '9').first()
    db.session.delete(book2)
    # 需要提交事务给数据库
    db.session.commit()
    return 'delete success!'

if __name__ == '__main__':
    app.run(debug=True)

数据的添加

  • 确保数据库已经建立好后,直接使用db.session.add(<实例化对象>)的方法就可以把依据某一个表类实例化的数据对象插入到对应表中;
  • 这一步操作需要进行事务提交db.session.commit()
  • 这里用一个路由来完成插入操作:
# 添加数据的路由
@app.route('/add')
def add_record():
    book1 = Book(title='Python基础教程(第3版)', publishing_office ='人民邮电出版社', price = '68.30 ', isbn = '9787115474889')
    book2= Book(title='Python游戏编程快速上手第4版',publishing_office = '人民邮电出版社', price = '54.50', isbn = '9787115466419')
    book3 = Book(title='JSP+Servlet+Tomcat应用开发从零开始学',publishing_office = '清华大学出版社', price = '68.30', isbn = '9787302384496')
	# 以列表形式添加必须使用add_all,错误使用add将会报错:Class 'builtins.list' is not mapped
    # db.session.add_all([book1,book2,book3])
    db.session.add(book1)
    db.session.add(book2)
    db.session.add(book3)
    # 需要提交事务给数据库
    db.session.commit()
    return 'add success!'
  • 访问http://127.0.0.1:5000/add后可以看到网页响应插入成功,MySQL数据库内也插入了对应的记录(这里访问了四次,共插入了十二条数据,方便后续执行查改等操作):

插入成功
插入记录展示

数据的查找

  • 数据的查找需要通过query.filter(<限制条件>)方法来实现,query继承自db.Model,query.filter返回的是查找到的所有满足限制条件的数据对象组成的列表,当没有限制条件时则返回表中所有记录对应的数据对象组成的列表;
  • 可以使用first()来获取查找到的第一个数据对象,也可以用all()来获取查找到的全部数据对象(一个列表);
  • 这里也用一个路由来完成查找操作:
# 查找数据的路由
@app.route('/query')
def query_record():
    # 查找id=1的第一个对象
    result = Book.query.filter(Book.id == '1').first()
    print(result.title)
    # 查找publishing_office=人民邮电出版社的全体对象
    result_list = Book.query.filter(Book.publishing_office == '人民邮电出版社').all()
    for books in result_list:
        print(books.title)
    return 'query success!'
  • 访问http://127.0.0.1:5000/query后可以看到网页响应查找成功,pycharm控制台也输出了对应查找结果:

查找成功
第一次查找
第二次查找
数据的修改

  • 数据的修改操作是基于数据的查找操作实现的,先通过查找记录获取到对应的数据对象,再对该对象的某一个字段进行修改即可;
  • 这一步操作需要进行事务提交db.session.commit()
  • 这里也用一个路由来完成修改操作:
# 修改数据的路由
@app.route('/edit')
def edit_record():
    # 查找id=1的第一个对象
    book1 = Book.query.filter(Book.id == '1').first()
    book1.price = 168
    # 需要提交事务给数据库
    db.session.commit()
    return 'edit success!'
  • 访问http://127.0.0.1:5000/edit后可以看到网页响应修改成功,查看MySQL数据库可以看到对应记录已被修改:

修改成功
修改书本的价格

数据的删除

  • 数据的删除和数据的修改一样,也是基于数据的查找实现的,先通过查找记录获取到对应的数据对象,再调用db.session.delete(<数据对象>)方法即可删除该数据对象与表中的记录;
  • 这一步操作需要进行事务提交db.session.commit()
  • 这里仍用一个路由来完成删除操作:
# 删除数据的路由
@app.route('/delete')
def delete_record():
    # 查找id=9的第一个对象
    book2 = Book.query.filter(Book.id == '9').first()
    db.session.delete(book2)
    # 需要提交事务给数据库
    db.session.commit()
    return 'delete success!'
  • 访问http://127.0.0.1:5000/delete后可以看到网页响应删除成功,查看MySQL数据库可以看到对应记录已被删除:

删除成功
删除查找到的记录

5)表模型的关联关系构建
  • 数据库实体型(表)间有3种关联关系:一对一一对多多对多。一个学生只有一个身份证号码,构成了一对一关系;一个班级有多名学生,构成了一对多的关系;一个学生可以选修多门课程,一门课程对应多个学生,课程和学生的关系就构成了多对多关系。

数据库回顾

  • 在数据库中对这三种类型的联系,处理方式如下:
  1. 一对一联系(1:1)一般是将每个实体型转化为一个关系模式,然后将联系任意一端的关系模式合并;被合并端的关系模式中要以外键的方式添加另一个关系模式的主属性,并将联系中包含的全部属性也加入自身属性。
  2. 一对多联系(1:n)同样也是将每个实体型转化为一个关系模式,然后将联系n端对应的关系模式合并;n端对应的关系模式中同样要以外键的方式添加1端对应的关系模式的主属性,并将联系中包含的全部属性也加入自身属性。
  3. 多对多联系(m:n)首先将每个实体型转化为一个关系模式,然后将联系也转化为一个单独的关系模式;这个联系对应的关系模式中要以外键的方式添加与其相连的关系模式的主属性,并包含自身的全部属性。
  • 其中一对一和一对多也可以采用多对多的方式来处理,但通常采用上面列举的方式(防止产生过多的关系模式);对上文中的部分解释如下:
  1. 实体型指的是一个对象,如:学生或老师;
  2. 联系是指两个实体型间的关系,如:学生和课程的关系是选修,老师和学生的关系是授课等;
  3. 关系模式就是用表的方式来表示这个实体对象,如:学生信息表,课程信息表;
  4. 模式的属性就是表的字段(列),如:学生的学号、性别,选修的课程等。

SQLAlchemy处理方式

  • 在SQLAlchemy中,为了在表模型间创建上述的三种关联关系,同样要在一个表模型中通过db.ForeignKey('表模型类.属性')的方式添加外键,绑定另一个表模型的主属性(primary_key);
  • 添加外键后,还需要使用<字段名1> = db.relationship('<多方表名>',backref=db.backref('<字段名2>')[,secondary=<Table对象名>,uselist=False])的方法来正式构建关系,其中:
  • 字段名1:当前表用该字段来获取另一个表中的信息;
  • 参数一 多方表名:一般将少的一方作为使用relationship方法的表模型,所以与其建立关系的另一个表模型就是多的一方;
  • 参数二 backref:另一个表用其指定的”字段名2“来获取当前表中的信息;
  • 可选参数 secondary:用于申明多对多关系,该参数用来指向我们通过Table类构建的关联关系表;
  • 可选参数 uselist:用于申明一对一关系,该参数设为False能禁用列表查询,即当前表用字段1只能在另一个表中获取一条信息(1:1);默认为True,此时当前表用字段1能在另一个表中获取一个信息列表(1:n);
  • 表模型实例化一个对象后,对象调用字段名1对应的属性,就可以依据自己的外键属性值(外键字段在当前表,直接读对象的外键属性值,并在另一张表中获取该值绑定的单条记录)或另一张表中外键字段的绑定情况(外键字段不在当前表,遍历另一张表,获取其中外键字段绑定了当前对象主键值的单条/多条记录)来获取另一个表中的信息;

  • 下面三个示例中,各用两张表来分别展示三种表间关系如何构建:

  1. 创建一对一关系,外键和relationship都可以由相关联的两个表模型中,任意一个表模型添加;
#定义用户表
class User(db.Model):
    __tablename__ = 'user'
    id = db.column (db.Integer, primary_key = True, autoincrement = True)
    username = db.Column(db.String(50),nullable = False)
    password = db.Column(db.String(50),nullable = False)
    phone = db.Column(db.String (11),nullable = False)
    email = db.Column(db.String (30),nullable = False)
    reg_time = db.Column(db.DateTime,default = datetime.now)

#定义借书证表
class Lib_card(db.Model):
    __tablename__ = 'lib_card'
    id = db.Column(db.Integer,primary_key = True,autoincrement = True)
    card_id = db.Column(db. Integer,nullable = False)
    papers_type = db.Column(db.String(50),nullable = False)
    borrow_reg_time = db.Column(db. DateTime,default = datetime.now)
    # 在Lib_card表中添加外键,绑定User表的主键id(表名不区分大小写)
    user_id = db.Column(db.Integer,db.ForeignKey('user.id'))
    # 通过relationship方法与User表建立起关系
    # Lib_card表的实例对象,可以通过users属性,查找到外键user_id对应的User表中的单条用户信息
    # User表的实例对象,可以通过cards属性,查找到Lib_card表中外键绑定了自身id的单条借书证信息
    users = db.relationship('User',backref = db.backref('cards'),uselist = False)
  1. 创建一对多关系,外键必须由相关联的两个表模型中,“多”对应的表模型添加;relationship则需要由“少”对应的表模型添加;
#定义作者表
class Writer(db.Model):
    __tablename__ = 'writer'
    id = db.Column(db.Integer,primary_key=True)
    name = db.Column(db.String(50) ,nullable=False)
    # 通过relationship方法与Book表建立起关系
    # Writer表的实例对象,可以通过books属性,查找到Book表中外键绑定了自身id的多条书本信息
    # Book表的实例对象,可以通过writers属性,查找到外键writer_id对应的Writer表中的单条作者信息
    books = db.relationship('Book',backref='writers')

#定义书本表
class Book(db.Model):
    __tablename__ = 'books'
    id = db.Column(db.Integer,primary_key=True)
    title = db.Column(db.String(50),nullable=False)
    publishing_office = db.Column(db.String(100),nullable=False)
    isbn = db.Column (db.String (50),nullable=False)
    # 在Book表中添加外键,绑定Writer表的主键id(表名不区分大小写)
    writer_id = db.Column(db.Integer,db.ForeignKey('writer.id'))
  1. 创建多对多关系,则需单独创建一个存储关联关系的表(不需要构建表模型类,直接用db.Table()方法实例化一个表对象),并在这个表中添加外键来绑定它所连接的表;relationship则可以由相关联的两个表模型中的任意一个添加;
# 实例化一个关联关系表对象
# 等号前的book_tag是Table对象名,等号后的参数一 book_tag是表名
book_tag = db.Table('book_tag',
                    # 设置外键,绑定Book表主键
                    db.Column('book_id',db.Integer,db.ForeignKey('book.id'),primary_key=True),
                    # 设置外键,绑定Shelfing表主键
                    db.Column('tag_id',db.Integer,db.ForeignKey('shelfing.id'),primary_key=True))

# 定义图书表
class Book(db.Model):
    __tablename__ = 'book'
    id = db.column(db.Integer,primary_key=True,autoincrement=True)
    name = db.Column(db.String(50),nullable=False)
    # 通过relationship方法与Shelfing表建立起关系
    # Book表的实例对象,可以通过tags属性,查找到book_tag表中外键绑定了自身id的多条关联记录-->to Shelfing
    # Shelfing表的实例对象,可以通过books属性,查找到book_tag表中外键绑定了自身id的多条关联记录-->to Book
    # 再通过每条关联记录内绑定的另一张表的外键,到对应表中获取信息
    tags = db.relationship('Shelfing',secondary=book_tag,backref=db.backref('books'))

# 定义图书上架建议表
class Shelfing(db .Model):
    __tablename__ = 'shelfing'
    id = db.Column(db.Integer,primary_key=True,nullable=False)
    tag = db.Column(db.String(50),nullable=False)
  • 注意:我们定义的表模型在实例化时,返回结果是一个表内记录对象;而用Table类直接实例化得到的是表对象
  • 在完成了上述表模型间关系创建后,creat_all()生成对应表 → \rightarrow db.session.add()插入数据 → \rightarrow query.filter执行查找操作,即可依据查询结果验证表间关系是否构建成功,这里不再提供示例。
6)init_app作用详解

init_app是什么?

  • 在数据增删改查部分,我们使用到了一行代码:db.init_app(),当时只知道它是初始化数据库的相关配置,却不知道它实际上干了什么。为了后续解决Flask循环引用的问题,这些基础的语句就必须要弄清楚。
  • 关于init_app(),官方文档给出的解释是这样的:This callback can be used to initialize an application for the use with this database setup. Never use a database in the context of an application not initialized that way or connections will leak.

意思是说:此回调函数可将应用程序中对于此数据库的设置进行初始化。切勿在未以这种方式初始化的应用程序上下文中使用数据库,否则(数据库)连接将泄漏

  • 打开init_app的源码,可以看到其中包含多条app.config.setdefault(<参数一>,<参数二>)语句,这些语句有什么用?实际上,setdefault会为指定的配置(参数一)设置默认值(参数二),前提是这个配置本身没有值,setdefault才能设置成功,如果之前已经为某个配置设置了值,则setdefault不会为该配置再设置默认值。

init_app怎么用?

  • 我们都非常熟悉db = SQLAlchemy(app),并且在前面的操作中经常把它添加在配置导入语句app.config.from_object(<配置文件名>)后方,使app中有关数据库的相关配置被加载到SQLAlchemy对象中:
import flask学习.config

app.config.from_object(flask学习.config)
db = SQLAlchemy(app)
print(db)
-----------------运行结果---------------
<SQLAlchemy engine=mysql://root:***@127.0.0.1:3306/flaskdb?charset=utf8>
  • 执行上述代码后,可以看到我们在配置文件中设置URI已经被传给了SQLAlchemy的engine。那么假如在执行db = SQLAlchemy(app)之前没有进行数据库的配置,结果会如何?
import flask学习.config

# app.config.from_object(flask学习.config)
db = SQLAlchemy(app)
print(db)

---------------运行结果------------------
D:\Python\lib\site-packages\flask_sqlalchemy\__init__.py:851: UserWarning: Neither SQLALCHEMY_DATABASE_URI nor SQLALCHEMY_BINDS is set. Defaulting SQLALCHEMY_DATABASE_URI to "sqlite:///:memory:".
  warnings.warn(

D:\Python\lib\site-packages\flask_sqlalchemy\__init__.py:872: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.  Set it to True or False to suppress this warning.
  warnings.warn(FSADeprecationWarning(

<SQLAlchemy engine=sqlite:///:memory:>

  • 可以看到python在给出两行警告之后,依然输出了SQLAlchemy对象,而且SQLAlchemy的engine也有值。细心的话你就会发现,这个值对应init_app中的第一条初始化配置语句:app.config.setdefault('SQLALCHEMY_DATABASE_URI', 'sqlite:///:memory:')
  • 这么看来,init_app方法在实例化时被执行了。但是仅这样判断还不够严谨,我们打开SQLAlchemy的源码,可以在__init__方法下看到这样一段代码:
def __init__(self, app=None, use_native_unicode=True,session_options=None,metadata=None, query_class=BaseQuery, model_class=Model,engine_options=None):
	'''
	中间部分的代码省略...
	'''
	# 关键代码:
	if app is not None:
		self.init_app(app)
  • 也就是说,只要在实例化SQLAlchemy对象的过程中,如果参数app不为空(判断是否传入了flask对象,参数app默认值为None),那么就会调用init_app方法来初始化flask对象中数据库部分的相关配置。这时如果我们提前为flask对象导入了配置文件,init_app就无法覆写我们自定义的配置内容,也就相当于“没有起作用”。
  • 这么看来init_app在每次实例化的时候都被执行,那么就不需要我们再手动调用它?其实不然。在下一部分提到的flask循环调用问题中,我们在实例化SQLAlchemy对象时将不再预先传入flask对象,这就意味着app==None,if语句下方的代码将不会被执行,我们需要手动在外部调用并执行这段代码
import flask学习.config

app.config.from_object(flask学习.config)
db = SQLAlchemy()
print(db)
---------------运行结果------------------
<SQLAlchemy engine=None>
  • 可以看到此时init_app方法没有被执行,engine值是None!我们必须在其下方添加db.app=appdb.init_app(app)方可让程序在后续能正常运行。
import flask学习.config

app.config.from_object(flask学习.config)
db = SQLAlchemy()
# 源代码中还包含self.app=app这条语句,所以添加此语句:
db.app=(app)
db.init_app(app)
print(db)
---------------运行结果------------------
<SQLAlchemy engine=mysql://root:***@127.0.0.1:3306/flaskdb?charset=utf8>
  • 不仅如此,在使用了db.app=app后,我们甚至可以把配置文件的导入操作放在db = SQLAlchemy()后面,因为这一步就是将db对象的app属性指向了我们的flask对象——app,后续对flask对象的修改也就是对db.app属性的修改(对象传参,传的是指针),但是要注意导入配置的操作要在db.init_app(db)前执行。
import flask学习.config

db = SQLAlchemy()
db.app=(app)
# 导入配置操作放在此处也不影响
app.config.from_object(flask学习.config)
db.init_app(app)
print(db)
---------------运行结果------------------
<SQLAlchemy engine=mysql://root:***@127.0.0.1:3306/flaskdb?charset=utf8>
7)Flask循环引用问题
  • 当我们在编写flask程序时,为了实现项目模块化,往往会把定义的表模型类都放在一个文件当中,然后在应用程序文件内导入该文件;但是我们在定义表模型类的时候往往又需要从应用程序文件内导入db对象,从而让表模型类继承db.Model类,这时候就出现了循环引用错误。
  • 示例如下:
  • app.py
from flask import Flask 
from flask学习.models import User 
from flask_sqlalchemy import SQLAlchemy
import flask学习.config

app = Flask(__name__) 
app.config.from_object(config)

db = SQLAlchemy(app)
db.create_all()

@app.route('/') def hello_world():
    return 'Hello world!'

if __name__=='__main__':
    app.run()
  • model.py
from flask学习.app import db

class User(db.Model):
    __tablename__ = 'user'
    id = db.Column(db.Integer,primary_key=True,autoincrement=True)
    username = db.Column(db.String(50),nullable=False)
    password = db.Column(db.String(100),nullable=False)
    telephone = db.Column(db.String (11), nullable=False) 
  • 上面的代码出现了app.py导入model.py,又出现了model.py导入app.py,循环导入出现报错:
ImportError: cannot import name 'db' from partially initialized module 'flask学习.app' (most likely due to a circular import)
  • 要想解决这个问题,就需要将导致app.py和model.py循环引用的资源SQLAlchemy对象,即db,放到另一个文件下,例如配置文件config.py中;
  • 这时我们要在config.py下导入SQLAlchemy库,然后使用db=SQLAlchemy()实例化对象db,此时无法在实例化时传入app(flask对象),因此需要在app.py下添加我们上一部分提到的db.init_app(app)
  • 修改后的示例如下:
  • config.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

USERNAME = 'root'
PASSWORD = '123456'
HOST ='127.0.0.1'
PORT = '3306'
DATABASE ='flaskdb'
DB_URI = 'mysql://{}:{}@{}:{}/{}?charset=utf8'.format(USERNAME,PASSWORD,HOST,PORT,DATABASE)

URI SQLALCHEMY_DATABASE_URI = DB_URI
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ECHO = True
  • app.py
from flask import Flask 
from flask学习.models import User
from flask学习.config import db  # 导入db
import flask学习.config  # 导入配置文件

app = Flask(__name__) 
app.config.from_object(config)

# 替换部分
db.app=app
db.init_app(app)
db.create_all()

@app.route('/') def hello_world():
    return 'Hello world!'

if __name__=='__main__':
    app.run()
  • model.py
from flask学习.config import db  # 导入db

class User(db.Model):
    __tablename__ = 'user'
    id = db.Column(db.Integer,primary_key=True,autoincrement=True)
    username = db.Column(db.String(50),nullable=False)
    password = db.Column(db.String(100),nullable=False)
    telephone = db.Column(db.String (11), nullable=False) 
  • 运行后,程序没有报错,循环引用问题得以解决。
6)python控制台下的查找操作
  • 导入该模块时报错:

尝试:上级包名.py模块名
例:from flask学习.flasksql import *

  • 直接all()查询与get()查询:
User.query.all() #获取User表存储下的全部对象的列表
'''  -->  [<User 1>,<User 2>,<User 3>]  '''
User.query.get(1) #获取User表中下标为1的对象,get方法必须指定下标
'''  -->  <User 1>  '''
User.query.get(1).name #此时可以通过指定某一属性获取具体内容
'''  -->  'zhangsan'  '''
User.query.count() #返回表中存储的数据总数
'''  -->  3  '''
  • 重写表模型类中的__repr__()方法,可以改变查询的返回值:
def __repr__(self):
    return '%s'%self.name
User.query.all() # 修改后,all()将返回对象内的name属性值的列表
'''  -->  [zhangsan,lisi,wangwu]  '''
User.query.get(1) # 同理get(1)也将直接获取第一个对象的name属性
'''  -->  zhangsan  '''
  • filter()过滤后再进行查询:
User.query.filter(User.name == 'zhangsan').all()
'''  -->  [zhangsan]  '''
User.query.filter(User.name == 'zhangsan').all()[0].id
'''  -->  1  '''
User.query.filter(User.name.like('%a%')).all() # %与_同sql语句中
'''  -->  [zhangsan,wangwu]  '''			   # 代表多个或一个字符

-导入sqlalchemy库下的and_方法,可以进行多个条件的查询:

from sqlalchemy import and_
#注意id需要使用字符串
User.query.filter(and_(User.name == 'zhangsan',id == '1')).all()
'''  -->  [zhangsan]  '''

四、 踩坑收录

1. name ! = ‘__main__’

  • py文件只有在自身被执行时,__name__才为'__main__',如果我们在当前目录下创建了flask项目时,一定要看清运行时使用的是flask服务器还是python;
  • 如果不小心使用flask服务器执行当前py文件,那么该文件会被视为由falsk服务器调用,此时__name__会是当前文件名而不是'__main__'!!!结果就是if __name__ == '__main__':下的代码死都不会执行的!!!

日志-1:本人目前是在校大学生,目前刚开始进行web相关的学习,这篇笔记是看完了一个简短的flask入门视频后进行的知识总结,日后在学习过程中有新的收获将会继续补充

2021/09/15


日志-2:随着学校内课程的进展,本人对笔记中很多简陋说明的地方做了详细的补充,参考的教材是钱游的《Python-Flask实战》。这本书作为flask的入门教材非常合适,对每一个知识点基本上都有代码展示,笔记中很多代码也是取自书上的示例

2021/10/15


日志-3:书上关于flask使用的内容已经基本整理完成,在整理的过程中发现这本书内对于蓝图、cookie和session等部分的讲解不够详细,只是介绍了基本的使用并附上代码。为了更好的理解这部分知识,本人翻阅了一些资料来自行补充,总结出来的最终效果还算满意

2021/10/26


日志-4:web课程进入期末阶段,书本上和flask有关的知识点基本上都整理在上面了,课程期末设计内还整理了两个小知识点,感兴趣的话可以看看

2021/11/26

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐