Python后端开发flask—最佳实践

最佳实践

json序列化问题

项目中使用sqlalchemy,且路由函数的返回结果是jsonify()封装的,则如下代码会报错:TypeError: Object of type User is not JSON serializable

@bp_user.route('/login', methods=['POST'])
def login():
    """ 登录 """
    username = request.json['username']
    password = request.json['password']
    target_user = User.query.filter(User.username==username, User.password==password).first()
    if target_user == None:
        return jsonify(status=-1, message='用户名或密码错误', data=None)
    return jsonify(status=1, message='登陆成功', data=target_user)

以下是解决步骤(参考)

  1. User模型类中,添加keys()__getitem__()两个方法:

    class User(db.Model):
        __tablename__ = 'user'
    
        id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
        username = db.Column(db.String(256), unique=True ,nullable=False)
        password = db.Column(db.String(256), nullable=False)
        create_time = db.Column(db.DateTime, default=datetime.datetime.now)
        status = db.Column(db.Integer, nullable=False, default=1)
    
        def __repr__(self):
            return 'User{id=%s}' % self.id
    	
        # 使用静态方法的好处是,可以方便地获取列名,直接调用User.keys()即可
        @staticmethod
        def keys():
            return ['id', 'username', 'password', 'create_time', 'status']
    
        def __getitem__(self, item):
            return getattr(self, item)
    
  2. 创建一个encoder.py文件,创建如下类:

    import datetime
    import decimal
    import uuid
    from flask.json import JSONEncoder as _JSONEncoder
    
    class JSONEncoder(_JSONEncoder):
        def default(self, o):
            if hasattr(o, 'keys') and hasattr(o, '__getitem__'):
                return dict(o)
            elif isinstance(o, datetime.datetime):
                return o.strftime('%Y-%m-%d %H:%M:%S')
            elif isinstance(o, datetime.date):
                return o.strftime('%Y-%m-%d')
            elif isinstance(o, decimal.Decimal):
                return str(o)
            elif isinstance(o, uuid.UUID):
                return str(o)
            elif isinstance(o, bytes):
                return o.decode('utf-8')
            else:
                _JSONEncoder.default(self, o)
    
  3. 在app.py(不一定,只要能对app变量操作均可)中,引入上面的JSONEncoder并设置给app变量

    from utils.encoder import JSONEncoder
    app.json_encoder = JSONEncoder
    
  4. 路由函数(无需变动):

    @bp_user.route('/login', methods=['POST'])
    def login():
        """ 登录 """
        username = request.json['username']
        password = request.json['password']
        target_user = User.query.filter(User.username==username, User.password==password).first()
        if target_user == None:
            return jsonify(status=-1, message='用户名或密码错误', data=None)
        return jsonify(status=1, message='登陆成功', data=target_user)
    
  5. 测试:
    在这里插入图片描述

同一个app问题

如果在数据模型db_model.py中实例化了app变量,这样仅运行db_model.py测试是没问题的。但入口文件app.py中如果也实例化了app变量,且引用了数据模型User,则在操作数据的时候可能报错:RuntimeError: The current Flask app is not registered with this 'SQLAlchemy' instance. Did you forget to call 'init_app', or did you create multiple 'SQLAlchemy' instances?
在这里插入图片描述

**解决方式:**不要再app.py中重新定义app变量,而是导入db_model.py的共享即可

# app.py
# app = Flask(__name__, static_folder='../static')
# 修改为下面的
from blueprint.db_model import app

响应流数据

需求

后端:收到请求后,返回csv文件的二进制流
前端:发送请求得到响应后,对收到的二进制流可以做两种操作:

  1. 保存为本次csv文件
  2. 不保存到磁盘,而是直接在内存中操作数据。比如直接使用pd.read_csv(io)读取这个csv流的内容进行数据分析和处理

后端

@bp_parser.route('/result', methods=['POST'])
def result():
    """ 获取解析结果 """
    user_id = request.json['user_id']
    method = request.json['method']
    uuid4 = request.json['uuid4']
    template_path = './static/parser/{}_result/{}_{}_structured.csv'.format(method, user_id, uuid4)
    # 文件存在性和合法性校验
    if not os.path.exists(template_path) or not os.path.isfile(template_path):
        return jsonify(status=0, message='未找到此文件', data=None)
    return send_file(os.path.abspath(template_path))

前端

保存本地简单;需要注意直接内存读取时,要先将二进制流response.content转化为能被pd读取的io流

  1. 保存为本次csv文件:

    import requests
    
    url = 'http://127.0.0.1:5000/parser/result'
    body = {
        'user_id': -1,
        'method': 'Drain',
        'uuid4': '9fe61260-de14-4238-9a0e-f35a284a80af',
    }
    response = requests.post(url, json=body)
    with open('test.csv', 'wb') as fp:
        fp.write(response.content)
    
  2. 直接在内存中操作:

    import io
    import requests
    import pandas as pd
    
    url = 'http://127.0.0.1:5000/parser/result'
    body = {
        'user_id': -1,
        'method': 'Drain',
        'uuid4': '9fe61260-de14-4238-9a0e-f35a284a80af',
    }
    response = requests.post(url, json=body)
    
    df = pd.read_csv(io.BytesIO(response.content))
    print(df)
    

