整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

Flask博客实战 - 使用 WTForms 进行表

Flask博客实战 - 使用 WTForms 进行表单验证

一章节我们通过在html中直接编写表单的方式进行数据传递,并且在视图中对前端传递的数据进行了简单的认证,但是如果把验证数据的代码与逻辑混合在一起,将使得视图的代码不够清晰,并且难以维护,稍加疏忽就会产生验证漏洞,如果细心的同学其实可以发现,在之前的登录注册中我们一直没有对空表单进行验证,当然这是我故意为之,但如果在生产环境,这将是一个灾难的开始,所以,在编程中无论是前端还是后端都要求要对数据进行验证,作为后端,更要保持一种永远不相信前端传递数据的态度去做数据校验。

本章节我们将使用Flask官方推荐的Flask-WTF扩展来重构我们的登录注册表单!

关于Flask-WTF

Flask-WTF是Flask 和 WTForms 的简单集成,包括 CSRF、文件上传和 reCAPTCHA。

  • 安装Flask-WTF:
pip install Flask-WTF

创建登录注册表单类

app/auth/目录下新建一个forms.py的文件,所有的表单验证代码都放到这个文件当中!

构建登录表单

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired, Length, ValidationError, EqualTo
from werkzeug.security import check_password_hash
from .models import User

class LoginForm(FlaskForm):
    # 登录表单

    def qs_username(username):
        # 对该字段进行在传递之前处理
        u=f'{username}123456'
        print(u)
        return username

    username=StringField('username', validators=[
        DataRequired(message="不能为空"), 
        Length(max=32, message="不符合字数要求!")
        ], filters=(qs_username,))
    password=PasswordField('password', validators=[
        DataRequired(message="不能为空"),
        Length(max=32, message="不符合字数要求!")
        ])

    def validate_username(form, field):
        user=User.query.filter_by(username=field.data).first()
        if user is None:
            error='该用户不存在!'
            raise ValidationError(error)
        elif not check_password_hash(user.password, form.password.data):
            raise ValidationError('密码不正确')

代码详解:

class LoginForm(FlaskForm): 创建了一个登录表单类,继承了FlaskForm类

StringField, PasswordField

这些都是wtforms内置的字段,负责呈现和数据转换。

官方文档:https://wtforms.readthedocs.io/en/3.0.x/fields/

他继承自Filed的基类,其中有一些比较重要的参数我们大概在这里理解一下!

第一个字符串其实是该类的label参数,字段的标签,也就是转换到html中的label!

validators传入对该字段的一些验证器,在提交数据之前对数据进行验证!

filters这个参数比较特殊,官方文档并没有对其详细说明,只说是筛选器,其实怎么说就是在额外的方法中对该字段的值提前处理过滤,元组中的每个值都是一个回调函数,函数不需要传入括号,但这个回调函数默认有一个参数,这个参数就是本身该字段的值,所以在定义该函数时就必须传入一个参数!例如:我们定义username之前定义的这个方法!

def qs_username(username):
    # 对该字段进行在传递之前处理
    u=f'{username}123456'
    print(u)
    return username

备注:必须返回处理后的这个参数,否则会触发DataRequired验证器,后端获取不到该表单的值!

  • DataRequired, Length 这是内置的验证器,第一个是验证字段是否为空,第二个Length是验证输入长度,当然内置的还有很多,这里就不一一列举,具体我们可参考文档!

官方文档: https://wtforms.readthedocs.io/en/3.0.x/validators/#custom-validators

自定义验证用户名和密码

在之前的视图函数中我们对用户名和密码都做了校验,现在我们需要把验证的代码全部移动到表单类中,代码如下:

def validate_username(form, field):
    user=User.query.filter_by(username=field.data).first()
    if user is None:
        error='该用户不存在!'
        raise ValidationError(error)
    elif not check_password_hash(user.password, form.password.data):
        raise ValidationError('密码不正确')
  • validate_username(form, field)

这个函数的写法是固定的validate_{filed},validate_后边的filed是指你需要验证的某个字段名,比如我们这个验证,他主要就是对username字段进行验证,这个函数中参数的filed就是这个字段,通过field.data就可以获取到usernam的值。form参数则指代的是整个表单,可以用form.{filed}.data的方式获取表单类中某个具体字段的值!

构建注册表单

当了解了登录表单后,我们完全就可以参照登录表单去实现注册表单,代码如下:

