Bootstrap

Flask入门,看这一篇就够了

python笔记

Flask

路由配置篇

基本使用

综合案例——音乐网站

https://github.com/xishandong/Music_Web


在我们创建好的项目中,我们有一个基础的路由,如下所示:

from flask import Flask

app = Flask(__name__)


@app.route('/')
def hello_world():  # put application's code here
    ...
    
if __name__ == '__main__':
    app.run()

其中的@app.route(‘/’)就是我们需要的路由,我们运行这个就可以打开对应的网页。

返回数据(html,json,重定向)
  • 返回html数据

    • 纯粹的网页数据

    我们可以使用模板进行返回数据,我们需要从flask中导入render_template,然后将我们需要的网页模板放在templates目录中即可。

    from flask import Flask, render_template
    
    app = Flask(__name__)
    
    
    @app.route('/')
    def hello_world():  # put application's code here
        return render_template('index.html')
        
    if __name__ == '__main__':
        app.run()
    

    这样我们就可以在对应的页面看见index.html中的内容了。

    • 后端给前端传数据

      我们只需要在返回数据的时候传入对应的值即可。

    @app.route('/')
    def hello_world():  # put application's code here
        something = [], {}, '', 1
        return render_template('index.html', something=something)
    

    ​ 我们在前端用{{ something }}即可接受到传递的数据

  • 返回json数据

    我们需要从flask导入jsonify,像这样就可以传递json信息了。

from flask import jsonify


@app.route('user/', methods=['POST'])
def user_json():
       return jsonify({'success': 200, 'message': '请登陆后进行操作'})
  • 重定向

    使用这个功能我们需要从flask导入redirect

from flask import redirect, url_for

@app.route('/refer', method=['GET', 'POST'])
def refer():
    return redirect('/')  # 表示重定向到/路由下
	# 这个是重定向到蓝图中的user的login函数
	# return redirect(url_for("user.login"))

路由配置高级篇

蓝图

我们在实际开发时,由于代码量会很大,如果我们把所有内容都放到app.py一个文件中去编写,那么无疑是很痛苦的一件事情,这个时候就可以用到Flask的蓝图函数。

我们需要在项目中创建一个文件夹,可以叫blueprints,我们在里面就可以编写蓝图了。

  • 在app中
from blueprints.user import bp as user_bp

# 注册蓝图
app.register_blueprint(user_bp)
  • 在蓝图文件中
from flask import Blueprint

bp = Blueprint("user", __name__, url_prefix='/')

然后就可以正常写路由了。

路由传参

我们可以在url中自定义参数进行获取,如下所示

# 可以定义类型 @app.route('/blog/<int: blog_id>')
@app.route('/blog/<blog_id>')
def blog_detail(blog_id):
    return render_template("blog_detail.html", blog_id=blog_id)

Flask获取前端传入的数据

包含在url中的数据

@app.route('/book/list')
def book_list():
    # argument: 参数
    # request.args: 字典类型
    page = request.args.get("page", default=1, type=int)
    return f"你获取的是{page}"
json格式

  • 前端指定了contentType: ‘application/json;charset=UTF-8’
@bp.route('user/search_add', methods=['POST'])
def search_add():
    query = request.json.get('query')
    offset = request.json.get('offset')
    return "成功"
  • 前端没有指定json,而是表单传输
@bp.route('user/addPlaylist', methods=['POST'])
def add_playlist():
    name = request.form.get('playlistName')
    desc = request.form.get('playlistDesc')
    return "成功"
文件格式

@bp.route('user/upload_cover/<id>', methods=['POST'])
def upload_cover(id):
    file = request.files['file']
    filename = file.filename
    file.save(save_path)
    return jsonify({'success': 200, 'file': save_path})

在js中:

var avatar = $("#avatar")
avatar.click(function () {
    $("#fileInput").click();
});
$("#fileInput").change(function () {
    var fileInput = document.getElementById("fileInput");
    var file = fileInput.files[0];
    var formData = new FormData();
    formData.append("file", file);
    $.ajax({
        url: "/upload",
        type: "POST",
        data: formData,
        processData: false,
        contentType: false,
        success: function (response) {
            if (response.file) {
                $("#avatar").attr("src", response.file);
            }
        },
        error: function (error) {
            console.log(error);
        }
    });
});