解决跨域

  1. 安装flask-cors

    pip install flask-cors
    
  2. 包装app对象

    from flask import Flask
    from flask_cors import CORS
    
    app = Flask(__name__)
    CORS(app)
    

验证码生成及校验

安装captcha库

pip install captcha -i https://mirrors.aliyun.com/pypi/simple

准备验证码字体

C:\Windows\Fonts\文件夹下选择一种字体,将其复制到/assets/文件夹下,例如ALGER.TTF

后端

import os
import random

from captcha.image import ImageCaptcha
from flask import Flask, Response, session, jsonify, request

app = Flask(__name__)
# flask中要使用session会话技术, 必须要配置该参数
app.config['SECRET_KEY'] = os.urandom(24)

# 验证码配置项
VCODE_LENGTH = 4
VCODE_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'


@app.route('/system/vcode', methods=['GET'])
def generate_vcode():
    """ 生成验证码 """
    vcode_char_list = random.choices(VCODE_CHARS, k=VCODE_LENGTH)
    vcode_text = ''.join(vcode_char_list)
    mime_type = 'image/png'
    image = ImageCaptcha(fonts=['./assets/ALGER.TTF'])
    data = image.generate(vcode_text)  # 生成图片验证码
    response = Response(data.getvalue(), mimetype=mime_type)
    response.headers['Content-Type'] = mime_type
    # 统一变成小写存入session
    session['vcode'] = vcode_text.lower()
    return response


@app.route('/user/login', methods=['POST'])
def login():
    """ 登录及验证 """
    vcode = request.json.get('vcode')
    username = request.json.get('username')
    password = request.json.get('password')
    # 参数缺失校验
    if vcode is None or username is None or password is None:
        return jsonify(status=0, message='请求参数缺失', data=None)
    # 校验验证码, 统一对照小写
    vcode_session = session.get('vcode')
    if vcode.lower() != vcode_session:
        return jsonify(status=0, message='验证码错误', data=None)
    # 校验用户名密码
    if username == 'admin' and password == 'admin':
        data = {'username': username}
        return jsonify(status=1, message='登陆成功', data=data)
    return jsonify(status=0, message='用户名或密码错误', data=None)


if __name__ == '__main__':
    app.run(debug=False, port=5000)

前端

编写登录api,创建/src/api/user.js,并编写如下内容:

import request from "../http/request";

export const login = (data) => {
  return request({
    url: '/user/login',
    method: 'post',
    data: data
  });
};

构建html表单界面:

<el-form ref="loginForm" :model="loginForm">
    <div class="login-form-title">
        <h1 class="title">满文识别系统</h1>
    </div>
    <el-form-item prop="account">
        <el-input v-model="loginForm.username" type="text" auto-complete="off" placeholder="账号" maxlength="20"
                  />
    </el-form-item>
    <el-form-item prop="password">
        <el-input v-model="loginForm.password" type="password" placeholder="密码"
                  maxlength="20"
                  @keyup.enter.native="handleLogin"/>
    </el-form-item>
    <el-form-item prop="vcode">
       <el-input v-model="loginForm.vcode" type="text" placeholder="验证码"
                  maxlength="20"
                  @keyup.enter.native="handleLogin"/>
    </el-form-item>
    <el-image id="vcode" :src="vcodeSrc" :fit="'fill'" title="看不清?点击换一张"
              @click="handleVcode()"></el-image>
    <el-form-item style="width:100%;">
        <el-button :loading="loading" class="login-btn" size="medium" type="primary" style="width:100%;"
                   @click.native.prevent="handleLogin">
            <span v-if="!loading">登  录</span>
            <span v-else>登 录 中...</span>
        </el-button>
    </el-form-item>
</el-form>

编写js刷新验证码及登录逻辑:

<script>
  import {login} from "@/api/user";

  export default {
    name: 'Login',
    data() {
      return {
        loginForm: {
          username: 'admin',
          password: 'admin',
          vcode: ''
        },
        loading: false,
        vcodeSrc: '/api/system/vcode',
      }
    },
    methods: {
      /** 点击登录按钮 */
      handleLogin() {
        // 参数完整性校验
        if (Object.values(this.loginForm).some((param) => param ==='')) {
          return this.$message.error('请填写完整表单');
        }
        // 发送请求
        this.loading = true;
        login(this.loginForm).then((res) => {
          this.loading = false;
          if (res.status === 1) {
            // 登录成功
            this.$message.success('登陆成功, 欢迎访问');
            // 1. 存储 user
            localStorage.setItem('user', JSON.stringify(res.data));
            // 2. 跳转到后台主页
            this.$router.replace('/upload');
          } else {
            // 登录失败
            localStorage.removeItem('user');
            // 清空vcode
            this.loginForm.vcode = '';
            this.$message.error(res.message);
          }
        }).catch((err) => {
          this.loading = false;
          this.$message.error('系 统 错 误: ' + err.toString());
        });
      },
      /** 点击验证码图片刷新 */
      handleVcode() {
        this.vcodeSrc = "/api/system/vcode?time=" +  new Date().getTime();
      }
    }
  }
</script>

测试

在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值