路径:app/auth/forms.py

class RegisterForm(FlaskForm):
    # 注册表单
    username=StringField('username', validators=[
        DataRequired(message="不能为空"), 
        Length(min=2, max=32, message="超过限制字数!")
        ])
    password=PasswordField('password', validators=[
        DataRequired(message="不能为空"),
        Length(min=2, max=32, message="超过限制字数!"),
        EqualTo('password1', message='两次密码输入不一致!')
        ])
    password1=PasswordField('password1')

    def validate_username(form, field):
        user=User.query.filter_by(username=field.data).first()
        if user is not None:
            error='该用户名称已存在!'
            raise ValidationError(error)

这里唯一需要注意的是两次密码是否输入一致,我们用了一个内置的验证器EqualTo,使用方式可完全参照代码,他会自动校验password和password1输入的值是否一致!

重构登录和注册视图

  • 路径:app/auth/views/auth.py
from ..forms import LoginForm, RegisterForm

@bp.route('/login', methods=['GET', 'POST'])
def login():
    # 登录视图
    # form=LoginForm(meta={'csrf': False}) # 禁用csrf
    form=LoginForm()
    if form.validate_on_submit():
        user=auth.User.query.filter_by(username=form.username.data).first()
        session.clear()
        session['user_id']=user.id
        return redirect(url_for('index'))
    return render_template('login.html', form=form)


@bp.route('/register', methods=['GET', 'POST'])
def register():
    # 注册视图
    form=RegisterForm()
    if form.validate_on_submit():
        user=auth.User(username=form.username.data, password=generate_password_hash(form.password.data))
        db.session.add(user)
        db.session.commit()
        session.clear()
        session['user_id']=user.id
        return redirect(url_for('index'))
    return render_template('register.html', form=form)

1、首先从forms.py中引入了我们定义的登录(LoginForm)和注册(RegisterForm)表单类!

2、form=RegisterForm() 实例化表单类

3、if form.validate_on_submit(): 验证前端传递的数据是否有效,并且会自动判断是POST请求还是GET请求!

4、 数据验证通过则进入之后的逻辑,未验证通过则返回我们在表单类中传入的验证提示!

模板中调用验证信息

我们以调用username字段的验证提示为例,在模板中加入这段代码即可获得错误提示!

<!-- 表单验证 -->
{% if form.username.errors %}
<b-message type="is-danger">
    <ul class="errors">
        {% for error in form.username.errors %}
            <li>{{ error }}</li>
        {% endfor %}
    </ul>
</b-message>
{% endif %}

重构登录注册html模板

路径:app/auth/templates/login.html 以登陆表单为例,代码如下:

{% block auth_form %}
    <form action="" method="post" style="margin-top: 40%;" class="box">
        <div class=" has-text-centered mb-3">
          <p class=" subtitle">登录</p>
          <h1 class="title">FlaskBlog</h1>
        </div>
        {{ form.csrf_token }}
        <!-- 消息闪现 -->
        {% with messages=get_flashed_messages() %}
        <b-message type="is-danger">
          {% if messages %}
          <ul class=flashes>
            {% for message in messages %}
            <li>{{ message }}</li>
            {% endfor %}
          </ul>
          {% endif %}
        </b-message>
        {% endwith %}

        <!-- 表单验证 -->
        {% if form.username.errors %}
        <b-message type="is-danger">
          <ul class="errors">
            {% for error in form.username.errors %}
            <li>{{ error }}</li>
            {% endfor %}
          </ul>
        </b-message>
        {% endif %}

        <div class="field">
          <p class="control has-icons-left has-icons-right">
            {{ form.username(class='input', placeholder='Username') }}
            <span class="icon is-small is-left">
              <i class="fas fa-envelope"></i>
            </span>
            <span class="icon is-small is-right">
              <i class="fas fa-check"></i>
            </span>
          </p>
        </div>
        <div class="field">
          <p class="control has-icons-left">
            {{ form.password(class='input', placeholder='Password') }}
            <span class="icon is-small is-left">
              <i class="fas fa-lock"></i>
            </span>
          </p>
        </div>
        <div class="field">
          <p class="control">
            <input class="button is-success is-fullwidth" type="submit" value="Login">
          </p>
        </div>
      </form>
    {% endblock auth_form %}

{{ form.csrf_token }} 隐式的创建一个csrftoken的表单