在html中:

<!-- 上传头像 -->
<input type="file" id="fileInput" style="display: none;">

Flask的jinjia2模板语言

模板中的数据展示

在flask中,开发者为我们提供了一种jinjia2的模板语言,可以配合flask后端使用。

<div>
    {{ message }}
</div>

这样就可以展示后端传递给html的信息了,如何传递信息见上文Flask-路由配置篇-返回数据下。

模板的循环以及条件语句

  • 循环语句

    在jinjia2中,我们用下述方式进行循环,注意循环会完全遍历,不能中途break掉。假设后端给模板传递了一个items的可迭代数据:

{% for item in items %}
	<div>
        {{ item }}
	</div>
{% endfor %}
  • 条件语句

    类似于上述写法,jinjia2也可以进行条件语句的编写

{% if something %}
<div>
    条件语句为真
</div>
{% else %}
<div>
    条件语句为假
</div>
{% endif %}
模板的继承

如果我们的网页有很多重复的内容,例如导航栏以及页脚等内容,我们可以用模板继承的写法来编写,然后再父模板非公共部分划分出block即可让子模版在非公共地方编写。

  • 在父模板中

    假设父模板的名字叫base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>title</title>
    <link>
    {% block head %}{% endblock %}
</head>
<body>
    {% block body %}{% endblock %}
</body>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
{% block tail %}{% endblock %}
</html>
  • 在子模版中
{%extends "base.html" %}

{% block head %}
<link>
{% endblock %}

{% block body %}
<div>
    我是子模版
</div>
{% endblock %}

{% block tail %}
<script></script>
{% endblock %}
在模板中引用静态文件

  • 引用网络静态文件

    这个和普通的html文件一样。

<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/font-awesome/4.0.3/css/font-awesome.css">
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
  • 引用本地静态文件

    我们在模板中使用本地的静态文件,可以使用url_for来实现,例如:

<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<script src="{{ url_for('static', filename='js/base.js') }}"></script>
<img id="avatar" src="{{ url_for('static', filename='img/profile.webp') }}"
  • 页面跳转

    我们在一个页面想要跳转到其他页面时,也需要使用url_for,下面将展示两种不同的情况:

<!-- 这个hello_world时app里面的函数 -->
<a href="{{ url_for('hello_world') }}" class="item-link" id="pageLink">
<!-- playlists是user蓝图中的函数 -->
<a href="{{ url_for('user.playlists') }}" class="item-link icon-fill" id="pageLink">
模板中的 | 语法(过滤器)

总所周知,模板我们可以使用{{ something }}来展示后端给前端的数据,但是我们如何对这个数据进行一些简单的处理呢?这就要用到模板语言中的 | 语法。

首先,我们来看一个相对复杂的案例:

<li>推荐指数<span>{{ (10 - ((score - song.score | float) | abs | round(2) )) | round(2) }}</span></li>

这个是什么意思呢?首先第一个是将分数转化为浮点数,然后用另外一个分数去做差,然后去绝对值,同时保留两位小数,然后用10去做差,再保留两位小数。当然,jinjia2还支持更多的运算,下面将列出常见的用法:

过滤器名称说明例子
safe渲染时值不转义

{{ ‘name’ |safe }}

escapeHTML转义,即使autoescape关了也转义,可以缩写为e{% autoescape false %}

{{ ‘name’ |escape }}

{% endautoescape %}
capitialize把值的首字母转换成大写,其他子母转换为小写

{{ ‘hello world’ |capitalize }}

lower把值转换成小写形式

{{ ‘XML’ |lower }}

upper把值转换成大写形式

{{ ‘hello world’ |upper}}

title把值中每个单词的首字母都转换成大写

{{ ‘hello world’ |title}}

trim把值的首尾空格去掉

{{ ’ hello ’ |trim }}

striptags渲染之前把值中所有的HTML标签都删掉
join拼接多个值为字符串
replace替换字符串的值
round默认对数字进行四舍五入,也可以用参数进行控制

