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

判断当次请求sessionpermanent,如果是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里面连接数据库有两种方式:

pymysqlSQLAlchemy
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 ScriptFlask本身的工作方式类似,只需定义和添加从命令行中被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.xlsxuser两个参数,利用openpyxl模块把user.xlsx文件打开读出里面的数据,插入到user表。

 

posted @ 2022-10-09 23:41  不会钓鱼的猫  阅读(199)  评论(0编辑  收藏  举报