{{ form.username(class='input', placeholder='Username') }} 这样就可以直接获得一个表单html并自动渲染,向该表单增加书香的方式就是像代码中这样传入参数和值即可,当然也可以提前在表单类中定义!

剩下的注册表单,就当是给大家留作的一个作业,大家自行去参照登录表单完善重构一下,加油哦!我相信你可以!

到这里我们的表单验证就大概了解了,之后的章节就是基本的增删改查以及表单验证,都是基于我们这些章节学习的知识点,所以之后的章节就不会过多的去讲解每行代码的意思,重心放在逻辑的展示上,如果基础较差的同学,到这里,可以去反复的把前边所有章节的内容去练习,写代码其实就是写的多了就会了,也就理解了,练习 练习 再练习!

者:俊欣

来源:关于数据分析与可视化

今天小编带领大家用Python自制一个自动生成探索性数据分析报告这样的一个工具,大家只需要在浏览器中输入url便可以轻松的访问,如下所示

第一步

首先我们导入所要用到的模块,设置网页的标题、工具栏以及logo的导入,代码如下

from st_aggrid import AgGrid
import streamlit as st
import pandas as pd
import pandas_profiling
from streamlit_pandas_profiling import st_profile_report
from pandas_profiling import ProfileReport
from  PIL import Image

st.set_page_config(layout='wide') #Choose wide mode as the default setting

#Add a logo (optional) in the sidebar
logo=Image.open(r'wechat_logo.jpg')
st.sidebar.image(logo,  width=120)

#Add the expander to provide some information about the app
with st.sidebar.expander("关于这个项目"):
     st.write("""
        该项目是将streamlit和pandas_profiling相结合,在您上传数据集之后自动生成相关的数据分析报告,当然该项目提供了两种模式 全量分析还是部分少量分析,这里推荐用部分少量分析,因为计算量更少,所需要的时间更短,效率更高
     """)

#Add an app title. Use css to style the title
st.markdown(""" <style> .font {                                          
    font-size:30px ; font-family: 'Cooper Black'; color: #FF9633;} 
    </style> """, unsafe_allow_html=True)
st.markdown('<p class="font">请上传您的数据集,该应用会自动生成相关的数据分析报告</p>', unsafe_allow_html=True)

output

上传文件以及变量的筛选

紧接的是我们需要上传csv文件,代码如下

uploaded_file=st.file_uploader("请上传您的csv文件: ", type=['csv'])

我们可以选择针对数据集当中所有的特征进行一个统计分析,或者只是针对部分的变量来一个数据分析,代码如下

if uploaded_file is not None:
     df=pd.read_csv(uploaded_file)
     option1=st.sidebar.radio(
          '您希望您的数据分析报告中包含哪些变量呢',
          ('所有变量', '部分变量'))

     if option1=='所有变量':
          df=df

     elif option1=='部分变量':
          var_list=list(df.columns)

要是用户勾选的是部分变量,只是针对部分变量来进行一个分析的话,就会弹出来一个多选框来供用户选择,代码如下

var_list=list(df.columns)
option3=st.sidebar.multiselect(
     '筛选出您希望在数据分析报告中包含的变量',
     var_list)
df=df[option3]

用户可以挑选到底是“简单分析”或者是“完整分析”,要是勾选的是“完整分析”的话,会跳出相应的提示,提示“完整分析”由于涉及到更加复杂的计算操作,耗时更加地长,要是遇到大型的数据集,还会有计算失败的情况出现

 option2=st.sidebar.selectbox(
      '筛选模式,完整分析还是简单分析',
      ('简单分析', '完整分析'))

 if option2=='完整分析':
      mode='complete'
      st.sidebar.warning(
           '完整分析由于涉及到更加复杂的计算操作,耗时更加地长,要是遇到大型的数据集,还会有计算失败的情况出现,这里推荐使用简单分析')
 elif option2=='简单分析':
      mode='minimal'
      grid_response=AgGrid(
           df,
           editable=True,
           height=300,
           width='100%',
      )

      updated=grid_response['data']
      df1=pd.DataFrame(updated)

当用户点击“生成报告”的时候就会自动生成一份完整的数据分析报告了,代码如下

