Flask是一个非常小的PythonWeb框架,被称为微型框架;只提供了一个稳健的核心,其他功能全部是通过扩展实现的;意思就是我们可以根据项目的需要量身定制,也意味着我们需要学习各种扩展库的使用。
1)安装:
pip install flask
2)组成:WSGI系统、调试、路由
3)模板引擎:Jinja2(由Flask核心开发者人员开发)
4)使用到装饰器:以@开头的代码方法
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()
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()
from flask import Flask
app = Flask(__name__)
# 可以在路径内以/<参数名>的形式指定参数,默认接收到的参数类型是string
'''#######################
以下为框架自带的转换器,可以置于参数前将接收的参数转化为对应类型
string 接受任何不包含斜杠的文本
int 接受正整数
float 接受正浮点数
path 接受包含斜杠的文本
########################'''
@app.route("/index/" ,)
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/" )
def index(value):
print(value)
return "Hello World!"
if __name__=='__main__':
app.run(debug=True)
url_map
,这个url_map
中包含了url到endpoint的映射;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
查看当前url与endpoint的绑定情况;# 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/' (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)
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()
{'static': <function Flask.__init__.<locals>.<lambda> at 0x000002056C378CA0>, 'Test': <function test at 0x000002056C3E0C10>}
#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()
想要在正常执行的代码的前、中、后时期,强行执行一段我们想要执行的功能代码,便要用到钩子函数——用特定装饰器装饰的函数。
下面将介绍Flask内,四种常用的钩子:
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
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
after_request
:每一次请求之后都会调用;# 代码替换视图函数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
teardown_request
:每一次请求之后都会调用;# 代码替换视图函数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
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()
使用: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()
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()
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<img src="../static/error404.jpg" alt="" width="1428px" height="57px">
body>
html>
url_for('static', filename='文件路径')
静态文件需要存储在当前工程下的static目录内。
url_for('模块名.视图名',变量=参数)
- 参数一中的视图名实质指的是endpoint,url_map中存储了url到endpoint的映射,只是默认情况下endpoint与视图函数名相同;
- 如果在装饰器中修改了endpoint,则url_for只能使用endpoint设置的名字来反转url;
- 在单模块程序下我们可以省略模块名,但当使用了蓝图(buleprint)后,参数一就必须使用"蓝图模块名.视图名",因为不同蓝图下的视图函数可以重名。
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)
- 定义时需要继承flask的
views.View
这一基类;- 每个类视图内必须包含一个
dispatch_request
方法,每当类视图接收到请求时都会执行该方法,返回值的设定和视图函数相同;- 视图函数可以通过@app.route和app.add_url_rule来进行注册(映射到url),但类视图只能通过app.add_url_rule来注册,注册时view_func不能直接使用类名,需要调用基类中的
as_view
方法来为自己取一个“视图函数名”
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)
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
这是首页!{{ ads }}
body>
html>
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
这是登录页面!{{ ads }}
body>
html>
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
这是注册页面!{{ ads }}
body>
html>
{'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.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)
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<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>
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
@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__)
登录操作!
这是新闻详情页!
inner
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
这是新闻列表页的第5页!
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页!
from flask import Blueprint
- 当接收到请求时,Flask会遍历Flask对象下(已注册)的各蓝图对象,比对蓝图对象中记录的url,比对成功则映射到该url绑定的视图函数并返回响应
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对象类似,但其不是一个单独的应用;
- 每拓展一个蓝图对象,就要在主路由文件下添加一行注册代码;
- 蓝图对象内记录了当前模块下的所有视图函数,固视图函数不可与蓝图对象同名;
- 在蓝图内需要通过蓝图对象来定义路由和调用其他装饰器,由蓝图对象定义的路由处于休眠状态,在蓝图被注册时才成为程序的一部分。
from flask import Blueprint
# 实例化蓝图对象,参数一类似于蓝图对象的名称
# 一个app下的蓝图对象不可重名
new_list = Blueprint('news',__name__)
# 蓝图对象的使用和app类似
# 一个蓝图下的视图函数名、endpoint不可重复
@new_list.route('/news')
def new():
return '这是新闻模块!'
from flask import Blueprint
# 实例化蓝图对象,参数一类似于蓝图对象的名称
# 一个app下的蓝图对象不可重名
new_list = Blueprint('products',__name__)
# 蓝图对象的使用和app类似
# 一个蓝图下的视图函数名、endpoint不可重复
@new_list.route('/products')
def product():
return '这是产品模块!'
new_list = Blueprint('news',__name__,url_prefix='/index')
@new_list.route('/news')
def new():
return '这是新闻模块!'
app.register_blueprint(news.new_list,url_prefix='/test')
设置域名
# 当前网站域名设置为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),右边则是我们定义的域名
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)
设置子域名
subdomain='admin'
即为该蓝图设置子域名admin;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'
from flask import rander_template
,模板中注释需放在{# #}
中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()
{{ }}
中使用该变量:DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
hello world
<br>
{{ 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 }}
。jinja2模板引擎中也可使用if和for控制语句,但是语句需要放置在{% %}
中;
if条件判断语句必须包含结束标签{% endif %},其他部分与python中类似,可以与比较运算符> >= < <= == !=
结合使用,或与逻辑运算符and,or,not,()
结合使用;
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
{% if name==1 %}
<h1>恭喜你抽中了一等奖!h1>
{% if name==2 %}
<h1>恭喜你抽中了二等奖!h1>
{% else %}
<h1>恭喜你抽中了三等奖!h1>
{% endif %}
body>
html>
{% for 目标 in 对象 %}
<p>目标p>
{% endfor %}
- loop.index: 获取当前的索引值 从1开始
- loop.index0:获取当前的索引值 从0开始
- loop.first: 判断当前是否是第一次迭代, 是返回True否则返回False
- loop.last: 判断当前是否是最后一次迭代, 是返回True否则返回False
- 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>
{{ 内容 | 过滤器 }}
的" | "后使用;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>Titletitle>
head>
<body>
{{ 'hello_world' | upper }}
<br>
{{ 'hello world' | title }}
<br>
{{ 'hello_world' | replace('hello','hi') }}
<br>
mylist列表:{{ data.mylist | li2 }}
body>
html>
<span class="{{ 给过滤器的参数|过滤器名称 }}">span>
{% %}
中进行。
{% macro input1() %}
<label>表单项1:
<input type="text" name='username' value=''>
<br>
label>
{% endmacro %}
{% macro input2(name, value='', type='text', size=30) %}
<label>表单项2:
<input type="{{ type }}" name="{{ name }}" value="{{ value }}" size="{{ size }}">
<br>
label>
{% endmacro %}
{{ input1() }}
{{ input2() }}
{{ input2('username') }}
{{ input2('username', value='cheng', type='password', size=50) }}
import 模板文件名 as 别名
或from 模板文件名 import 宏名称
,就像python中库和包的导入一样;{% %}
中进行,调用过程在{{ }}
在进行。
{% 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>
<p>用户名:{{input2('username')}}p>
<p>密码:{{input2('password',type='password')}}p>
<p>登录:{{input2('submit',type='submit',value='登录')}}p>
div>
{% %}
中使用,采用语句{% include 模块名 %}
,需要注意两点:
- include是直接将目标模板中的所有内容直接“copy”在当前位置,所以被导入的模板如果有head和body部分也将被导入过来;
- include和import都是在templates这个目录下搜索的,所以使用路径时不需要添加相对路径:上级目录 “ ../ ” 和当前目录 “ ./ ” ;
(PS:子模板与父模板的说法是为了更好理解)
DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
{% include "common/header.html" %}
<div class="content">
中间的
div>
{% include "common/footer.html" %}
body>
html>
<nav>
<div class="top">
这是顶部
div>
nav>
<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)
{% set telephone=1234567890 %}
{% set lis=[('produce.html','produce'),('index.html','index')] %}
{{ telephone }}
{{ lis }}
{% with %}
到{% endwith %}
这个代码块间使用;
{% with test=60 %}
{{ test }}
{% endwith %}
url_for(静态文件名称)
设置为反转url要比使用相对路径更好。<head>
<script type="text/javascript" src="{{url_for('static',filename='js/jquery-3.5.1/jquery-3.5.1.js')}}">script>
<link rel="stylesheet" href="{{url_for('static',filename='css/car.css')}}">
head>
<body>
<img alt="" src="{{ url_for('static',filename='image/car.jpg') }}"/>
body>
<script>
// 该js文件是标准jq库复制粘贴来的
if(jQuery){
alert('jQuery已加载!');
}
else{
alert('jQuery未加载!');
}
script>
简单点说,如果把二者都按照父子模板来称呼,
- include中我们在后端导入的父模板,将子模板插入到父模板中需要的地分;
- extend中我们在后端导入子模板,将父模板的框架(页面布局)拿来显示子模板;
block后跟的是当前区块的名称,可以自己定,子模板内容中便可以依据名字插入到对应的位置;
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>
{% block title %}
{% endblock %}
-我的网站
title>
head>
<body>
{% block body %}
这是基类中的内容
{% endblock %}
body>
html>
在继承操作中,如果子模板实现了父模板的某个block,那么子模板中该block的代码就会覆写父模板中的代码,如果我们在子模板中仍然想保留父模板的代码,可以使用
super()
方法实现。
{% extends "father.html" %}
{% block title %}
网站首页
{% endblock %}
{% block body %}
{{ super() }}
<h4>这是网站首页的内容!h4>
{% endblock %}
如果我们在一个block中想要调用其他block的内容,则可以使用
{{ self.其他block名称() }}
方法实现。
{% extends "father.html" %}
{% block title %}
产品列表页
{% endblock %}
{% block body %}
<h4>这是产品列表页的内容!h4>
取得网页标题的内容:
<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)
request.form
来获取post请求中的表单数据:# 判断请求方式
if request.method == 'POST':
# 获取表单中name为username的文本域提交的数据
name = request.form.get('username')
# 获取表单中name为password的文本域提交的数据
password = request.form.get('password')
return name+" "+password
- wtforms安装:
pip install wtforms
- flask-wtf安装:
pip install Flask-WTF
或pip 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保护:
app.config.from_object(<配置对象>)
或app.config.from_pyfile(<'配置文件名'>)
导入到flask对象app中,这个配置对象可以是配置模块也可以是配置类:# config.py
CSRF_ENABLED = TRUE # 用于开启CSRF保护,但默认状态下都是开启的
SECRET_KEY = 'X1X2X3X4X5' # 用于生成动态令牌的秘钥
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)
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)
flask_wtf.FlaskForm
或flask_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()
form.csrf_token
或form.hidden_tag()
语句添加动态令牌(用户不可见也不可编辑,用于验证):DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Flask_WTFtitle>
<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">
{{ 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>
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">×</span>
</button>
<strong>Warning!</strong> {{ message }}
</div>
{% endfor %}
</div>
flask_wtf.file
库下的上传字段类:FileField,以及检验组件:FileRequired和 FileAllowed;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/' )
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)
DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>文件上传title>
head>
<body>
{% block content %}
<form method="post" enctype="multipart/form-data">
{{ form.csrf_token }}
{{ form.photo.label }}<br>
{{ form.photo }}<br>
{{ form.submit }}<br>
form>
{% endblock %}
body>
html>
{% extends 'upload_file/upload.html' %}
{% block content %}
{% if session.filenames %}
{% for filename in session.filenames %}
<a href="{{ url_for('get_file', filename=filename) }}" target="_blank">
<img alt="" src="{{ url_for('get_file', filename=filename) }}">
a>
{% endfor %}
{% endif %}
{% endblock %}
FileField
与提交字段类SubmitField
;
- 上传字段内包含两种验证组件:
验证器 说明 FileRequired(message=None) 验证是否包含文件对象 FileAllowed( upload_set,message=None) 用来验证文件类型,upload _set参数用来传入包含允许的文件后缀名列表
- 出于安全考虑,必须对上传的文件类型进行限制。如果用户可以上传HTML文件,而且我们同时提供了视图函数获取上传后的文件,那么很容易导致XSS攻击;
- 使用HTML5中的accept属性也可以在客户端实现简单的类型过滤,即:
- 当用户单击文件选择按钮后,打开的文件选择窗口会默认将accept属性之外的文件过滤掉(实质上只是不显示,没有真的过滤掉),但用户还是可以选择设定之外的文件,所以仍然需要在服务器端验证。
- 客户端上传的文件被保存在服务器的指定文件夹下,该文件夹的绝对路径存储在配置文件的自定义配置变量UPLOAD_PATH中,我们可以通过
app.config['UPLOAD_PATH'] = os.path.join(app.root_path, '<文件夹名>')
来定义存储路径;- 服务器允许接收的最大报文长度存储在配置文件的自定义变量MAX_CONTENT_LENGTH中;
@app.route('/upload', methods=['GET', 'POST'])
def upload():
form = UploadForm()
return render_template('upload.html',form = form)
enctype="multipart/form-data"
;{% block content %}
<form method="post" enctype="multipart/form-data">
{{ form.csrf_token }}
{{ form.photo.label }}<br>
{{ form.photo }}<br>
{{ form.submit }}<br>
form>
{% endblock %}
- 如果是前端直接定义的上传字段,如步骤1中的示例,则需要在后端使用request对象的files属性来获取,该属性对应一个ImmutableMultiDict字典对象,其中存储了上传字段name属性(键)到文件对象(值)的映射;
- 所以我们可以通过
request.files.get(
或) request.files[
这两种方法来获取文件对象;]
- 而在上面代码中我们使用的是flask_WTF内的上传字段类FileField,flask_WTF会自动帮我们获取请求中的文件对象,固我们只需要通过上传字段类的data属性便可以获取客户端上传的文件,即
f = 自定义表单对象.上传字段对象.data
;
- 对于确定来源安全的文件,我们可以直接使用原文件名,原文件名可以通过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模块来生成随机字符串,然后用该字符串为文件重命名;
- 要保存文件,只需要调用FileStorage对象的save()方法即可,该方法需要传入包含目标文件夹绝对路径和文件名在内的完整保存路径;
- 即
f.save(os.path.join(app.config['UPLOAD_PATH'],filename))
# 使用flash通知用户文件上传成功
flash('Upload success.')
# 保存文件名到session,采用列表是为了后续拓展为多文件上传
session['filenames'] = [filename]
- 上述代码中的show_images视图函数便返回uploaded.html模板 ;
- 另外定义一个get_file视图函数用于返回获取上传文件的url;
- 在uploaded.html模板中通过session获取文件名filename(右),再将获取到的文件名作为url变量filename(左),通过url_for传给视图get_file来获取文件url,最后将该url作为标签的src属性值(上述代码中传输的文件为图片),便可以看到上传的文件了;
<img alt="" src="{{ url_for('get_file', filename=filename) }}">
form.validate_on_submit()
来验证表单,并通过form.上传字段对象名.data
来获取字段的数据:包含所有上传文件对象(werkzeug.datastructures.FileStorage)的列表;多文件上传处理通常会使用JavaScript库在客户端进行预验证,并添加进度条来优化用户体验。
Cookie也被称作Cookies,它是一种让网站的服务器端可以把少量数据存储在客户端的硬盘或内存中,而后续又可以从客户端中读取该数据的技术。
- 无序状态是指协议对于事务处理没有记忆能力,同一个服务器上你新打开的网页和之前打开的网页之间没有任何联系,你的当前请求和上一次请求究竟是不是一个用户发出的,服务器也无从得知;
- 当用户访问服务器并登录成功后,服务器向客户端返回一些数据(Cookie);
- 客户端将服务器返回的Cookie数据保存在本地,当用户再次访问服务器时,浏览器自动携带Cookie数据给服务器,服务器便知道访问者的身份信息了;
- 值得注意的是,浏览器发送的是当前本地保存的所有作用在当前域且在有效期内的cookie;
- 也就是说由上一次访问当前网站某一板块时服务器发送的旧cookie如果未过期,在访问其他板块的新请求中也会被携带,一同发送出去。这就是为什么有些时候我们抓包获取到了cookie,但用它测试连接时却发现这个cookie可有可无;
- 单个Cookie数据大小一般规定不超过3KB。
设置cookie
set_cookie()
方法来设置,其基本语法如下:# 前两个参数必须设置,后续参数则为可选参数
set_cookie(key,value[,max_age,expires,path,domain,secure,httponly,samesite])
参数 描述 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访问Cookiesamesite 可选项,规定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)
@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
request.cookies
方法来查看我们发送的请求报文中携带的cookie;request.cookies.get('')
或request.cookies['']
@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
delete_cookie()
方法来删除,括号内填入需要删除的cookie名称(key):@app.route('/d')
def delete_cookie():
resp = Response('删除Cookie!')
resp.delete_cookie('username')
return resp
设置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)
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值'
上文中已经介绍了cookie的使用,在介绍session使用之前,我们首先要了解到session是基于cookie实现的,也就是说二者之间是存在紧密联系的,下面我们就先来对cookie和session做一个具体的分析:
1. 为什么需要session和cookie呢?
- web1.0:强调的是资源的共享
http协议是一种无状态的协议。- web2.0:强调的是交互
交互意味着是有多步操作,请求和请求之间是有依赖存在的;
引入了session和cookie机制是来实现状态的记录。- web3.0: 强调的是双赢。
2. Session和cookie的特征
- session是存储在服务器的,而cookie是会返回给客户端的。
- 返回形式:置于响应信息头 —— set-cookie
- 客户端(浏览器)在发送请求的时候,会自动将存活、可用的cookie封装在请求头中和请求一起发送。
- 发送形式:置于请求信息头 —— Cookie
- cookie的生命周期,一般来说,cookie的生命周期受到两个因素的影响
- cookie自身的存活时间,是服务器生成cookie时设定的;
- 客户端是否保留了cookie。客户端是否保留cookie,只对客户端自身有影响,对其它封包工具是没有影响的。
- session的生命周期,一般来说,session的生命周期也是受到两个因素的影响:
- 服务器对于session对象的保存的最大时间的设置。
- 客户端进程是否关闭。客户端进程关闭,只对客户端自身有影响,对其它封包工具是没有影响的。
3. 为什么session比cookie安全?
经过上文对session和cookie的比较分析后,我们再把目光放回到这部分的主角session身上,来看看session是怎么工作的:
- session被创建之后,就可以调用session相关的方法往session中增加内容了,而这些内容只会保存在服务器中,发到客户端的只有sessionID;
- 值得注意的是:
sessionID并不是session值
,sessionID会以类似于cookie的方式返回给客户端;- sessionID是服务器用来识别、操作存储session值的对象的。一般来说,在服务器端,session的存储方式有文件方式与数据库方式,sessionID就是用来指向这个文件或数据库的标识;
- 当客户端再次发送请求的时候,会将这个sessionID带上,服务器接受到请求之后就会依据sessionID找到相应的session,从而再次使用已经创建的session;
- 服务端在创建了session的同时,会为该session生成唯一的sessionID,而sessionID会在随后的请求中会被用来重新获得已经创建的session;
app.config['SECRET_KEY']
。设置session
session['name']='value'
方法来对session内的值进行设置;from flask import Flask,session
# 设置session
@app.route('/')
def set_session():
# 将 username=zhangsan 存储在session中
# session的存储与获取都是字典方式
session['username'] = 'zhangsan'
return '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.pop('name')
方法;session.clear()
方法;# 清除session
@app.route('/d')
def del_session():
# 删除session中username的这条记录
session.pop('username')
# 清空session
# session.clear()
return 'Session被删除!'
指定session的过期时间
session.permanent = True
语句将session的permanent属性置为True,则session的有效期将被延长至31天;session.permanent = True
语句,操作方法如下:from datetime import timedelta
# 配置session有效期为7天
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
from flask import session
# 将有效期设为启用
session.permanent = True
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)
- 所以说session是基于cookie(ID以cookie形式发送)实现的;
- 该“cookie” 的:key = ‘session’,value = 加密得到的sessionID值。
什么是SQLAlchemy?
- 安装:
pip install flask-sqlalchemy
- flask数据库入门:点击跳转
- flask数据库配置:点击跳转
- 如果映射所连接的是MySQL数据库,还要事先安装好PyMySQL:
pip install pymsql
对象-关系的映射(ORM)实质
# 建立表book
create table book('id' int(11) NOT NULL AUTO_INCREMENT, 'tiltle' varchar(50),'publishing_office' varchar(100),'isbn' varchar(4));
# 使用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) # 入库时间字段
id、tiltle、publishing_office、isbn、storage_time
字段被抽象成一个类的五个属性,而该表的一条数据记录就抽象成该类的一个实例化对象,不用再写烦琐的底层SQL语句了。
- 二者的映射可以概括如下表:
面向对象概念 面向关系概念 类 表 实例对象 表的行(记录) 类属性 表的列(字段)
为什么使用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,方便编码风格的统一和后期维护。
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)
create_all()
方法,就可以将我们定义的所有表模型全部映射为数据库下的表;create_all()
这行代码时都会将所有表创建一次,导致重复创建表的问题,可以在前面添加一行drop_all()
代码来删除已经创建的表;__tablename__='<表名>'
来将字符串对象设为表名,使用<列名>=db.Column()
来将Column类的实例对象设为表的字段;我们为字段(列)指定的数据类型和约束条件,作为Column实例化时的参数;
- SQLAlchemy常用的数据类型名称(必选参数):
类型名称 Python类型 描述 Integer int 整型,通常为32位,映射到数据库中是int类型 SmallInteger int 短整型,通常为16位,映射到数据库中是int类型 BigInteger int或long 整型,精度不受限制,映射到数据库中是int类型 Float float 浮点数,映射到数据库中是float类型 Numeric decimal 定点数 String str 可变长度字符串,映射到数据库中是varchar类型 Text str 可变长度字符串,适合大量文本 Unicode unicode 可变长度Unicode字符串 Boolean bool 布尔值,映射到数据库中的是tinyint类型 Date datetime.data 日期类型 Time datetime.time 时间类型 Datetime datetime.datetime 日期时间类型 Interval Datetime.timedata 时间间隔 Enum str 字符列表 PickleType 任意Python对象 自动Pickle序列化 LargeBinary str 二进制
- SQLAlchemy可选的约束条件参数(加在数据类型后,一个或多个):
可选参数 描述 Primary_key 如果设置为True,该列为表的主键 unique 如果设置为True,该列不允许有相同值 index 如果设置为True,为提高查询效率,为该列创建索引 nullable 如果设置为True,该列允许为空。设置为False,该列不允许为空值 default 定义该列的默认值
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()
完整示例代码
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数据库可以看到对应记录已被删除:数据库回顾
- 一对一联系(1:1)一般是将每个实体型转化为一个关系模式,然后将联系与任意一端的关系模式合并;被合并端的关系模式中要以外键的方式添加另一个关系模式的主属性,并将联系中包含的全部属性也加入自身属性。
- 一对多联系(1:n)同样也是将每个实体型转化为一个关系模式,然后将联系与n端对应的关系模式合并;n端对应的关系模式中同样要以外键的方式添加1端对应的关系模式的主属性,并将联系中包含的全部属性也加入自身属性。
- 多对多联系(m:n)首先将每个实体型转化为一个关系模式,然后将联系也转化为一个单独的关系模式;这个联系对应的关系模式中要以外键的方式添加与其相连的关系模式的主属性,并包含自身的全部属性。
- 实体型指的是一个对象,如:学生或老师;
- 联系是指两个实体型间的关系,如:学生和课程的关系是选修,老师和学生的关系是授课等;
- 关系模式就是用表的方式来表示这个实体对象,如:学生信息表,课程信息表;
- 模式的属性就是表的字段(列),如:学生的学号、性别,选修的课程等。
SQLAlchemy处理方式
db.ForeignKey('表模型类.属性')
的方式添加外键,绑定另一个表模型的主属性(primary_key);<字段名1> = db.relationship('<多方表名>',backref=db.backref('<字段名2>')[,secondary=,uselist=False])的方法来正式构建关系,其中:
- 字段名1:当前表用该字段来获取另一个表中的信息;
- 参数一 多方表名:一般将少的一方作为使用relationship方法的表模型,所以与其建立关系的另一个表模型就是多的一方;
- 参数二 backref:另一个表用其指定的”字段名2“来获取当前表中的信息;
- 可选参数 secondary:用于申明多对多关系,该参数用来指向我们通过Table类构建的关联关系表;
- 可选参数 uselist:用于申明一对一关系,该参数设为False能禁用列表查询,即当前表用字段1只能在另一个表中获取一条信息(1:1);默认为True,此时当前表用字段1能在另一个表中获取一个信息列表(1:n);
-
表模型实例化一个对象后,对象调用字段名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)
- 创建一对多关系,外键必须由相关联的两个表模型中,“多”对应的表模型添加;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'))
- 创建多对多关系,则需单独创建一个存储关联关系的表(不需要构建表模型类,直接用
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=app
和db.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.query.get(1) #获取User表中下标为1的对象,get方法必须指定下标
''' --> '''
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
你可能感兴趣的:(web学习笔记,flask,python,后端)