Flask-3实战高阶(下)

498 阅读9分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第30天,点击查看活动详情

服务操作命令

systemctl start redis.service  #启动redis服务

systemctl stop redis.service  #停止redis服务

systemctl restart redis.service  #重新启动服务

systemctl status redis.service  #查看服务当前状态

systemctl enable redis.service  #设置开机自启动

systemctl disable redis.service  #停止开机自启动

Redis 基础知识

www.runoob.com/redis/redis…

五种数据类型

  • string 字符串
  • hash 哈希(键值对)
  • list 列表
  • set 集合
  • zset 有序集合(常用于排序)
SET name value

GET value

虚拟环境安装 redis 插件

(myblog) [root@izbp1i7e0dqxcb89vkdgc3z ~]# pip install redis

Flask-caching

为了尽量减少缓存穿透,同时减少web的响应时间,可以针对那些需要一定时间才能获取结果的函数和那些不需要频繁更新的视图函数提供缓存服务,可以在一定的时间内直接返回结果而不是每次都需要计算或者从数据库中查找

(myblog) [root@izbp1i7e0dqxcb89vkdgc3z ~]# pip install flask-caching

引入项目

ext/__init__.py

from flask_caching import Cache

cache = Cache()

apps/__init__.py

......
from ext import db, bootstrap, cache

config = {
    'CACHE_TYPE': 'redis',
    'CACHE_REDIS_HOST': '127.0.0.1',
    'CACHE_REDIS_PORT': 6379
}
# 配置 redis 数据库

def create_app():
    app = Flask(__name__, template_folder='../templates', static_folder='../static')
    app.config.from_object(settings.ProductionConfig)
    cache.init_app(app=app, config=config)
    # 初始化缓存文件
......

缓存键值对

设置:

cache.set(key,value,timeout=second)
cache.set_many([key,value],[key,value].......)

获取:

cache.get(key)
cache.get_many(key1,key2......)

删除:

cache.delete(key)
cache.delete_many(key1,key2......)
cache.clear()

手机验证码就可以使用 缓存键值对的形式 保存校验

缓存视图函数

@user_bp.route('/', endpoint='index')
@cache.cached(timeout=50)
def index():
    ......
    
# @cache.cached(timeout=50)
timeout 设置过期时间

直接加到对应路由上,适用于某一页面内容较多刷新速度较慢时

与Flask-caching类似的插件:Flask-cache(较少用)

WTForms

开发文档:www.pythondoc.com/flask-wtf/

Flask-WTF是集成WTForms,并带有 CSRF 令牌的安全表单和全局的 CSRF 保护功能,在建立表单所创建的类都是继承 Flask_wtf 中的 FlaskForm,而 FlaskForm 是继承 WTForms 中的 forms

功能

  • 集成 wtforms。
  • 带有 csrf 令牌的安全表单。
  • 全局的 csrf 保护。
  • 支持验证码(Recaptcha)。
  • 与 Flask-Uploads 一起支持文件上传。
  • 国际化集成。

安装

[root@izbp1i7e0dqxcb89vkdgc3z ~]# pip install Flask-WTF

表单

使用方式类似于数据库

只提供表单所以要加 form 标签

使用 wtform 必须设置 SECRET_KEY

标准表单字段

TextField 		代表<input type ='text'> HTML表单元素
IntegerField	用于显示整数的TextField
TextAreaField 	代表<testarea> html表单元素
PasswordField 	代表<input type ='password'> HTML表单元素
SubmitField 	表示<input type ='submit'>表单元素
SelectField 	表示选择表单元素
StringField
PasswordField
DecimalField
BooleanField
DatetimeField
......

常用验证器

DataRequired 	检查输入栏是否为空
mail 			检查字段中的文本是否遵循电子邮件ID约定
IPAddress 		验证输入字段中的IP地址
Length 			验证输入字段中字符串的长度是否在给定范围内
NumberRange 	在给定范围内的输入字段中验证一个数字
URL 			验证输入字段中输入的URL
EqualTO


# 添加方法 
name = TextField("Name Of Student",[validators.Required("Please enter your name.")])

使用

  1. 引入 CSRF
# __init__.py
from flask_wtf import CSRFProtect

def create_app():
    ......
    CSRFProtect.init_app(app=app)
    ......
  1. 定义 Form.py
from flask_wtf import Form, validators
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired, Length


class UserForm(Form):
    username = StringField('name', validators=[DataRequired()])
    password = PasswordField('password', validators=[Length(min=6, max=20, message='长度必须在6~20位之间')])

    # 此表有 username和password两个字段,类似于sqlalchemy
  1. 使用