if st.button('生成报告'):
        if mode=='complete':
            profile=ProfileReport(df,
                title="User uploaded table",
                progress_bar=True,
                dataset={
                    "简介": '欢迎关注公众号:关于数据分析与可视化',
                    "作者": '俊欣',
                    "时间": '2022.05'
                })
            st_profile_report(profile)
        elif mode=='minimal':
            profile=ProfileReport(df1,
                minimal=True,
                title="User uploaded table",
                progress_bar=True,
                dataset={
                    "简介": '欢迎关注公众号:关于数据分析与可视化',
                    "作者": '俊欣',
                    "时间": '2022.05'
                })
            st_profile_report(profile)

最后出来的结果如下,这里再来显示一遍

取网页图片的基本流程是:

1 使用urllib.request模板请求返回网页文本;

2 从网页文本中使用正则表达式筛选出img src地址(返回一个全部src的列表);

3 图片文件逐一检索或复制;

代码:

运行效果:

附代码1:

import re

import urllib.request

import os

#1 抓取网页

#url='http://www.kgc.cn/list'

url='http://www.ttpaihang.com/vote/rank.php?voteid=1410&page=2'

req=urllib.request.urlopen(url)

buf=req.read()

req.close()

#2 获取图片地址

i=url.find("/",9) # 本句及下面三句截取url的前半截

url2=url

if i > 0 :

....url2=url[:i]

#buf=buf.decode('UTF-8')

buf=buf.decode('gb2312')

#listurl=re.findall(r'http:.[^"]+\.jpg',buf)

listurl=re.findall(r'img src=.[^"]+\.jpg',buf)

for i in range(len(listurl)):.... # 把字符img src="去掉

....listurl[i]=listurl[i].replace('img src="',"")

....if not re.match("http",listurl[i]):

........listurl[i]=url2 + listurl[i]

....print(listurl[i])

#3 抓取图片并保存到本地

i=0

fpath="D:\pic2\"

if not os.path.isdir(fpath):

....os.mkdir(fpath)

for url in listurl:

....f=open(fpath + str(i)+'.jpg','wb')

....req=urllib.request.urlopen(url)

....buf=req.read()

....f.write(buf)

....f.close()

....i+=1

........

附代码2(写成函数的形式)

import re .... .... .... .... # 正则表达式

import urllib.request .... .... # 从服务器请求返回资源

import os .... .... .... .... # 文件和目录操作

import socket .... .... .... .... # 套接字操作

#socket.setdefaulttimeout(20)....................# 设置socket层的超时时间为20秒

def gethtml(url): #1 抓取网页html内容

....with urllib.request.urlopen(url) as req:

........buf=req.read()

........return buf

def getImg(buf,codec,fpath): #2 从html筛选图片地址到list

....i=url.find("/",9)............................ # 本句及下面三句截取url的前半截

....url2=url

....if i > 0 :

........url2=url[:i]

....buf=buf.decode(codec)

....

....reg=r'img src="(.+?\.jpg)"'....#正则表达式,得到图片地址

....#listurl=re.findall(r'http:.[^"]+\.jpg',buf)

....#listurl=re.findall(r'img src=.[^"]+\.jpg',buf)

....listurl=re.findall(reg,buf)

....print("准备下载图片数量:",len(listurl))

....for i in range(len(listurl)):................

........#listurl[i]=listurl[i].replace('img src="',"") # 把字符img src="去掉

........if not re.match("http",listurl[i]):

............listurl[i]=url2 + listurl[i]

........print(listurl[i])

............#3 抓取图片并保存到本地

....i=0

....

....if not os.path.isdir(fpath):

........os.mkdir(fpath)

....'''

....for imgurl in listurl:

........urllib.request.urlretrieve(imgurl,fpath + str(i)+'.jpg')

........i+=1

....'''#下面的操作方式要快一点

....for imgurl in listurl:

........f=open(fpath + str(i)+'.jpg','wb') # 新建空白图片文件

........req=urllib.request.urlopen(imgurl) # 获取网页图片文件

........buf=req.read().... .... .... # 读取网站上图片文件内容

........f.write(buf).... .... .... # 将网站上图片内容写入新建的图片文件

........f.close()

........i+=1

# 四处内容需要确认:1 网页url; .... ....2 网页编码UTF-8或gb2312;

#................ 3 图片扩展名jpg或png(两处); 4 保存的文件夹

#url='http://www.kgc.cn/list'

url='http://www.ttpaihang.com/vote/rank.php?voteid=1410&page=3'

buf=gethtml(url)

#codec='UTF-8'

codec='gb2312'

fpath="D:\pic4\"

print(getImg(buf,codec,fpath))

-End-