{{ 12.8888 |round(2, ‘floor’) }}

int把值转换成整型

{{ 12.8888 |int }}

float把值转化为小数{{ song.score | float }}
abs把值转化为正数

{{ -12 |abs }}

reverse字符串反转

{{ ‘hello’ |reverse }}

default增加默认值

{{ name |default(‘No name’) }}

format格式化显示

{{ ‘%s is %d’ |format(“Number”, 99) }}

first取第一个元素

{{ [1,2,3] |first }}

last取最后一个元素

{{ [1,2,3] |last }}

length返回列表长度,可以写为count

{{ [1,2,3,4,5] |length }}

sum列表求和

{{ [1,2,3,4,5] |sum }}

sort(列表)列表排序,默认为升序

{{ [3,2,1,5,4] |sort }}

join合并为字符串

{{ [1,2,3,4,5] |join(’ |') }}

sort(字典)按指定字段排序,这里设reverse为true使其按降序排
  • {% for user in users |sort(attribute=‘age’, reverse=true) %}
  • {{ user.name }}, {{ user.age }}
  • {% endfor %}
groupby列表分组,每组是一个子列表,组名就是分组项的值
  • {% for group in users|groupby(‘gender’) %}
  • {{ group.grouper }}
    • {% for user in group.list %}
    • {{ user.name }}
    • {% endfor %}
  • {% endfor %}
map取字典中的某一项组成列表,再将其连接起来

{{ users |map(attribute=‘name’) |join(', ') }}

见到了常见的过滤器之后,我们还可以在python内部进行自定义过滤器

# 第一种方式 
def get_even_list(l): 
	return l[::2] 
# 函数的第一个参数是过滤器函数,第二个参数是过滤器名称 
app.jinja_env.filters['even_filter'] =get_even_list 
# 第二种方式 
@app.template_filter() # 过滤器函数 
def is_even(num): 
	if num % 2 == 0: 
		return "even number" 
	else:
		return "odd number" 
<p>{{ [1,2,3,4,5] | even_filter }}</p> 
<p>{{ 2 | is_even }}</p>
测试器

测试器总是返回一个布尔值,它可以用来测试一个变量或者表达式,使用”is”关键字来进行测试。

{% set name='ab' %} 
{% if name is lower %} 
	<h2>"{{ name }}" are all lower case.</h2> 
{% endif %} 

测试器本质上也是一个函数,它的第一个参数就是待测试的变量,在模板中使用时可以省略去。如果它有第二个参数,模板中就必须传进去。测试器函数返回的必须是一个布尔值,这样才可以用来给if语句作判断。

  • jinjia2内置测试器
{# 检查变量是否被定义,也可以用undefined检查是否未被定义 #} 
{% if name is defined %} 
	<p>Name is: {{ name }}</p> 
{% endif %} 
{# 检查是否所有字符都是大写 #} 
{% if name is upper %} 
	<h2>"{{ name }}" are all upper case.</h2> 
{% endif %} 
{# 检查变量是否为空 #} 
{% if name is none %} 
	<h2>Variable is none.</h2> 
{% endif %} 
{# 检查变量是否为字符串,也可以用number检查是否为数值 #} 
{% if name is string %} 
	<h2>{{ name }} is a string.</h2> 
{% endif %} 
{# 检查数值是否是偶数,也可以用odd检查是否为奇数 #} 
{% if 2 is even %} 
	<h2>Variable is an even number.</h2> 
{% endif %} 
{# 检查变量是否可被迭代循环,也可以用sequence检查是否是序列 #} 
{% if [1,2,3] is iterable %} 
	<h2>Variable is iterable.</h2> 
{% endif %} 
{# 检查变量是否是字典 #} 
{% if {'name':'test'} is mapping %} 
	<h2>Variable is dict.</h2>
{% endif %} 
  • 自定义测试器
# 自定义测试器 
# 第一种方式 
import re 
def test_tel(tel_num): 
	tel_re = r'\d{11}' 
	return re.match(tel_re,tel_num) 
app.jinja_env.tests['is_tel'] = test_tel 
# 第二种方式 
@app.template_test('start_with') 
def start_with(str, suffix): 
	return str.lower().startswith(suffix.lower()) 
{% set tel = '18910171111' %} 
{% if tel is is_tel %} 
	<h2>{{ tel }} is mobile phone</h2> 
{% endif %} 
{% set name = 'Hello world' %} 
{% if name is start_with 'hello' %} 
	<h2>"{{ name }}" start_with "hello"</h2> 
{% endif %} 

数据库篇

数据库配置以及模型建立

  • 首先安装好下述依赖

    pip install pymysql
    pip install sqlalchemy
    pip install flsk-sqlalchemy
    # 这个是进行提交的
    pip install flask-migrate
    
  • 然后编写config.py文件

    # MySQL的主机名
    HOSTNAME = "127.0.0.1"
    # MySQL监听的端口, 默认3306
    PORT = 3306
    # 连接MySQL的用户名
    USERNAME = "root"
    # 连接MySQL的密码
    PASSWORD = ""
    # MySQL上连接的数据库名称
    DATABASE = "test"
    # 配置
    app.config['SQLALCHEMY_DATABASE_URI'] = f'mysql+pymysql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=utf8'
    
  • 编写exts.py文件

    from flask_sqlalchemy import SQLAlchemy
    
    db = SQLAlchemy()
    

    这个文件的作用是解决了文件引用死锁

  • 在app.py中注册

    from flask_migrate import Migrate
    from exts import db
    import config
    
    # 添加配置文件
    app.config.from_object(config)
    db.init_app(app)
    migrate = Migrate(app, db)
    
  • 在models.py中编写数据库的表

    from exts import db
    
    
    class User(db.Model):
        __tablename__ = "user"
        id = db.Column(db.Integer, primary_key=True, autoincrement=True)
        username = db.Column(db.String(100), nullable=False)
        password = db.Column(db.String(500), nullable=False)
        email = db.Column(db.String(100), nullable=False, unique=True)
        interval = db.Column(db.Float(precision=1), nullable=False, default=2)
        avatar = db.Column(db.String(500), nullable=False, default='')
        songs = db.relationship('Song', secondary='user_song', backref=db.backref('users', lazy='dynamic'))
        playlists = db.relationship('Playlist', backref='users', cascade="all, delete-orphan")
    
    
    class UserSong(db.Model):
        __tablename__ = 'user_song'
        user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), primary_key=True)
        song_id = db.Column(db.String(100), db.ForeignKey('song.id', ondelete='CASCADE'), primary_key=True)
    
    
    class Song(db.Model):
        __tablename__ = "song"
        id = db.Column(db.String(100), primary_key=True)
        name = db.Column(db.String(100), nullable=False)
        singer = db.Column(db.String(100), nullable=False)
        cover_url = db.Column(db.String(500), nullable=False)
        score = db.Column(db.String(100), nullable=False)
    
    
    class Playlist(db.Model):
        __tablename__ = 'playlists'
        id = db.Column(db.Integer, primary_key=True, autoincrement=True)
        name = db.Column(db.String(100), nullable=False)
        description = db.Column(db.String(200), nullable=True)
        user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
        cover = db.Column(db.String(500), nullable=False, default='img/default_cover.png')
        songs = db.relationship('Song', secondary='playlist_song',
                                backref=db.backref('playlists', lazy='dynamic', cascade="save-update, merge"))
    
    
    class PlaylistSong(db.Model):
        __tablename__ = 'playlist_song'
        playlist_id = db.Column(db.Integer, db.ForeignKey('playlists.id', ondelete='CASCADE'), primary_key=True)
        song_id = db.Column(db.String(100), db.ForeignKey('song.id', ondelete='CASCADE'), primary_key=True)
    
    

    上述表结构展现了常见的一对多以及多对多和外键约束

提交表结构
  • 在terminal输入以下命令

    flask db init
    flask db migrate
    flask db upgrade
    
  • 创建完成

使用ORM模型进行增删改查

使用这些模型就像是使用对象一样简洁

from models import User...

@app.route('/user/add')
def add_user():
    user = User(username='张三', password='123456')
    db.session.add(user)
    db.session.commit()
    return "用户创建成功!"

@app.route('/user/query')
def query_user():
    # 1. get: 根据主键
    # user = User.query.get(1)
    # print(user.id, user.username, user.password)
    # 2.filter, 返回的是Query, list
    users = User.query.filter_by(username='张三')
    for user in users:
        print(user)
    return "数据查找成功"

@app.route('/user/update')
def update_user():
    user = User.query.filter_by(username='张三').first()
    user.password = '2222'
    db.session.commit()
    return "数据修改成功"

@app.route('/user/delete')
def delete():
    user = User.query.filter_by(username='张三').first()
    db.session.remove(user)
    db.session.commit()
    return "用户删除成功"

@bp.route('user/deletePlaylistSong', methods=['POST'])
def delete_PlaylistSong():
    pid = request.form.get('pid')
    mid = request.form.get('mid')
    playlist = Playlist.query.filter_by(id=pid).first()
    song = Song.query.filter_by(id=mid).first()
    if playlist.user_id == g.user.id:
        if song in playlist.songs:
            playlist.songs.remove(song)
            try:
                db.session.commit()
                return jsonify({'success': 200, 'message': '歌曲从歌单中移除'})
            except Exception as e:
                current_app.logger.error('user/deletePlaylistSong: %s', str(e))
                return jsonify({'success': 200, 'message': '数据库错误'})
    else:
        return jsonify({'success': 500, 'message': '错误操作!'})

表单验证

定义验证表格

在我们将用户提交的信息存入数据库之前一定要进行表单验证,这里我们使用wtforms来进行表单验证

pip install wtforms

注: 我们在使用Email进行表单校验的时候还需要安装一个邮箱验证的第三方库,到时候根据报错信息自行安装即可。

  • 常规验证
import wtforms
from wtforms.validators import Email, Length, EqualTo


class RegisterForm(wtforms.Form):
    email = wtforms.StringField(validators=[Email(message="邮箱格式错误"), EmailVerify()])
    username = wtforms.StringField(validators=[Length(min=3, max=20, message="用户名格式错误, 最少3位,至多20位!")])
    password = wtforms.StringField(validators=[Length(min=6, max=20, message="密码格式错误! 最少6位, 最多20位!")])
    password_confirm = wtforms.StringField(
        validators=[EqualTo("password", message="两次输入密码不一致! 请检查密码输入!")])


class LoginForm(wtforms.Form):
    email = wtforms.StringField(validators=[Email(message="邮箱格式错误")])
    password = wtforms.StringField(validators=[Length(min=6, max=20, message="密码格式错误! 最少6位, 最多20位!")])

在上述代码中,我们对注册以及登录接口的数据进行了验证,但是在email我们加入了自定义验证,自定义验证见下文。

  • 自定义验证
class EmailVerify(wtforms.Form):
    def __call__(self, form, field):
        email = field.data
        user = User.query.filter_by(email=email).first()
        if user:
            raise wtforms.ValidationError(message="该邮箱已经被注册!")

我们定义自定义验证时,利用python的魔术方法__call__来定义类在调用时的方法,这里我们进行了数据库筛查,判断邮箱是否被注册,如果被注册就返回一个验证错误的错误信息。

下面我们来看一个另一种自定义的一个验证:

class RegisterForm(wtforms.Form):
    email = wtforms.StringField(validators=[Email(message="邮箱格式错误"), EmailVerify()])
    captcha = wtforms.StringField(validators=[Length(min=4, max=4, message="验证码格式错误!")])
    username = wtforms.StringField(validators=[Length(min=3, max=20, message="用户名格式错误, 最少3位,至多20位!")])
    password = wtforms.StringField(validators=[Length(min=6, max=20, message="密码格式错误! 最少6位, 最多20位!")])
    password_confirm = wtforms.StringField(
        validators=[EqualTo("password", message="两次输入密码不一致! 请检查密码输入!")])

    def validate_captcha(self, field):
        captcha = field.data
        email = self.email.data
        confirm = EmailCaptcha.query.filter_by(email=email, captcha=captcha).first()
        if not confirm:
            raise wtforms.ValidationError(message="邮箱或验证码错误!")

我们在想要验证的字段前加入__validate___这个字段,就可以自定义验证这个字段。

使用验证表格

我们需要先导入我们自定义的验证表单

from .forms import RegisterForm, LoginForm

之后我们只需要将前端传入的表单数据直接传递给这个Form就可以了

form = LoginForm(request.form)
    if form.validate():
        ...
    else:
		# return '表单验证失败'
        return redirect(url_for("user.login"))

配置全局变量

Flask中我们可以为整个后端配置一个全局变量,这个需要引入g,然后配置属性即可

from flask import g

setattr(g, "user", user)

配置登录信息

我们想要给用户配置登录信息,需要用到一个session函数

在user的蓝图中,我们在登录的路由登录成功后添加一个session信息

import ...

@bp.route('user/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template("login.html")
    elif request.method == 'POST':
        form = LoginForm(request.form)
        if form.validate():
            email = form.email.data
            password = form.password.data
            user = User.query.filter_by(email=email).first()
            if not user:
                return redirect(url_for("user.login"))
            if check_password_hash(user.password, password):
                # cookie
                session['user_id'] = user.id
                return redirect("/")
            else:
                return redirect(url_for("user.login"))
        else:
            return redirect(url_for("user.login"))

之后在app中配置上下文以及请求

import ...

# before_request/ before_first_request/ after_request
@app.before_request
def get_session():
    # 在登录成功后设置cookie过期时间为一年
    session.permanent = True
    app.permanent_session_lifetime = timedelta(days=365)
    user_id = session.get("user_id")
    if user_id:
        user = User.query.get(user_id)
        setattr(g, "user", user)
    else:
        setattr(g, "user", None)

@app.context_processor
def my_context_processor():
    return {"user": g.user}        

第一个是在每次发送请求的时候都在cookie中获取用户信息,如果有用户信息,就把他设置为全局变量的一个属性,更多的用法还有注释中的两种。

第二个是在上下文管理器中定义了user这个字段,这个字段就是可以在前端模板中,以{{ user }}访问到用户的信息。

注:在登录成功后设置cookie过期时间为一年,若不设置,默认浏览器关闭即清除登录状态。

Django

数据库篇

连接数据库以及配置模型

  • 首先在settings中配置好数据库的相关信息, 注:需要安装好mysqlclient

    DATABASES = {
        'default': {
            'ENGINE': 'django.db.backends.mysql',  # 数据库的连接类型
            'NAME': 'app01',					   # 数据库的名字
            'USER': 'root',						   # 数据库的登录账号
            'PASSWORD': '',               # 密码
            'HOST': '127.0.0.1',                   # ip
            'PORT': '3306',						   # 端口
        }
    }
    
  • 在对应app的models.py文件中编写对应的表结构

    下面展示简单的表结构以及外键约束

    class Department(models.Model):
        """ 部门表 """
        title = models.CharField(max_length=32, verbose_name='部门名称')
        
    
    class Employee(models.Model):
        """ 员工表 """
        name = models.CharField(max_length=16, verbose_name='姓名')
        password = models.CharField(max_length=64, verbose_name='密码')
        age = models.IntegerField(verbose_name='年龄')
        account = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='账户余额')
        created_at = models.DateTimeField(verbose_name='入职事件')
        # 外键约束
        # 设置级联删除
        department = models.ForeignKey(to="Department", to_field="id", on_delete=models.CASCADE)
        # 设置置空
        # department = models.ForeignKey(to="Department", to_field="id", null=True, blank=True, on_delete=models.SET_NULL)
        
        # 设置选择,即确立简单的对应关系,由Django自动匹配
        gender_choices = (
            (1, "男"),
            (2, "女"),
        )
        gender = models.SmallIntegerField(verbose_name='性别', choices=gender_choices)
    
将编写好的表结构注册进数据库

  • 首先将数据库在MySQL中创建好
    create database your_database_name
    
  • 执行下列命令
    python manage.py makemigrations
    python manage.py migrate
    
  • 创建完成
;