视图函数

@article_bp1.route('form', methods=["GET", "POST"], endpoint='form')
def form():
    userform = UserForm()
    # 进行校验
    if userform.validate_on_submit():
        return 'ok'
    return render_template('article/form.html', userform=userform)

模板中

<form method="POST" action="{{ url_for('article.form')}}">
    {{ userform.csrf_token }}
    {#  防止csrf,必须设置secret_key  #}
    {{ userform.username }}{% if userform.username.errors %}{{ userform.username.errors.0 }}{% endif %}
    {#  如果有报错则输出报错的message, .0表示只输出内容  #}
    {{ userform.password }}{% if userform.password.errors %}{{ userform.password.errors.0 }}{% endif %}
    <input type="submit" value="Go">
</form>

#如果模板中存在表单,你不需要做任何事情。与之前一样:
<form method="post" action="/">
    {{ form.csrf_token }}
</form>

# 但是如果模板中没有表单,你仍然需要一个 CSRF 令牌:

<form method="post" action="/">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
</form>

image-20210130093544251

自定义验证

# form.py
class UserForm(FlaskForm):
    username = StringField(label='用户名', validators=[DataRequired(), Length(min=6, max=20, message='长度必须在6~20位之间')])
    password = PasswordField(label='密码', validators=[DataRequired(), Length(min=6, max=20, message='长度必须在6~20位之间')])
    confirm_password = PasswordField('确认密码', validators=[DataRequired(), Length(min=6, max=20, message='长度必须在6~20位之间'),EqualTo('password', '密码不一致')])
    phone = StringField('手机号', validators=[DataRequired(), Length(min=11, max=11, message='长度必须是11位')])
    email = EmailField('邮箱', validators=[DataRequired()])

    def validate_username(self, data):
        # validata_(与上边定义的字段名相同)
        if self.username.data[0].isdigit():
            raise ValidationError('用户名不能以数字开头')

    def validate_phone(self, data):
        phone = data.data
        # 如果匹配上就返回一个对象,没匹配就返回 None
        if not re.search(r'^1[356789]\d{9}$', phone):
            raise ValidationError('手机号格式不正确')
            # 注意使用 raise 关键字而非 return 

image-20210130093744991

form.html

<form method="POST" action="{{ url_for('article.form')}}">
    {{ userform.csrf_token }}
    {#  防止csrf,必须设置secret_key  #}
    {{ userform.username.label }}:{{ userform.username }}{% if userform.username.errors %}{{ userform.username.errors.0 }}{% endif %}<br>
    {#  如果有报错则输出报错的message, .0表示只输出内容  #}
    {{ userform.password.label }}:{{ userform.password }}{% if userform.password.errors %}{{ userform.password.errors.0 }}{% endif %}<br>
    {{ userform.confirm_password.label }}:{{ userform.confirm_password }}{% if userform.confirm_password.errors %}{{ userform.confirm_password.errors.0 }}{% endif %}<br>
    {{ userform.phone.label }}:{{ userform.phone }}{% if userform.phone.errors %}{{ userform.phone.errors.0 }}{% endif %}<br>
    {{ userform.email.label }}:{{ userform.email }}{% if userform.email.errors %}{{ userform.email.errors.0 }}{% endif %}<br>

    <input type="submit" value="Go">

文件上传

FileField

接受一个 FileStorage

  1. 定义form

    使用 FileField,如果要指明上传类型需要使用:FileAllowed(['jpg','png'])

  2. 模板中的使用和其它类型字段一致,但是必须在 form 上面加:enctype=“multipart/form-data”

  3. 视图函数中如果验证成功

    file=uform.data

验证码

  1. 使用 Flask-WTF 内置验证码

recaptcha

使用 Google 验证器,国内不太友好

  1. pillow

安装

pip install -i https://pypi.douban.com/simple/  pillow

使用

utils/captcha.py

import os
import random
from PIL import Image, ImageFont, ImageDraw


# 生成随机颜色,返回 rgb 元组
def get_random_color():
    return (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))


# 生成验证码图片
def generate_image(length):
    s = 'qazwPLMTRDXZSENBHUJCFhnujmikol098IYGNBHUJIYGV12NBHUJIYGVCF365478WAQ'
    size = (130, 50)
    # 创建画布
    im = Image.new('RGB', size, color=get_random_color())
    # 创建字体
    font = ImageFont.truetype('simfang.ttf', size=30)
    print(os.path.curdir)
    # 创建 ImageDraw 对象
    draw = ImageDraw.Draw(im)
    # 绘制验证码
    code = ''
    for i in range(length):
        c = random.choice(s)
        code += c
        draw.text((5 + random.randint(2, 15) + 20 * i, random.randint(2, 7)), text=c, fill=get_random_color(),
                  font=font)

    # 绘制干扰线
    for i in range(8):
        x1 = random.randint(0, 130)
        y1 = random.randint(0, 50 / 2)
        x2 = random.randint(0, 130)
        y2 = random.randint(50 / 2, 50)

        draw.line(((x1, y1), (x2, y2)), fill=get_random_color())
    return im, code

user/view.py

@user_bp.route('/image')
def get_image():
    im, code = generate_image(4)
    session['valid'] = code
    # 将 image 对象转换成二进制
    buffer = BytesIO()
    im.save(buffer, 'JPEG')
    buf_bytes = buffer.getvalue()
    response = make_response(buf_bytes)
    response.headers['Content-Type'] = 'image/jpg'
    return response

注意:字体文件,部署服务器使也需要设置,可以使用命令查找

[root@izbp1i7e0dqxcb89vkdgc3z sbin]# find  / -name *.ttf 
/usr/share/fonts/dejavu/DejaVuSansCondensed-BoldOblique.ttf
/usr/share/fonts/dejavu/DejaVuSans-ExtraLight.ttf
/usr/share/fonts/dejavu/DejaVuSansCondensed.ttf
/usr/share/fonts/dejavu/DejaVuSans-BoldOblique.ttf
.....

bootstrap结合flask-wtf

{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}


{% block mycontent %}

    <form action="{{ url_for('user.form02') }}" method="post" enctype="multipart/form-data">
    {{ wtf.quick_form(uform,button_map={'submit_button':'primary'},horizontal_columns=('sm',1,1)) }}
    </form>

{% endblock %}

在大多数flask教程中,都会介绍使用WTForms和bootstrap,使用起来确实比较方便。方法大致如下:

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired

class LoginForm(FlaskForm):
    name = StringField('姓名', validators=[DataRequired()])
    password = PasswordField('密码', validators=[DataRequired()])
    submitfield = SubmitField('提交')
12345678

在模版中各表单项分开渲染:

<form id="loginform" action="/login" method="post">
	{{ form.hidden_tag() }}
	{{ form.name.label }}{{ form.name }}
	{{ form.password.label }}{{ form.password }}
	{{ form.submitfield() }}
</form>
123456

或者使用quick_form()渲染:

{% import 'bootstrap/wtf.html' as wtf %}
{{ wtf.quick_form(form) }}
12

对于WTForms大概也是如此介绍,一般情况下也能用。但是如果我想把表单的检验放在前端呢?如何下手?渲染时若使用第一种方法,那还可以在<form>中设置id、action之类的;但若使用的是quick_form()呢?你想修改一点什么东西都无从下手。百度中也很少有人提及这个问题,查了不少资料,总算摸出点头绪来,解决了自己的问题。

在各种Field的定义中,可以加入一个render_kw。这是一个字典,平常HTML各种定义都可以放在里边。比如:

class LoginForm(FlaskForm):
    name = StringField('姓名', render_kw={"id":"name", "placeholder":"请输入用户名"})
    password = PasswordField('密码', render_kw={"id":"pwd", "placeholder":"请输入密码"})
    submitfield = SubmitField('提交', render_kw={"type": "button", "onclick":"alert('提交')"})
1234

这样子就可以在点击提交按钮后调用自定义的js函数进行检验,若满足条件再submit()。(上例中只是使用alert()弹出一个窗口。注意:在SubmitField中,需要加上"type": "button",要不然就算js函数中检验未通过不想submit(),它最终依然还是会自己提交表单。----其实加上"type": "button"的SubmitField,已经可以当作普通的button来使用了。)但是在前端中调用submit()需要用到form对象,而获取form对象需要定义form的id,再使用var form = document.getElementById("loginform");。但quick_form(form)渲染时是没有id的。这就需要在quick_form()中加入一些参数:

{{ wtf.quick_form(form, id='loginform', action='/login', form_type='horizontal', horizontal_columns=('lg',4,4)) }}

id和action意义很明显了。form_type是指定按什么样的方式渲染form的,horizontal_columns是指定一行几列,这两参数都与bootstrap有关,在原代码中看不太懂,大家修改做下测试,一般能满足到你的要求的。

闪现

message flash

Flask 使用闪现系统向用户反馈信息

flash 内容默认存储到 session 中,所以要提前设置 SECRET_KEY

index.html

{% extends 'base.html' %}

{% block middle %}

    {% with messages = get_flashed_messages() %}
        <ul>
            {% for message in messages %}
                <li>
                    {% if message %}
                        {{ message }}
                    {% endif %}
                </li>
            {% endfor %}
        </ul>
    {% endwith %}

{% endblock %}

user/view.py

@user_bp.route('/login', methods=['get', 'post'], endpoint='login')
def login():
    if request.method == "POST":
        username = request.form.get('username')
        if username == 'admin':
            flash('验证成功!1')
            flash('验证成功!2')
            flash('验证成功!3')
            return render_template('user/index.html')
    else:
        return render_template('user/login.html')

分类闪现

三种类型

  • message(未指定默认为 message)
  • error
  • warning
  • info

接受列表元组

@user_bp.route('/login', methods=['get', 'post'], endpoint='login')
def login():
    if request.method == "POST":
        username = request.form.get('username')
        if username == 'admin':
            flash('验证成功!1', 'info')
            flash('验证成功!2', 'warning')
            flash('验证成功!3', 'error')
            return render_template('user/index.html')

index.html

{% block middle %}

    {% with messages = get_flashed_messages(with_categories=True) %}
        <ul>
            {% for category,message in messages %}
                <li class="{{ category }}">
                    {% if message %}
                        {{ message }}
                    {% endif %}
                </li>
            {% endfor %}
        </ul>
    {% endwith %}

{% endblock %}
  1. 在一个请求结束的时候添加 flash

  2. 在当前请求中渲染获取或者是下一个请求中可以获取,其他不可以

    使用 redirect 依然可以接收到闪现的信息,但第三次请求就接受不到了

    flash('验证成功!1', 'info')
    return redirect(url_for('user.index'))
    
  3. 获取闪现内容

    get_flash_messages(whit_categories=True)
    

过滤闪现消息

可选,有针对性地获取对应类型的闪现消息

get_flash_messages(category_filter=['error'])

日志记录

python logging模块

级别排序:CRITICAL > ERROR > WARNING > INFO > DEBUG

import logging  # 引入logging模块
logging.basicConfig(level=logging.DEBUG,format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')  
# logging.basicConfig函数对日志的输出格式及方式做相关配置
# 由于日志基本配置中级别设置为DEBUG,所以一下打印信息将会全部显示在控制台上

......

日志总结

uwsgi --> uwsgi.log

  1. 使用 app 自带

    app.logger.info('')
    app.logger.debug('')
    app.logger.warning('')
    app.logger.error('')
    
  2. 通过 logging 进行创建

    import logging
    logger = logging.getLogger('name')
    # 默认 flask 的名字叫 app
    

保存到文件

  1. logging.basicConfig(level=logging.DEBUG,format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')  
    
  2. import logging
    
    logger = logging.getLogger(__name__)
    logger.setLevel(level=logging.INFO)
    handler = logging.FileHandler('log.txt')
    handler.setLevel(logging.INFO)
    formatter = logging.Formatter('%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    
  3. 使用 logger.info('message')
    

获取闪现内容

get_flash_messages(whit_categories=True)

过滤闪现消息

可选,有针对性地获取对应类型的闪现消息

get_flash_messages(category_filter=['error'])

日志记录

python logging模块

级别排序:CRITICAL > ERROR > WARNING > INFO > DEBUG

import logging  # 引入logging模块
logging.basicConfig(level=logging.DEBUG,format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')  
# logging.basicConfig函数对日志的输出格式及方式做相关配置
# 由于日志基本配置中级别设置为DEBUG,所以一下打印信息将会全部显示在控制台上

......

日志总结

uwsgi --> uwsgi.log

  1. 使用 app 自带

    app.logger.info('')
    app.logger.debug('')
    app.logger.warning('')
    app.logger.error('')
    
  2. 通过 logging 进行创建

    import logging
    logger = logging.getLogger('name')
    # 默认 flask 的名字叫 app
    

保存到文件

  1. logging.basicConfig(level=logging.DEBUG,format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')  
    
  2. import logging
    
    logger = logging.getLogger(__name__)
    logger.setLevel(level=logging.INFO)
    handler = logging.FileHandler('log.txt')
    handler.setLevel(logging.INFO)
    formatter = logging.Formatter('%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    
  3. 使用 logger.info('message')