Flask:第三方组件
1 flask-session
第三方session
,替换falsk
内置的session
,支持保存到redis/memcached/file/Mongodb/SQLAIchemy
安装 pip3 install flask-session
flask-session
支持多种存储类,以redis
为例的基本用法,用RedisSessionInterface
类替换内置类,传入redis
对象和redis
存储name的前缀
from flask import Flask, session
from flask_session import RedisSessionInterface
import redis
app = Flask(__name__)
conn = redis.Redis(host='127.0.0.1', port=6379)
app.session_interface = RedisSessionInterface(redis=conn, key_prefix='zl')
@app.route('/')
def index():
session['user'] = 'lei'
return 'hello world'
if __name__ == '__main__':
app.run()
以redis
为例的常见用法:
from flask import Flask, session
from redis import Redis
from flask_session import Session
app = Flask(__name__)
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_KEY_PREFIX'] = 'zl'
app.config['SESSION_REDIS'] = Redis(host='127.0.0.1', port=6379)
# 本质跟上面的用法一样
# 类似的用法在flask中很常见,把app传给函数进行包装,做一些初始化的功能
Session(app)
@app.route('/')
def index():
session['user'] = 'lei'
return 'hello world'
if __name__ == '__main__':
app.run()
问题:设置cookie
时,如何设定关闭浏览器让cookie
失效?
在设置cookie时:
response.set_cookie('k', 'v', expries=None) # 默认就是 exipre=None,关闭浏览器失效
在flask-session中设置:
app.session_interface = RedisSessionInterface(redis=conn, key_prefix='zl'. permanent=Flase) # 设置permanent=Flase就可以了
那么如何通过一个参数的配置做到呢?我们来简单分析一下源码,RedisSessionInterface
类,初始化时,permanent
默认True
class RedisSessionInterface(SessionInterface):
serializer = pickle
session_class = RedisSession
def __init__(self, redis, key_prefix, use_signer=False, permanent=True):
if redis is None:
from redis import Redis
redis = Redis()
self.redis = redis
self.key_prefix = key_prefix
self.use_signer = use_signer
self.permanent = permanent
self.has_same_site_capability = hasattr(self, "get_cookie_samesite")
当调用open_session()
方法,实例化得到session
对象时,把permanent
参数传了进去
def open_session(self, app, request):
sid = request.cookies.get(app.session_cookie_name)
if not sid:
sid = self._generate_sid()
return self.session_class(sid=sid, permanent=self.permanent)
当调用save_session()
方法,最后set_cookie
时,会传入expries
参数,由此来判断cookie
设置
def save_session(self, app, session, response):
...
expires = self.get_expiration_time(app, session)
response.set_cookie(app.session_cookie_name, session_id,
expires=expires, httponly=httponly,
domain=domain, path=path, secure=secure,
**conditional_cookie_kwargs)
expries
变量的获取,来自get_expiration_time(app, session
方法,代码如下:
def get_expiration_time(self, app, session)
if session.permanent:
return datetime.utcnow() + app.permanent_session_lifetime
return None
判断当次请求session
的permanent
,如果是False就返回None。
问题:cookie
默认超时时间是多少?如何设置超时时间?
# 源码 expires = self.get_expiration_time(app, session)
def get_expiration_time(self, app, session)
if session.permanent:
return datetime.utcnow() + app.permanent_session_lifetime
# 源码
permanent_session_lifetime = ConfigAttribute(
"PERMANENT_SESSION_LIFETIME", get_converter=_make_timedelta
)
# 源码
def _make_timedelta(value):
if value is None or isinstance(value, timedelta):
return value
return timedelta(seconds=value)
配置:"PERMANENT_SESSION_LIFETIME" = timedelta(seconds=value)
"PERMANENT_SESSION_LIFETIME" = timedelta(days=value)
2 DButils 数据库连接池
flask中是没有ORM的,如果在flask里面连接数据库有两种方式:
pymysql
或SQLAlchemy
SQLAlchemy
是python 操作数据库的一个库,能够进行 orm
映射官方文档 sqlchemy
。SQLAlchemy
“采用简单的Python语言,为高效和高性能的数据库访问设计,实现了完整的企业级持久模型”。SQLAlchemy
的理念是,SQL数据库的量级和性能重要于对象集合;而对象集合的抽象又重要于表和行。
pymysql
直接连接数据库操作,有两个问题:
方式1 这种方式每次请求,反复创建数据库链接,多次链接数据库会非常耗时。 解决办法:放在全局,单例模式
import pymysql
from flask import Flask
app = Flask(__name__)
@app.route('/index')
def index():
# 链接数据库
conn = pymysql.connect(host="127.0.0.1",port=3306,user='root',password='123', database='pooldb',charset='utf8')
cursor = conn.cursor()
cursor.execute("select * from td where id=%s", [5, ])
result = cursor.fetchall() # 获取数据
cursor.close()
conn.close() # 关闭链接
print(result)
return "执行成功"
if __name__ == '__main__':
app.run(debug=True)
方式2 放在全局,如果是单线程,这样就可以,但是如果是多线程,就得加把锁。这样就成串行的了不支持并发,也不好。
import pymysql
from flask import Flask
from threading import RLock
app = Flask(__name__)
CONN = pymysql.connect(host="127.0.0.1",port=3306,user='root',password='123', database='pooldb',charset='utf8')
@app.route('/index')
def index():
with RLock:
cursor = CONN.cursor()
cursor.execute("select * from td where id=%s", [5, ])
result = cursor.fetchall() # 获取数据
cursor.close()
print(result)
return "执行成功"
if __name__ == '__main__':
app.run(debug=True)
所以我们选择用数据库连接池,既减少链接次数,也能支持并发,需要利用DButils
模块,是python用于实现数据库连接池的模块。
基于DButils实现的数据库连接池有两种模式:
模式一:PersistentDB
,为每一个线程创建一个链接(是基于本地线程来实现的。thread.local),每个线程独立使用自己的数据库链接,该线程关闭不是真正的关闭,本线程再次调用时,还是使用的最开始创建的链接,直到线程终止,数据库链接才关闭。我们不会用此模式,它违背了池的概念。
模式二:PooledDB
,创建一个链接池,为所有线程提供连接,使用时来进行获取,使用完毕后在放回到连接池。链接池里所有的链接都能重复使用,共享的, 即实现了并发,又防止一次性创建太多的链接次数。
import time
import pymysql
from threading import Tread
from dbutils.pooled_db import PooledDB
POOL = PooledDB(
creator=pymysql, # 使用链接数据库的模块
maxconnections=6, # 连接池允许的最大连接数,0和None表示不限制连接数
mincached=2, # 初始化时,链接池中至少创建的空闲的链接,0表示不创建
maxcached=5, # 链接池中最多闲置的链接,0和None不限制,超过的闲置连接先销毁,不一直占用资源
maxshared=3, # 链接池中最多共享的链接数量,0和None表示全部共享。PS: 无用,因为pymysql和MySQLdb等模块的 threadsafety都为1,所有值无论设置为多少,_maxcached永远为0,所以永远是所有链接都共享。
blocking=True, # 连接池中如果没有可用连接后,是否阻塞等待。True,等待;False,不等待然后报错
maxusage=None, # 一个链接最多被重复使用的次数,None表示无限制
setsession=[], # 开始会话前执行的命令列表。如:["set datestyle to ...", "set time zone ..."]
ping=0, # ping MySQL服务端,检查是否服务可用。# 如:0 = None = never, 1 = default = whenever it is requested, 2 = when a cursor is created, 4 = when a query is executed, 7 = always
host='127.0.0.1',
port=3306,
user='root',
password='123',
database='pooldb',
charset='utf8'
)
def func():
# 检测当前正在运行连接数的是否小于最大链接数,如果不小于则:等待或报raise TooManyConnections异常
# 否则优先去初始化时创建的链接中获取链接 SteadyDBConnection。
# 然后将SteadyDBConnection对象封装到PooledDedicatedDBConnection中并返回。
# 如果最开始创建的链接没有链接,则去创建一个SteadyDBConnection对象,再封装到PooledDedicatedDBConnection中并返回。
# 一旦关闭链接后,连接就返回到连接池让后续线程继续使用。
# PooledDedicatedDBConnection
conn = POOL.connection()
# print(th, '链接被拿走了', conn1._con)
# print(th, '池子里目前有', pool._idle_cache, '\r\n')
cursor = conn.cursor()
cursor.execute('select * from tb1')
result = cursor.fetchall()
time.sleep(2)
print(result)
conn.close()
if __name__ == '__main__':
for i in range(10):
t = Tread(target=func)
t.start()
flask中用DButils
实现数据库连接池
settings.py
把POOL做成单例模式,配置到flask的config中
from datetime import timedelta
from flask_session import Session
from redis import Redis
import pymysql
from dbutils.pooled_db import PooledDB, SharedDBConnection
class Config(object):
DEBUG = True
SECRET_KEY = 'sajhsjaljslajssqqqssaa'
PERMANENT_SESSION_LIFETIME = timedelta(days=31)
SERVER_TYPE = 'redis'
SESSION_KEY_PREFIX = 'zl'
PYMYSQL_POOL = PooledDB(
creator=pymysql,
maxconnections=6,
blocking=True,
host='127.0.0.1',
port=3306,
user='root',
password='123',
database='pooldb',
charset='utf8'
)
class ProductionConfig(Config):
SESSION_REDIS = Redis(host='192.168.10.29', port=6379)
class DevelopmentConfig(Config):
SESSION_REDIS = Redis(host='127.0.0.1', port=6379)
class TestingConfig(Config):
pass
utils/sql.py
, 把获取连接池连接、放回连接、执行sql等方法进行封装
import pymysql
from settings import Config
class SQLHelper(object):
@staticmethod
def open(cursor):
POOL = Config.PYMYSQL_POOL
conn = POOL.connection()
cursor = conn.cursor(cursor=cursor)
return conn, cursor
@staticmethod
def close(conn, cursor):
conn.commit()
cursor.close()
conn.close()
@classmethod
def fetch_one(cls, sql, args, cursor=pymysql.cursors.DictCursor):
conn, cursor = cls.open(cursor)
cursor.execute(sql, args)
obj = cursor.fetchone()
cls.close(conn, cursor)
return obj
@classmethod
def fetch_all(cls, sql, args, cursor=pymysql.cursors.DictCursor):
conn, cursor = cls.open(cursor)
cursor.execute(sql, args)
obj = cursor.fetchall()
cls.close(conn, cursor)
return obj
@classmethod
def execute(cls, sql, args, cursor=pymysql.cursors.DictCursor):
conn, cursor = cls.open(cursor)
cursor.execute(sql, args)
cls.close(conn, cursor)
在flask框架的视图文件中,导入封装的类,获取连接-->执行sql-->放回连接
from utils.sql import SQLHelper
obj = SQLHelper.fetch_one(sql="select * from users where name=%(user)s and pwd=%(pwd)s", args=form.data)
# args传一个列表
3 wtforms组件
WTForms是一个支持多个web框架的form
组件,主要用于对用户请求数据进行校验,还可以渲染标签。它的用法和django的forms组件一样,在定义FORM类的时候有些区别
安装 pip3 install wtforms
3.1 用户登录示例
View Code
from flask import Flask,render_template,request,redirect
from wtforms.fields import core
from wtforms.fields import html5
from wtforms.fields import simple
from wtforms import Form
from wtforms import validators
from wtforms import widgets
app = Flask(__name__,template_folder="templates")
class Myvalidators(object):
'''自定义验证规则'''
def __init__(self,message):
self.message = message
def __call__(self, form, field):
print(field.data,"用户输入的信息")
if field.data == "zl":
return None
raise validators.ValidationError(self.message)
class LoginForm(Form):
'''Form'''
name = simple.StringField(
label="用户名", # 标签名
widget=widgets.TextInput(), # input框为文本类型
# 校验规则直接写在字段内
validators=[
Myvalidators(message="用户名必须是zl"), # 也可以自定义正则
validators.DataRequired(message="用户名不能为空"), # 该字段必填
validators.Length(max=8,min=3,message="用户名长度必须大于%(max)d且小于%(min)d")
],
render_kw={"class":"form-control"} #设置属性,类似django的widget,可以使用bootsrtap样式
)
pwd = simple.PasswordField(
label="密码",
validators=[
validators.DataRequired(message="密码不能为空"),
validators.Length(max=8,min=3,message="密码长度必须大于%(max)d且小于%(min)d"),
validators.Regexp(regex="\d+",message="密码必须是数字"),
],
widget=widgets.PasswordInput(), # input框为密文类型
render_kw={"class":"form-control"}
)
@app.route('/login',methods=["GET","POST"])
def login():
if request.method =="GET":
form = LoginForm()
return render_template("login.html",form=form)
else:
form = LoginForm(formdata=request.form)
if form.validate(): # 相当于django的 form.is_valid() 校验数据
print("用户提交的数据用过格式验证,值为:%s"%form.data)
return "登录成功"
else:
print(form.errors,"错误信息")
return render_template("login.html",form=form)
if __name__ == '__main__':
# app.__call__()
app.run(debug=True)
login.html
<body>
<form action="" method="post" novalidate>
<p>{{ form.name.label }} {{ form.name }} {{ form.name.errors.0 }}</p>
<p>{{ form.pwd.label }} {{ form.pwd }} {{ form.pwd.errors.0 }}</p>
<input type="submit" value="提交">
<!--用户名:<input type="text">-->
<!--密码:<input type="password">-->
<!--<input type="submit" value="提交">-->
</form>
</body>
3.2 用户注册示例
View Code
from flask import Flask,render_template,redirect,request
from wtforms import Form
from wtforms.fields import core
from wtforms.fields import html5
from wtforms.fields import simple
from wtforms import validators
from wtforms import widgets
app = Flask(__name__,template_folder="templates")
app.debug = True
=======================simple===========================
class RegisterForm(Form):
name = simple.StringField(
label="用户名",
validators=[
validators.DataRequired()
],
widget=widgets.TextInput(),
render_kw={"class":"form-control"},
default="zl" # 默认值
)
pwd = simple.PasswordField(
label="密码",
validators=[
validators.DataRequired(message="密码不能为空")
]
)
pwd_confim = simple.PasswordField(
label="重复密码",
validators=[
validators.DataRequired(message='重复密码不能为空.'),
validators.EqualTo('pwd',message="两次密码不一致") # django校验密码用的全局钩子,wtforms直接EqualTo校验,更强大
],
widget=widgets.PasswordInput(),
render_kw={'class': 'form-control'}
)
========================html5============================
#注意这里用的是html5.EmailField,邮箱格式校验
email = html5.EmailField(
label='邮箱',
validators=[
validators.DataRequired(message='邮箱不能为空.'),
validators.Email(message='邮箱格式错误')
],
widget=widgets.TextInput(input_type='email'), # input框为邮箱类型
render_kw={'class': 'form-control'}
)
===================以下是用core来调用的=======================
# 单选,radio类型
gender = core.RadioField(
label="性别",
choices=(
(1,"男"),
(2,"女"),
),
coerce=int #限制是choicese第一个参数是int类型的
)
# 单选,下拉框, select类型
city = core.SelectField(
label="城市",
choices=(
("bj","北京"),
("sh","上海"),
)
)
# 多选,下拉框,多选的select类型
hobby = core.SelectMultipleField(
label='爱好',
choices=(
(1, '篮球'),
(2, '足球'),
),
coerce=int
)
# 多选,多选的checkbox类型
favor = core.SelectMultipleField(
label="喜好",
choices=(
(1, '篮球'),
(2, '足球'),
),
widget = widgets.ListWidget(prefix_label=False),
option_widget = widgets.CheckboxInput(), # input框为checkbox类型
coerce = int,
default = [1, 2] # 默认值
)
def __init__(self,*args,**kwargs): #这里的self是一个RegisterForm对象
'''重写__init__方法'''
super(RegisterForm,self).__init__(*args, **kwargs) #继承父类Form的init方法
self.favor.choices =((1, '篮球'), (2, '足球'), (3, '羽毛球')) #把RegisterForm这个类里面的favor重新赋值
def validate_pwd_confim(self,field,):
'''
自定义pwd_config字段规则,例:与pwd字段是否一致
:param field:
:return:
'''
# 最开始初始化时,self.data中已经有所有的值
if field.data != self.data['pwd']:
# raise validators.ValidationError("密码不一致") # 继续后续验证
raise validators.StopValidation("密码不一致") # 不再继续后续验证
@app.route('/register',methods=["GET","POST"])
def register():
if request.method=="GET":
form = RegisterForm(data={'gender': 1}) #默认是1,
return render_template("register.html",form=form)
else:
form = RegisterForm(formdata=request.form)
if form.validate(): #判断是否验证成功
print('用户提交数据通过格式验证,提交的值为:', form.data) #所有的正确信息
else:
print(form.errors) #所有的错误信息
return render_template('register.html', form=form)
if __name__ == '__main__':
app.run()
register.html
<body>
<h1>用户注册</h1>
<form method="post" novalidate style="padding:0 50px">
{% for item in form %}
<p>{{item.label}}: {{item}} {{item.errors[0] }}</p>
{% endfor %}
<input type="submit" value="提交">
</form>
</body>
3.3 meta
View Code
from flask import Flask, render_template, request, redirect, session
from wtforms import Form
from wtforms.csrf.core import CSRF
from wtforms.fields import core
from wtforms.fields import html5
from wtforms.fields import simple
from wtforms import validators
from wtforms import widgets
from hashlib import md5
app = Flask(__name__, template_folder='templates')
app.debug = True
class MyCSRF(CSRF):
"""
Generate a CSRF token based on the user's IP. I am probably not very
secure, so don't use me.
"""
def setup_form(self, form):
self.csrf_context = form.meta.csrf_context()
self.csrf_secret = form.meta.csrf_secret
return super(MyCSRF, self).setup_form(form)
def generate_csrf_token(self, csrf_token):
gid = self.csrf_secret + self.csrf_context
token = md5(gid.encode('utf-8')).hexdigest()
return token
def validate_csrf_token(self, form, field):
print(field.data, field.current_token)
if field.data != field.current_token:
raise ValueError('Invalid CSRF')
class TestForm(Form):
name = html5.EmailField(label='用户名')
pwd = simple.StringField(label='密码')
class Meta:
# -- CSRF
# 是否自动生成CSRF标签
csrf = True
# 生成CSRF标签name
csrf_field_name = 'csrf_token'
# 自动生成标签的值,加密用的csrf_secret
csrf_secret = 'xxxxxx'
# 自动生成标签的值,加密用的csrf_context
csrf_context = lambda x: request.url
# 生成和比较csrf标签
csrf_class = MyCSRF
# -- i18n
# 是否支持本地化
# locales = False
locales = ('zh', 'en')
# 是否对本地化进行缓存
cache_translations = True
# 保存本地化缓存信息的字段
translations_cache = {}
@app.route('/index/', methods=['GET', 'POST'])
def index():
if request.method == 'GET':
form = TestForm()
else:
form = TestForm(formdata=request.form)
if form.validate():
print(form)
return render_template('index.html', form=form)
if __name__ == '__main__':
app.run()
4 flask-script
Flask Script
扩展提供向Flask
插入外部脚本的功能,包括运行一个开发用的服务器,一个定制的Python shell
,设置数据库的脚本,cronjobs
,及其他运行在web应用之外的命令行任务;使得脚本和系统分开;
Flask Script
和Flask
本身的工作方式类似,只需定义和添加从命令行中被Manager
实例调用的命令;用于实现类似于django中 python3 manage.py runserver
这种命令。flask-script 官方文档
安装 pip3 install flask-script
4.1 创建并运行命令
首先,创建一个python模板运行命令脚本,文件名可起名为manage.py;
在该文件中,必须有一个Manager实例,Manager类追踪所有在命令行中调用的命令和处理过程的调用运行情况;
Manager只有一个参数——Flask实例,也可以是一个函数或其他的返回Flask实例;
# manage.py文件
from flask_script import Manager
from flask import Flask
app = Flask(__name__)
manager = Manager(app)
...
if __name__ == '__main__':
manager.run()
启动flask框架:python3 manage.py runserver
,manage.py是脚本文件名
4.2 自定制命令
方式一:使用Command实例的@command修饰符
启动custom命令并必须传参,custom函数里写命令的执行逻辑。如创建类似python manage.py createsuperuser
命令,函数不接收参数,函数代码逻辑需要input接收用户输入的信息,并往数据库插入一条数据。
from flask_script import Manager
from flask import Flask
app = Flask(__name__)
manager = Manager(app)
@manager.command
def custom(args):
"""
自定制命令
python manage.py custom 123
:param args:
:return:
"""
print(args)
if __name__ == '__main__':
manager.run()
方式二:使用Command实例的@option修饰符
可以有多个@option选项参数;flask框架没有ORM,可以自定制初始化数据库的命令。
from flask_script import Manager
from flask import Flask
app = Flask(__name__)
manager = Manager(app)
#命令既可以用-n,也可以用--name,dest="name"用户输入的命令的名字作为参数传给了函数中的name
#命令既可以用-u,也可以用--url,dest="url"用户输入的命令的url作为参数传给了函数中的url
@manager.option('-n', '--name', dest='name', help='Your name', default='world')
@manager.option('-u', '--url', dest='url', default='www.baidu.com')
def cmd(name, url):
"""
自定制命令
执行: python manage.py cmd -n zl -u http://www.baidu.com
执行: python manage.py cmd --name zl --url http://www.baidu.com
:param name:
:param url:
:return:
"""
print(name, url)
if __name__ == '__main__':
manager.run()
应用:flask自定制命令,把excle的数据批量导入数据库。
python manage.py insertdb -f user.xlsx -t user
执行这条命令,把user.xlsx文件的数据插入数据库user表。
insertdb
函数接收user.xlsx
和user
两个参数,利用openpyxl
模块把user.xlsx
文件打开读出里面的数据,插入到user表。