19.Flask高级

1.1 生活案例

例如,用户 A 在超市购买的任何商品都应该放在 A 的购物车内,不论是用户 A 什么时间购买的,这都是属于同一个会话的,不能放入用户 B 或用户 C 的购物车内,这不属于同一个会话。

而 Web 应用程序是使用 HTTP 协议传输数据的。HTTP 协议是无状态的协议。一旦数据交换完毕,客户端与服务器端的连接就会关闭。再次交换数据需要建立新的连接,这就意味着服务器无法从连接上跟踪会话

即用户 A 购买了一件商品放入购物车内,当再次购买商品时服务器已经无法判断该购买行为是属于用户 A 的会话还是用户 B 的会话了。因此,必须引入一种机制,让服务器记住用户。

Cookie 就是这样的一种机制。它可以弥补 HTTP 协议无状态的不足。在 Session 出现之前,基本上所有的网站都采用 Cookie 来跟踪会话。

由于 HTTP 是一种无状态的协议,服务器单从网络连接上无从知道客户身份。怎么办呢?就给客户端们颁发一个通行证吧,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是 Cookie 的工作原理

Cookie 实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用 response 向客户端浏览器颁发一个 Cookie。客户端浏览器会把 Cookie 保存起来。

当浏览器再请求该网站时,浏览器把请求的网址连同该 Cookie 一同提交给服务器。服务器检查该 Cookie,以此来辨认用户状态。服务器还可以根据需要修改 Cookie 的内容。

注意
浏览器对 cookie 数量和大小有限制的!如果超过了这个限制,你的信息将丢失。
> > 不同的浏览器存储的 Cookie 的数量不同! > > 尽量保证 cookie 的数量以及相应的大小。cookie 个数最好 < 20~30 个;cookie 大小最好 < 4K。

2.1 设置

设置 cookie 是在 Response 的对象上设置。

flask.Response 对象有一个 set_cookie 方法,可以通过这个方法来设置 cookie 信息。

key,value 形式设置信息:

@app.route("/set_cookie/")
def set_cookie():
    resp = make_response("设置了一个 cookie 信息")
    resp.set_cookie("uname", "xln")
    return resp

在 Chrome 浏览器中查看 cookie 的方式:

  • 方式 1:借助于开发调式工具进行查看。
  • 方式 2:在 Chrome 的设置界面 -> 高级设置 -> 内容设置 -> 所有 cookie -> 找到当前域名下的 cookie。
@app.route("/get_cookie/")
def get_cookie():
    uname = request.cookies.get("uname")
    return f"Cookie 中的内容是 {uname}"
  • 方式 1:通过 Response对象.delete_cookie,指定 cookie 的 key,就可以删除 cookie 了。
@app.route("/del_cookie/")
def del_cookie():
    resp = make_response("删除了一个 cookie 信息")
    resp.delete_cookie("uname")
    return resp
  • 方式 2:在客户端浏览器人为的删除(清除浏览器浏览历史记录后,很多网站之前免密登录的都不好使了)。

默认的过期时间:如果没有显示的指定过期时间,那么这个 cookie 将会在浏览器关闭后过期。

max_age:以秒为单位,距离现在多少秒后 cookie 会过期。

expires:为 datetime 类型。这个时间需要设置为格林尼治时间,相对北京时间来说会自动 +8 小时

如果 max_age 和 expires 都设置了,那么这时候以 max_age 为标准。

注意
**max_age** 在 IE8 以下的浏览器是不支持的。
> > **expires** 虽然在新版的 HTTP 协议中是被废弃了,但是到目前为止,所有的浏览器都还是能够支持,所以如果想要兼容 IE8 以下的浏览器,那么应该使用 expires,否则可以使用 max_age。

3.1 默认的过期时间

@app.route("/create_cookie/default")
def create_cookie():
    # 如果没有设置有效期,默认会在浏览器关闭的时候,让 cookie 过期
    resp = make_response("通过默认值设置 cookie 有效期")
    resp.set_cookie("uname", "xln")
    return resp

关闭浏览器再次打开访问:

3.2 max_age

@app.route("/create_cookie/max_age")
def create_cookie2():
    resp = make_response("通过 max_age 设置 cookie 有效期")
    # max_age 以秒为单位设置 cookie 的有效期
    # 设置两小时后过期
    age = 60 * 60 * 2
    resp.set_cookie("uname", "xln", max_age=age)
    return resp

3.3 expires

from datetime import datetime


@app.route("/create_cookie/expires")
def create_cookie3():
    resp = make_response("通过 expires 设置 cookie 有效期")
    tmp_time = datetime(2025, 10, 1, 8, 30, 30)
    # expires 以指定时间为 cookie 的有效期
    # 时间为格林尼治时间,相对北京时间来说会自动 +8 小时
    resp.set_cookie("uname", "xln", expires=tmp_time)
    return resp

上面的方式需要我们自己计算时差,太麻烦了,可不可以自动计算过期时间呢?使用 datetime.timedelta 来进行计算。

from datetime import datetime, timedelta


@app.route("/create_cookie/expires1")
def create_cookie4():
    resp = make_response("通过 expires 设置 cookie 有效期")
    # 通过 timedelta 来计算过期时间
    # 两天后过期
    tmp_time = datetime.now() + timedelta(days=2)
    # expires 以指定时间为 cookie 的有效期
    resp.set_cookie("uname", "xln", expires=tmp_time)
    return resp

3.4 max_age 和 expires 同时设置

@app.route('/create_cookie/exp_max/')
def create_cookie5():
    resp = make_response('通过expires与max_age,设置cookie有效期')
    # expires 与 max_age 同时设置了,会以 max_age 为准
    tmp_time = datetime.now() + timedelta(days=2)
    resp.set_cookie('uname', 'python_sql', expires=tmp_time, max_age=60 * 60 * 2)
    return resp

4 Session 的介绍

Session 和 Cookie 的作用有点类似,都是为了存储用户相关的信息,都是为了解决 http 协议无状态的这个特点。

不同的是 Cookie 保存在客户端浏览器中,而 Session 保存在服务器上。

客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。

客户端浏览器再次访问时只需要从该 Session 中查找该客户的状态就可以了。

注意
不同的语言,不同的框架,有不同的实现。
> > 虽然底层的实现不完全一样,但目的都是让服务器端能方便的存储数据而产生的。

Session 的出现,是为了解决 cookie 存储数据不安全的问题的。

如果说 Cookie 机制是通过检查客户身上的“通行证”来确定客户身份的话,那么 Session 机制就是通过检查服务器上的“客户明细表”来确认客户身份

Session 相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了

4.1 Session 的跟踪机制

Flask 框架中,session 的跟踪机制跟 Cookie 有关,这也就意味着脱离了 Cookie,session 就不好使了。

因为 session 跟踪机制跟 cookie 有关,所以,要分服务器端客户端分别起到什么功能来理解。

session 工作过程

存储在服务器的数据会更加的安全,不容易被窃取。

但存储在服务器也有一定的弊端,就是会占用服务器的资源,但现在服务器已经发展至今,一些 session 信息还是绰绰有余的。

问题
(面试题)若客户端禁用了浏览器的 Cookie 功能,session 功能想继续保留,该咋整?给出你的实现思路(能代码实现最好)?

解决方法
URL 地址携带 SessionID。
> > 1. URL 重写(URL Rewriting) > > 这是最常用的方法,将会话 ID 作为 URL 参数传递给客户端。  > > - **实现方法**: > - 当用户访问服务器时,服务器生成一个唯一的会话 ID,并将其作为参数附加到返回给用户的 URL 的末尾。 > - 用户进行后续操作时,浏览器会自动带上这个 URL 参数,服务器通过解析 URL 中的参数来识别用户会话。 > - Flask 提供了 `url_for()` 函数,可以很方便地实现这一功能。 > > 2. 使用 Web Storage API(如 localStorage) > > 您可以利用浏览器提供的 `localStorage` 或 `sessionStorage` 来存储会话信息,而不是 Cookie。  > > - **实现方法**: > - 在服务器端(Flask 应用)向客户端发送包含会话 ID 的响应。 > - 客户端(浏览器)接收到会话 ID 后,将其存储在 `localStorage` 中。 > - 后续的请求中,客户端 JavaScript 可以从 `localStorage` 中读取会话 ID,并将其作为请求头或请求体发送给服务器。 > > 3. 混合方法 > > 在禁用 Cookie 的情况下,还可以采取更灵活的混合方法。  > > - **检测能力**:您的应用可以检测浏览器是否支持 Cookie。 > - **动态选择**: > - 如果支持 Cookie,则使用传统的 Cookie 方式。 > - 如果 Cookie 被禁用,则自动切换到 URL 重写或 Web Storage API 的方式。 > > 注意: > > - URL 重写方法会使 URL 变得冗长,且可能存在安全风险。  > - 使用 Web Storage API 需要 JavaScript 的支持,并且可能更复杂一些。  > - 如果选择使用 URL 重写,您需要妥善管理生成和移除会话 ID 的逻辑。

5 Flask 中使用 Session

需要先设置 SECRET_KEY。即加密所用的盐。

# ===通过类的方式进行设置===
class DefaultConfig(object):
    SECRET_KEY = 'fih9fh9eh9gh2'


app.config.from_object(DefaultConfig)


# ===或者直接设置===
app.secret_key = 'xihwidfw9efw'

# ===通过文件进行设置。。。===

5.1 设置、修改

from os import urandom

from flask import Flask, session

app = Flask(__name__)

# 设置 session 的盐
# app.secret_key = "ymfbblyf123"
# 计算机随机生成 24 位长度盐
app.secret_key = urandom(24)


@app.route("/set_session")
def set_session():
    # session["key"] = "value"
    session["uname"] = "xln"
    return "设置了一个 session 对象"

5.2 读取

@app.route("/get_session/")
def get_session():
    uname = session.get("uname")
    # 根据 uname 值,从数据库中获取用户信息
    return f"从 session 中读取到的信息为 {uname}"

5.3 删除

@app.route("/del_session/")
def del_session():
    # pop 删除一个 key
    session.pop("uname")
    # clear 输出所有 key
    # session.clear()
    return f"从 session 中删除 uname"

5.4 案例

from os import urandom

from flask import Flask, session

app = Flask(__name__)

# 设置 session 的盐
# app.secret_key = "ymfbblyf123"
# 计算机随机生成 24 位长度盐
app.secret_key = urandom(24)


@app.route("/")
def index():
    return "Hello"


@app.route("/set_session/")
def set_session():
    # session["key"] = "value"
    session["uname"] = "xln"
    return "设置了一个 session 对象"


@app.route("/get_session/")
def get_session():
    uname = session.get("uname")
    # 根据 uname 值,从数据库中获取用户信息
    return f"从 session 中读取到的信息为 {uname}"


@app.route("/del_session/")
def del_session():
    # pop 删除一个 key
    session.pop("uname")
    # clear 输出所有 key
    # session.clear()
    return f"从 session 中删除 uname"


if __name__ == '__main__':
    app.run(debug=True
  • 测试访问 http://127.0.0.1:5000/set_session/

    此时浏览器中没有 Session 数据。

  • 测试访问 http://127.0.0.1:5000/set_session/


    可以看到浏览器中已经有 Session 数据了。内容是加密过的。

  • 测试访问 http://127.0.0.1:5000/get_session/


    服务器可以正常获取到设置的 Session 值。

  • 测试访问 http://127.0.0.1:5000/del_session/


    浏览器中还有 Session 数据,再次尝试在服务器中获取 uname 的值,已经无法获取了。

6 Flask 设置 Session 的有效期

如果没有设置 session 的有效期。那么默认就是浏览器关闭后过期。

如果设置 session.permanent = True,那么就会默认在 31 天后过期。

@app.route("/set_session/")
def set_session():
    # 设置 session 的持久化,默认增加了 31 天
    session.permanent = True
    session["uname"] = "xln"
    return "设置了一个 Session 信息"

如果不想在 31 天后过期,按如下步骤操作:

  1. session.permanent = True
  2. 可以通过配置项 app.config['PERMANENT_SESSION_LIFETIME'] 来设置 session 过期时间。
    比如设置 app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=2) 在两天后过期。
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=2)


@app.route("/set_session/")
def set_session():
    # 设置 session 的持久化,默认增加了 31 天
    session.permanent = True
    session["uname"] = "xln"
    return "设置了一个 Session 信息"

重启服务器 Session 会过期吗?

@app.route("/get_session/")
def get_session():
    # 如果服务器异常关闭,还是可以正常获取到 session 信息
    # 如果 secret_key 设置是一个固定的值,那么服务器重启不会影响 session 的有效期
    # 如果 secret_key 设置不是一个固定的值,那么服务器之前设置的 session 将全部过期
    return session.get("uname")

Session 参考:https://www.cnblogs.com/cwp-bg/p/10084523.html

7 Session 实战

login.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>

<body>
    <form action="{{ url_for('login') }}" method="post">
        <table>
            <tr>
                <td>账号:</td>
                <td><input type="text" name="uname"></td>
            </tr>
            <tr>
                <td>密码:</td>
                <td><input type="password" name="pwd"></td>
            </tr>
            <tr>
                <td colspan="2"><input type="submit" value="登录"></td>
            </tr>
            {% if msg %}
                <tr>
                    <td colspan="2">{{ msg }}</td>
                </tr>
            {% endif %}
        </table>
    </form>
</body>

</html>

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>个人主页</h1>
</body>
</html>
from flask import Flask, render_template, request, session, redirect, url_for
from flask.views import MethodView

app = Flask(__name__)

app.secret_key = "adsfweffff"

@app.route("/")
def home():
    return "Hello"


class LoginView(MethodView):
    def __jump(self, msg=None):
        # 防止 post 和 get 方法互相调用
        return render_template("login.html", msg=msg)

    def get(self):
        msg = request.args.get("msg")
        return self.__jump(msg=msg)

    def post(self):
        uname = request.form.get("uname")
        pwd = request.form.get("pwd")

        if uname == "xln" and pwd == "123":
            # 记录用户信息
            session["uname"] = uname
            return render_template("index.html")
        return self.__jump("用户名或密码不正确")


app.add_url_rule("/login/", view_func=LoginView.as_view("login"))


@app.route("/index/")
def index():
    uname = session.get("uname")
    if uname:
        return render_template("index.html")
    else:
        return redirect(url_for("login", msg="请先登录"))


if __name__ == '__main__':
    app.run(debug=True)

8 Local 对象

8.1 需求

  • 要实现同时处理多个请求的需求,使用并行技术来实现,每进一个请求的我们都开启一个进程,开销太大,显然是不合理的。
  • 使用并发技术,用线程来处理每一个请求就是一个很好的解决方案。
  • 但是由于进程是分配资源的最小单位,线程是分配处理器的最小单位。所以线程中的数据是不互相隔离的,修改数据时存在数据不安全的问题

8.2 Local 对象

为了解决线程中存在的数据不安全问题,Flask 引入了 Local 对象。

在 Flask 中,类似于 request 对象等,其实是绑定到了一个 werkzeug.local.Local 对象上。

这样,即使是同一个对象,那么在多个线程中都是隔离的。类似的对象还有 session 对象。

from werkzeug.local import Local

# flask = werkzeug + sqlalchemy + jinja2

8.3 ThreadLocal 变量

Python 提供了 ThreadLocal 变量,它本身是一个全局变量,但是每个线程却可以利用它来保存属于自己的私有数据,这些私有数据对其他线程也是不可见的。

threading 中使用 local 对象来实现:

from threading import Thread, local

local = local()

# 主线程变量
local.uname = "小龙女"


class MyThread1(Thread):
    def run(self):
        local.uname = "周芷若"
        print(f"子线程 local.uname 变量值:{local.uname}")


class MyThread2(Thread):
    def run(self):
        local.uname = "赵敏"
        print(f"子线程 local.uname 变量值:{local.uname}")


my_thread1 = MyThread1()
my_thread2 = MyThread2()

my_thread1.start()
my_thread2.start()
# 阻塞等待子线程执行完毕
my_thread1.join()
my_thread2.join()

print(f"主线程 local.uname 变量值:{local.uname}")

flask 中对线程中的 local 对象做了封装,使用 werkzeug.local 包中的 Local 对象实现。

from threading import Thread
from werkzeug.local import Local

local = Local()

# 主线程变量
local.uname = "小龙女"


class MyThread1(Thread):
    def run(self):
        local.uname = "周芷若"
        print(f"子线程 local.uname 变量值:{local.uname}")


class MyThread2(Thread):
    def run(self):
        local.uname = "赵敏"
        print(f"子线程 local.uname 变量值:{local.uname}")


my_thread1 = MyThread1()
my_thread2 = MyThread2()

my_thread1.start()
my_thread2.start()
my_thread1.join()
my_thread2.join()

print(f"主线程 local.uname 变量值:{local.uname}")

8.4 总结

只要满足绑定到 localLocal 对象上的属性,在每个线程中都是隔离的,那么他就叫做 ThreadLocal 对象,也叫 ThreadLocal 变量。

9 Flask_app 上下文

App 上下文,也叫应用上下文:

上下文(感性的理解)
> 每一段程序都有很多外部变量,只有像 `add` 这种简单的函数才是没有外部变量的。`add` 函数只需要传参就可以独立调用。 > > 但一段程序如果有了外部变量,这段程序就不完整,不能独立运行,比如 `index` 函数独立调用后并无法通过网络去映射到其处理逻辑。而为了能让这段程序可以运行,就要给所有的外部变量一个一个设置一些值。这些值所在的集合就是叫上下文。 > > 并且上下文这一概念在中断任务的场景下具有重大意义,其中任务在被中断后,处理器保存上下文并提供**中断处理**,因些在这之后,任务可以在同一个地方继续执行(上下文越小,延迟越小)。 ---
举例
> 运行的 Flask 项目,每一个路由映射的内容片段,都不可以单独拿出来使用。 > > 当获取到了 `APP_Context` 以后,就可以直接通过程序映射的地址访问逻辑,并且可以重复使用。

上下文的一个典型应用场景就是用来缓存一些我们需要在发生请求之前或者要使用的资源。举个例子,比如数据库连接。

当我们在应用上下文中来存储东西的时候你得选择一个唯一的名字,这是因为应用上下文为 Flask 应用和扩展所共享。

应用上下文

应用上下文是存放到一个 LocalStack 的栈中。和应用 app 相关的操作必须要用到应用上下文

比如

通过 current_app 获取当前的这个 app 名字。

注意
**在视图函数中,不用担心应用上下文的问题**。因为视图函数要执行,那么肯定是通过访问 url 的方式执行的,那么这种情况下,Flask 底层就已经自动的帮我们把应用上下文都推入到了相应的栈中。
---
注意
如果想要**在视图函数外面执行与 app 相关的操作**,比如:获取当前的 app 名称,那么就**必须要手动推入应用上下文**。

直接使用应用上下文会报错

from flask import Flask, current_app

app = Flask(__name__)

# 直接使用 应用上下文 会报错
# print(current_app.name)


@app.route("/")
def index():
    return f"Hello!这是一个[{current_app.name}]的引用"


if __name__ == '__main__':
    app.run(debug=True)
  • 测试访问 http://127.0.0.1:5000/


    在视图函数中可以正常获取到 app 的名字。

  • 将视图函数外获取 app 名字的代码放开注释:


    可以看到,在视图函数外直接执行与 app 相关的操作,程序会因缺少应用上下文而报错。

第一种方式:便于理解的写法

from flask import Flask, current_app

app = Flask(__name__)

# 方法一
# 创建 app 上下文
app_context = app.app_context()
# 将 app 上下文推到栈顶
app_context.push()

print(current_app.name)


@app.route("/")
def index():
    return f"Hello!这是一个[{current_app.name}]的引用"


if __name__ == '__main__':
    app.run(debug=True)

第二种方式:用 with 语句

from flask import Flask, current_app

app = Flask(__name__)

# 方法二
# with 会将创建的上下文自动推送到栈顶
with app.app_context():
    print(current_app.name)


@app.route("/")
def index():
    return f"Hello!这是一个[{current_app.name}]的引用"


if __name__ == '__main__':
    app.run(debug=True)

10 Flask_request 上下文详解

10.1 请求上下文

请求上下文也是存放到一个 LocalStack 的栈中。

和请求相关的操作必须用到请求上下文,比如使用 url_for 反转视图函数。

注意
**在视图函数中,不用担心请求上下文的问题**。因为视图函数要执行,那么肯定是通过访问 url 的方式执行的,那么这种情况下,Flask 底层就已经自动的帮我们把应用上下文和请求上下文都推入到了相应的栈中。
> ---
注意
如果想要在**视图函数外面执行与请求相关的操作**,比如反转 url,那么就**必须要手动推入请求上下文**:
>

直接在外面使用 url_for 函数会报错:

from flask import Flask, url_for

app = Flask(__name__)


@app.route("/")
def index():
    url = url_for("test")
    return f"Hello==={url}"


@app.route("/test/")
def test():
    return f"这是为了测试请求上下文"


# 直接使用会报错
# url = url_for("test")
# print(url)

if __name__ == '__main__':
    app.run(debug=True)
  • 测试访问 http:127.0.0.1:5000/


    可以看到在视图函数中执行与请求相关的操作没有任何问题。

  • 将在视图函数外执行与请求相关操作的代码打开:


    可以看到,在函数外执行与请求相关的操作时会报错。

上面的错误提示需要应用上下文,创建一个应用上下文后再运行:

from flask import Flask, url_for

app = Flask(__name__)


@app.route("/")
def index():
    url = url_for("test")
    return f"Hello===<{url}>"


@app.route("/test/")
def test():
    return f"这是为了测试请求上下文"


with app.app_context():
    # 只有应用上下文也会报错
    url = url_for("test")
    print(url)

if __name__ == '__main__':
    app.run(debug=True)

依旧报错,提示需要请求上下文:

from flask import Flask, url_for

app = Flask(__name__)


@app.route("/")
def index():
    url = url_for("test")
    return f"Hello===<{url}>"


@app.route("/test/")
def test():
    return f"这是为了测试请求上下文"


with app.test_request_context():
    url = url_for("test")
    print(f"{url}")

if __name__ == '__main__':
    app.run(debug=True)

底层代码执行说明
> 1. 推入请求上下文到栈中,会首先判断有没有应用上下文。 > 2. 如果没有那么就会先推入应用上下文到栈中。 > 3. 然后再推入请求上下文到栈中。
总结
为什么上下文需要放在栈中?
> > 1. 应用上下文:Flask 底层是基于 werkzeug,werkzeug 是可以包含多个 app 的,所以这时候用一个栈来保存。 > > 如果你在使用 app1,那么 app1 应该是要在栈的顶部,如果用完了 app1,那么 app1 应该从栈中删除。方便其他代码使用下面的 app。 > > 2. 如果在写测试代码,或者离线脚本的时候,我们有时候可能需要创建多个请求上下文,这时候就需要存放到一个栈中了。 > > 使用哪个请求上下文的时候,就把对应的请求上下文放到栈的顶部,用完了就要把这个请求上下文从栈中移除掉。

11 Flask_线程隔离的 g 对象

保存为全局对象 g 对象的好处

g 对象是在整个 Flask 应用运行期间都是可以使用的。并且和 request 一样,是线程隔离的。

这个对象是专门用来存储开发者自己定义的一些数据,方便在整个 Flask 程序中都可以使用。

一般使用就是,将一些经常会用到的数据绑定到上面,以后就直接从 g 上面取就可以了,而不需要通过传参的形式,这样更加方便。

g 对象使用场景:有一个工具类 utils.py 和 用户办理业务:工具类中工具函数需要获得请求中的用户名来做一些处理。

def func_a(uname):
    return f"func_a {uname}"


def func_b(uname):
    return f"func_b {uname}"


def func_c(uname):
    return f"func_c {uname}"

用户办理业务

from flask import Flask
from utils import *

app = Flask(__name__)


@app.route("/")
def index():
    uname = "小龙女"
    a = func_a(uname)
    b = func_b(uname)
    c = func_c(uname)
    return f"Hello<br>{a}<br>{b}<br>{c}"


if __name__ == '__main__':
    app.run(debug=True)

上面的方式可以实现需求,但是用户名在一次请求中是不会变的,那每处用到用户名的地方都进行传参太过麻烦,这时可以将用户名放到 flask 的全局 g 对象中,这样在一次请求中,无论位于哪里都可以直接使用,甚至不在一个模块文件中也可以。

优化工具类 utils.py

from flask import g


def func_a():
    return f"func_a {g.uname}"


def func_b():
    return f"func_b {g.uname}"


def func_c():
    return f"func_c {g.uname}"

优化用户办理业务

from flask import Flask
from new_utils import *

app = Flask(__name__)


@app.route("/")
def index():
    # 从请求中获取用户名
    uname = "小龙女"
    g.uname = uname

    a = func_a()
    b = func_b()
    c = func_c()
    return f"Hello<br>{a}<br>{b}<br>{c}"


if __name__ == '__main__':
    app.run(debug=True)

12 Flask——钩子函数介绍

12.1 钩子函数概念

在 Flask 中钩子函数是使用特定的装饰器装饰的函数。

为什么叫做钩子函数呢,是因为钩子函数可以在正常执行的代码中,插入一段自己想要执行的代码,所以这类函数就叫做钩子函数。

12.2 常见的钩子函数

  • before_first_request:处理项目的第一次请求之前执行。

    [!danger] 弃用
    before_first_request 在 Flask v2.3 后已删除。详解中有替代方式。

@app.before_first_request
def first_request():
    print('first time request')
  • before_request:在每次请求之前执行。通常可以用这个装饰器来给视图函数增加一些变量。请求已经到达了 Flask,但是还没有进入到具体的视图函数之前调用。一般这个就是在视图函数之前,我们可以把一些后面需要用到的数据先处理好,方便视图函数使用。
@app.before_request
def before_request():
    if not hasattr(g, 'glo1'):
        setattr(g, 'glo1', '想要设置的')
  • teardown_appcontext:不管是否有异常,注册的函数都会在每次请求之后执行。
@app.teardown_appcontext
def teardown(exc=None):
    if exc is None:
        db.session.commit()
    else:
        db.session.rollback()
        db.session.remove()
  • template_filter:在使用 Jinja2 模板的时候自定义过滤器。
@app.template_filter("upper")
def upper_filter(s):
    return s.upper()
  • context_processor:上下文处理器。使用这个钩子函数,必须返回一个字典。这个字典中的值在所有模版中都可以使用。这个钩子函数的函数是,如果一些在很多模版中都要用到的变量,那么就可以使用这个钩子函数来返回,而不用在每个视图函数中的 render_template 中去写,这样可以让代码更加简洁和好维护。
@app.context_processor
def context_processor():
    if hasattr(g, 'user'):
        return {"current_user": g.user}
    else:
        return {}
  • errorhandler:errorhandler 接收状态码,可以自定义返回这种状态码的响应的处理方法。在发生一些异常的时候,比如 404 错误,比如 500 错误,如果想要优雅的处理这些错误,就可以使用 errorhandler 来处理。
@app.errorhandler(404)
def page_not_found(error):
    return 'This page does not exist', 404

13 before_first_request 和 before_request 详解

before_first_request:处理项目的第一次请求之前执行。

# ====before_first_request 在 V2.3 后已删除====
# @app.before_first_request
def init_first():
    print("====init first====")


# ====替代====
with app.app_context():
    init_first()

弃用
`before_first_request` 在 flask 2.2 中已弃用,将在 Flask2.3 中删除。现在在创建 app 时可以进行初始化操作:
> > ```python > # In place of something like this > @app.before_first_request > def create_tables(): > db.create_all() > ... > > # USE THIS INSTEAD > with app.app_context(): > db.create_all() > ```

before_request:在每次请求之前执行。通常可以用这个装饰器来给视图函数增加一些变量。

请求已经到达了 Flask,但是还没有进入到具体的视图函数之前调用。

一般这个就是在视图函数之前,我们可以把一些后面需要用到的数据先处理好,方便视图函数使用。

# 每次请求前执行
@app.before_request
def init_before():
    print("====init before request====")

14 钩子函数 context_processor 详解

context_processor:上下文处理器。使用这个钩子函数,必须返回一个字典。这个字典中的值在所有模版中都可以使用。

使用场景

这个钩子函数的功能是,如果一些在很多模版中都要用到的变量,那么就可以使用这个钩子函数来返回,而不用在每个视图函数中的 render_template 中去写,这样可以让代码更加简洁和好维护。

@app.context_processor
def context_processor():
    return {"uname": "小龙女"}

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>这是主页内容</h1>
    <p>用户名:{{ uname }}</p>
</body>
</html>

home.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>用户中心</h1>
    <p>用户信息:{{ uname }}</p>
</body>
</html>


只要视图函数返回了模板,那么 context_processor 函数就会自动执行,哪怕模板中没有使用共享信息。

@app.route("/")
def index():
    print("====请求访问主页====")
    return "hello"


@app.route("/home/")
def home():
    print("====请求访问用户页面====")
    return render_template("home.html")
    

@app.context_processor
def context_processor():
    print("====向模板传递数据====")
    return {"uname": "小龙女"}

15 钩子函数 errorhandler 详解

errorhandler:在发生一些异常的时候,比如 404 错误,比如 500 错误,如果想要优雅的处理这些错误,就可以使用 errorhandler 来处理。

需要注意几点

  • 在 errorhandler 装饰的钩子函数中,一定要返回相应的状态码
  • 被 errorhandler 装饰的钩子函数,必须要写一个参数,来接收错误的信息,如果没有参数,就会直接报错。
  • 使用 flask.abort 可以手动的抛出相应的错误,比如开发者在发现参数不正确的时候可以自己手动的抛出一个 400 错误。

假如我们在执行视图函数时,使用了 g 上不存在的对象,那么向浏览器返回报错信息,响应状态码为 500。

from flask import Flask, g

app = Flask(__name__)


@app.route("/")
def index():
    print(g.uname)
    return f"Hello"


if __name__ == '__main__':
    # 关闭调试模式
    # app.run(debug=True)
    app.run()

常见的 500 错误处理

@app.errorhandler(500)
def server_error(e):  # 必须要有一个形参来接收错误信息
    # 必须向返回相应的状态码
    return render_template("500.html"), 500

如果 server_error 没有形参会报错:

@app.errorhandler(500)
def server_error():  # 必须要有一个形参来接收错误信息
    # 必须向返回相应的状态码
    return render_template("500.html"), 500

如果 server_error 没有返回状态码浏览器会认为是正常访问:

@app.errorhandler(500)
def server_error(e):
    return render_template("500.html")

常见的 404 错误处理

@app.errorhandler(404)
def server_error(e):
    return render_template("404.html"), 404

Flask 中的 abort 函数可以手动的抛出相应的错误(如 400)

@app.route("/home/")
def home():
    abort(400)

16 Flask——信号机制

16.1 信号机制

大白话来说,类似于两方属于敌对关系时,某人在敌对方阵营进行交谈,一旦遇到特殊情况,某人便会发送信号,他的同伙接收(监听)到他发的信号后,同伙便会做出一系列的应对策略(进攻|撤退)。

flask 中的信号使用的是一个第三方插件,叫做 blinker。通过 pip list 看一下,如果没有安装,通过以下命令即可安装 blinker。

pip install blinker

16.2 自定义信号步骤

自定义信号可分为 3 步来完成。

  1. 创建一个信号;
  2. 监听一个信号;
  3. 发送一个信号。

以下将对这三步进行讲解:

  • 创建信号:定义信号需要使用到 blinker 这个包的 Namespace 类来创建一个命名空间。比如定义一个在访问了某个视图函数的时候的信号。示例代码如下:
from blinker import Namespace


# 1. 创建信号
space = Namespace()
fire_signal = space.signal("fire")
  • 监听信号:监听信号使用 fire_signal 对象的 connect 方法,在这个方法中需要传递一个函数,用来指定监听到这个信号后做该做的事情。示例代码如下:
# 2. 监听信号
def start_fire(sender, uname):
    print(f"{uname} start fire")


fire_signal.connect(start_fire)
  • 发送信号:发送信号使用 fire_signal 对象的 send 方法,这个方法可以传递一些其他参数过去。示例代码如下:
# 3. 发送信号
fire_signal.send(uname="小龙女")

16.3 代码演示

from flask import Flask
from blinker import Namespace

app = Flask(__name__)


@app.route("/")
def index():
    # 3. 发送信号
    fire_signal.send(uname="小龙女")
    return f"Hello"


# 1. 创建信号
space = Namespace()
fire_signal = space.signal("fire")


# 2. 监听信号
def start_fire(sender, uname):
    print(f"{uname} start fire")


fire_signal.connect(start_fire)

if __name__ == '__main__':
    app.run(debug=True)

17 Flask 信号使用场景 _ 存储用户登录日志

17.1 信号使用场景

用信号的方式,记录用户的登录信息即登录日志。

定义一个登录的信号,每有一个用户登录成功,就发送一个登录信号;在监听到这个信号以后,就记录当前这个用户登录的信息。

编写一个 signals.py 文件创建登录信号。

from blinker import Namespace
from flask import request, g

space = Namespace()

# 创建信号
login_signal = space.signal("login")


def login_signal_func(sender):
    ip = request.remote_addr
    # 记录信息
    info = f"{ip}:{g.uname}\n"
    # 向日志中写入信息
    with open("login.log", "a", encoding="utf-8") as f:
        f.write(info)


# 监听信号
login_signal.connect(login_signal_func)

使用信号存储用户登录日志。

from flask import Flask, request, g
from signals import login_signal

app = Flask(__name__)


@app.route("/")
def index():
    return f"Hello"


@app.route("/login/")
def login():
    # 模拟登录
    uname = request.args.get("uname")

    if uname:
        g.uname = uname
        # 发送信号,记录用户登录信息
        login_signal.send()
        return f"{uname} 登录成功"
    else:
        return "输入的用户名或密码不正确"


if __name__ == '__main__':
    app.run(debug=True)

18 Flask——内置信号

Flask 内置了 10 个常用的信号,这些信号不需要我们去创建和发送,只需要监听即可:

  1. template_rendered:模版渲染完成后的信号。
  2. before_render_template:模版渲染之前的信号。
  3. request_started:请求开始之前,在到达视图函数之前发送信号。
  4. request_finished:请求结束时,在响应发送给客户端之前发送信号。
  5. request_tearing_down:请求对象被销毁时发送的信号,即使在请求过程中发生异常也会发送信号。
  6. got_request_exception:在请求过程中抛出异常时发送信号,异常本身会通过 exception 传递到订阅(监听)的函数中。一般可以监听这个信号,来记录网站异常信息。
  7. appcontext_tearing_down:应用上下文被销毁时发送的信号。
  8. appcontext_pushed:应用上下文被推入到栈上时发送的信号。
  9. appcontext_popped:应用上下文被推出栈时发送的信号。
  10. message_flashed:调用了 Flask 的 flash 方法时发送的信号。

template_rendered 的使用

from flask import Flask, render_template, template_rendered

app = Flask(__name__)


@app.route("/")
def index():
    return f"Hello"


@app.route("/home/")
def home():
    return render_template("home.html")


def render_func(sender, template, context):
    print(sender)
    print(template)
    print(context)


# 监听信号
template_rendered.connect(render_func)

if __name__ == '__main__':
    app.run(debug=True)

got_request_exception 的使用

from flask import Flask, got_request_exception

app = Flask(__name__)


@app.route("/")
def index():
    return f"Hello"


@app.route("/home/")
def home():
    num = 100 / 0
    return "执行成功"


def exception_log(sender, exception):
    print(sender)
    print(f"===={exception}====")


got_request_exception.connect(exception_log)

if __name__ == '__main__':
    app.run(debug=True)

19 WTForms 介绍和基本使用

19.1 WTForms 介绍

这个插件库主要有两个作用。

第一个是做表单验证,对用户提交上来的数据进行验证看是否符合系统要求。

第二个是做模版渲染(了解即可)。

官网:https://wtforms.readthedocs.io/en/latest/index.html

Flask-WTF 是简化了 WTForms 操作的一个第三方库。WTForms 表单的两个主要功能是验证用户提交数据的合法性以及渲染模板。而 Flask-WTF 还包括一些其他的功能:CSRF 保护,文件上传等。

安装 Flask-WTF 默认也会安装 WTForms,因此使用以下命令来安装 Flask-WTF 和 WTForms:

pip install flask-wtf

19.2 WTForms 表单验证的基本使用

  1. 自定义一个表单类,继承自 wtforms.Form 类。
  2. 定义好需要验证的字段,字段的名字必须和模版中那些需要验证的 input 标签的 name 属性值保持一致
  3. 在需要验证的字段上,需要指定好具体的数据类型。
  4. 在相关的字段上,指定验证器。
  5. 以后在视图函数中,只需要使用这个表单类的对象,并且把需要验证的数据,也就是 request.form 传给这个表单类,最后调用 表单类对象.validate() 方法进行验证,如果返回 True,那么代表用户输入的数据都是符合格式要求的,如果为 Flase 则代表用户输入的数据是有问题的。如果验证失败了,那么可以通过 表单类对象.errors 来获取具体的错误信息。
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>注册</title>
</head>

<body>
    <form action="{{ url_for('register') }}" method="post">
        <table>
            <tr>
                <td>用户名</td>
                <td><input type="text" name="uname"></td>
            </tr>
            <tr>
                <td>密码</td>
                <td><input type="password" name="pwd"></td>
            </tr>
            <tr>
                <td>确认密码</td>
                <td><input type="password" name="pwd2"></td>
            </tr>
            <tr>
                <td colspan="2"><input type="submit" value="注册"></td>
            </tr>
        </table>
    </form>
</body>

</html>
from flask import Flask, request, render_template
from wtforms import Form, StringField
from wtforms.validators import Length, EqualTo

app = Flask(__name__)


@app.route("/")
def index():
    return f"Hello"


class RegisterForm(Form):
    uname = StringField(validators=[Length(min=2, max=10)])
    pwd = StringField(validators=[Length(min=2, max=10)])
    pwd2 = StringField(validators=[
        Length(min=2, max=10),
        EqualTo("pwd", message="两次密码不一致")
    ])


@app.route("/register/", methods=["GET", "POST"])
def register():
    if request.method == "GET":
        return render_template("register.html")
    else:
        form = RegisterForm(request.form)
        if form.validate():
            return "注册成功"
        else:
            return f"注册失败{form.errors}"


if __name__ == '__main__':
    app.run(debug=True)

20 WTForms 常用验证器

页面把数据提交上来,需要经过表单验证,进而需要借助验证器来进行验证,以下是常用的内置验证器:

  1. Length:字符串长度限制,有 min 和 max 两个值进行限制。
username = StringField(validators=[
    Length(min=3, max=10, message="用户名长度必须在3 到10位之间")
])
  1. EqualTo:验证数据是否和另外一个字段相等,常用的就是密码和确认密码两个字段是否相等。
password_repeat = StringField(validators=[Length(min=6, max=10), EqualTo("password")])
  1. Email:验证上传的数据是否为邮箱数据格式 如:223333@qq.com

    [!note] 注意
    想验证 email 需要安装下面的库。

    pip install wtforms[email]

email = StringField(validators=[Email()])
  1. InputRequired:验证该项数据为必填项,即要求该项非空。
username = StringField(validators=[input_required()])
  1. NumberRange:数值的区间,有 min 和 max 两个值限制,如果处在这两个数字之间则满足。
age = IntegerField(validators=[NumberRange(12, 18)])
  1. Regexp:定义正则表达式进行验证,如验证手机号码。
phone = StringField(validators=[Regexp(r'1[34578]\d{9}')])
  1. URL:必须是 URL 的形式 如 https://www.mi.com
home_page = StringField(validators=[URL()])
  1. UUID:验证数据是 UUID 类型。
uuid = StringField(validators=[UUID()])

数据项的类型,一般常用的有:

from wtforms import Form, StringField, IntegerField


class RegisterForm2(Form):
    uname = StringField(validators=[InputRequired()])
    age = IntegerField(validators=[NumberRange(18, 40)])

register.html 文件

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>注册页面2</title>
</head>

<body>
    <form action="/register/" method="post">
        <table>
            <tr>
                <th>邮箱:</th>
                <td><input type="email" name="email"></td>
            </tr>
            <tr>
                <th>用户名:</th>
                <td><input type="text" name="uname"></td>
            </tr>
            <tr>
                <th>年龄:</th>
                <td><input type="number" name="age"></td>
            </tr>
            <tr>
                <th>手机号码:</th>
                <td><input type="text" name="phone"></td>
            </tr>
            <tr>
                <th>个人主页:</th>
                <td><input type="text" name="homepage"></td>
            </tr>
            <tr>
                <th>uuid:</th>
                <td><input type="text" name="uuid"></td>
            </tr>
            <tr>
                <th></th>
                <td><input type="submit" value="注册"></td>
            </tr>
        </table>
    </form>
</body>

</html>

formscheck.py 表单验证工具类文件

from wtforms import Form, StringField, IntegerField
from wtforms.validators import Email, Regexp, InputRequired, URL, NumberRange, UUID


class RegisterForm(Form):
    # 想验证 email 需要安装下面的库
    # pip install wtforms[email]
    email = StringField(validators=[Email()])
    uname = StringField(validators=[InputRequired()])
    age = IntegerField(validators=[NumberRange(18, 40)])
    phone = StringField(validators=[Regexp(r'^1[34578]\d{9}$')])
    homepage = StringField(validators=[URL()])
    uuid = StringField(validators=[UUID()])

main.py 文件

from flask import Flask, request, render_template
from formscheck import RegisterForm

app = Flask(__name__)


@app.route("/")
def index():
    return f"Hello"


@app.route("/register/", methods=['GET', 'POST'])
def register():
    if request.method == 'GET':
        return render_template("register.html")
    else:
        form = RegisterForm(request.form)
        if form.validate():
            return "注册成功"
        else:
            return f"注册失败[{form.errors}]"


if __name__ == '__main__':
    # app.run(debug=True)
    app.run()
注意
想要用 WTForm 验证邮箱地址需要安装一个依赖包:
> > ```sh > pip install wtforms[email] > ```

21 WTForms 自定义验证器

只有当 WTForms 内置的验证器不够使的时候,才需要使用自定义验证器。

如果想要对表单中的某个字段进行更细化的验证,那么可以针对这个字段进行单独的验证。

自定义验证器步骤如下

  1. 定义一个类继承 wtforms 包中的 Form 类。
  2. 定义一个方法,方法的名字规则是:validate_字段名(self, field)
  3. 在方法中,使用 field.data 可以获取到这个字段的具体的值。
  4. 验证时,如果数据满足条件,那么可以什么都不做。如果验证失败,那么应该抛出一个 wtforms.validators.ValidationError 的异常,并且把验证失败的信息传到这个异常类中。

场景:验证码实现

formscheck.py

from flask import session
from wtforms import Form, StringField
from wtforms.validators import Length, InputRequired, ValidationError


class LoginForm(Form):
    uname = StringField(validators=[
        Length(min=2, max=6),
        InputRequired()
    ])
    pwd = StringField(validators=[Length(min=6, max=20)])
    code = StringField(validators=[Length(4, 4)])

    def validate_code(self, field):
        inp_code = field.data
        server_code = session.get("code")

        # <class 'str'>===8323
        # print(f"用户输入:{type(inp_code)}==={inp_code}")
        # 后端数据:<class 'int'>===8323
        # print(f"后端数据:{type(server_code)}==={server_code}")
        
        if str(server_code) != inp_code:
            raise ValidationError("验证码错误!!")

main.py

from flask import Flask, request, render_template, session
from random import randint
from formscheck import LoginForm

app = Flask(__name__)

app.secret_key = "adfsrewfseafa"


@app.route("/")
def index():
    return f"Hello"


@app.route("/login/", methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        code = randint(1000, 9999)
        session['code'] = code
        return render_template("login.html", code=code)
    else:
        form = LoginForm(request.form)
        if form.validate():
            return "登录成功"
        else:
            return f"登录失败{form.errors}"


if __name__ == '__main__':
    app.run(debug=True)

login.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>注册页面2</title>
</head>

<body>
    <form action="{{ url_for('login') }}" method="post">
        <table>
            <tr>
                <th>用户名:</th>
                <td><input type="text" name="uname"></td>
            </tr>
            <tr>
                <th>密码:</th>
                <td><input type="password" name="pwd"></td>
            </tr>
            <tr>
                <th>验证码:</th>
                <td>
                    <input type="text" name="code" maxlength="4">
                    <span style="background-color: deepskyblue;">{{ code }}</span>
                </td>
            </tr>
            <tr>
                <th></th>
                <td><input type="submit" value="登录"></td>
            </tr>
        </table>
    </form>
</body>

</html>

22 WTForms 渲染模版

渲染模版是 WTForms 的第二个作用,不过,我们只需要了解即可,不需要花太多精力和时间去研究它。

formscheck.py 文件

from wtforms import Form, StringField, IntegerField, BooleanField, SelectField, PasswordField, EmailField, SubmitField
from wtforms.validators import DataRequired, Length, InputRequired, NumberRange, EqualTo, Email


class RegisterForm(Form):
    # 文本框
    uname = StringField("用户名", validators=[
        DataRequired(),
        Length(min=1, max=10),
    ])
    # 邮箱
    email = EmailField("邮箱", validators=[
        Email(),
        DataRequired(),
    ])
    # 密码框
    pwd = PasswordField("密码", validators=[
        Length(min=6, max=10),
        DataRequired(),
    ])
    # 密码框
    pwd2 = PasswordField("确认密码", validators=[
        Length(min=6, max=10),
        EqualTo("pwd"),
        DataRequired(),
    ])
    # 数字框
    age = IntegerField("年龄", validators=[
        InputRequired(),
        NumberRange(min=1, max=120)
    ])
    # 多选框
    address = SelectField('地址', choices=[
        ('bj', '北京'),
        ('sh', '上海'),
        ('gz', '广州')
    ])
    # 单选框
    remember = BooleanField('记住我')
    # 提交按钮
    submit = SubmitField("注册")

main.py 文件

from flask import Flask, request, render_template
from formscheck import RegisterForm

app = Flask(__name__)


@app.route("/")
def index():
    return f"Hello"


@app.route("/register/", methods=['GET', 'POST'])
def register():
    if request.method == "GET":
        form = RegisterForm()
        return render_template("register.html", form=form)
    elif request.method == "POST":
        form = RegisterForm(request.form)
        if form.validate():
            return "注册成功"
        else:
            return f"注册失败{form.errors}"
    else:
        return "不支持的请求"


if __name__ == '__main__':
    app.run(debug=True)

create_form.html 文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .my-inp {
            background-color: #049ec4;
        }

        #pwd {
            background-color: #049ec4;
        }
    </style>
</head>
<body>
    <form action="{{ url_for('register') }}" method="post">
        <table>
            <tr>
                <th>{{ form.uname.label }}</th>
                <td>{{ form.uname(class="my-inp") }}</td>
            </tr>
            <tr>
                <th>{{ form.email.label }}</th>
                <td>{{ form.email() }}</td>
            </tr>
            <tr>
                <th>{{ form.pwd.label }}</th>
                <td>{{ form.pwd(id="pwd") }}</td>
            </tr>
            <tr>
                <th>{{ form.pwd2.label }}</th>
                <td>{{ form.pwd2() }}</td>
            </tr>
            <tr>
                <th>{{ form.age.label }}</th>
                <td>{{ form.age() }}</td>
            </tr>
            <tr>
                <th>{{ form.address.label }}</th>
                <td>{{ form.address() }}</td>
            </tr>
            <tr>
                <th>{{ form.remember.label }}</th>
                <td>{{ form.remember() }}</td>
            </tr>
            <tr align="center">
                <td colspan="2">{{ form.submit() }}</td>
            </tr>
        </table>
    </form>
</body>
</html>

23 Flask 安全上传文件——访问文件

上传文件步骤

  1. 在模版 html 中,表单需要指定 enctype='multipart/form-data' 才能上传文件。
  2. 在后台如果想要获取上传的文件,那么应该使用 request.files.get('input 的 name 值') 来获取。
  3. 保存文件之前,先要使用 werkzeug.utils.secure_filename 来对上传上来的文件名进行一个过滤。能保证不会有安全问题。
    比如文件名中含有 /,而在服务器中,/ 代表有一层目录,所以最后图片存储的位置可能和我们设想的不一致。
  4. 获取到上传上来的文件后,使用 文件对象.save(路径) 方法来保存文件。
    路径 = 完整路径 = 路径名 + 文件名

upload.html 页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="{{ url_for("upload") }}" method="post" enctype="multipart/form-data">
        上传文件:<input type="file" name="pic"><br>
        <input type="submit" value="提交">
    </form>
    {% if msg %}
        <p style="color:red;">{{ msg }}</p>
    {% endif %}
</body>
</html>

app.py 文件

import os.path

from flask import Flask, request, render_template
from werkzeug.utils import secure_filename

app = Flask(__name__)
UPLOAD_PATH = os.path.join(os.path.dirname(__file__), "static/upload_imgs")


@app.route("/")
def index():
    return f"Hello"


@app.route("/upload/", methods=["POST", "GET"])
def upload():
    if request.method == "GET":
        return render_template("index.html")
    elif request.method == "POST":
        img_file = request.files.get("pic")

        if img_file is None:
            return render_template("index.html", msg="请先上传文件")

        # file_name = img_file.filename
        # 对文件名作安全转换
        file_name = secure_filename(img_file.filename)
        # 保存文件
        img_file.save(os.path.join(UPLOAD_PATH, file_name))

        return f"{file_name} 上传成功"
    else:
        return "不支持的方式"


if __name__ == '__main__':
    app.run(debug=True)

访问文件

从服务器上读取文件,应该定义一个 url 与视图函数,来获取指定的文件。

在这个视图函数中,使用 send_from_directory(文件的目录, 文件名) 来获取。

@app.route("/download/<filename>")
def download_file(filename):
    return send_from_directory(UPLOAD_PATH, filename)

24 利用 flask-wtf 验证上传的文件

关键点

  1. 定义验证表单类的时候,对文件类型的字段,需要采用 FileField 这个类型,即 wtforms.FileField
  2. 验证器需要从 flask_wtf.file 中导入。 flask_wtf.file.FileRequiredflask_wtf.file.FileAllowed
  3. flask_wtf.file.FileRequired 是用来验证文件上传不能为空。
  4. flask_wtf.file.FileAllowed 用来验证上传的文件的后缀名, 如常见图片后缀 .jpg.png 以及.gif 等。
  5. 在视图函数中,需要使用 from werkzeug.datastructures import CombinedMultiDict 来把 request.formrequest.files 来进行合并。
  6. 最后使用 表单验证对象.validate() 进行验证。

代码如下

upload.html 页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="{{ url_for("upload") }}" method="post" enctype="multipart/form-data">
        上传文件:<input type="file" name="pic"><br>
        <input type="submit" value="提交">
    </form>
    {% if msg %}
        <p style="color:red;">{{ msg }}</p>
    {% endif %}
</body>
</html>

formscheck.py 文件

from flask_wtf.file import FileRequired, FileAllowed
from wtforms import Form, FileField


class UploadForm(Form):
    pic = FileField(validators=[FileRequired(), FileAllowed(['jpg', 'jpeg', 'png'])])

app.py 文件

import os.path

from werkzeug.datastructures import CombinedMultiDict

from forms_check import UploadForm
from flask import Flask, render_template, request
from werkzeug.utils import secure_filename

app = Flask(__name__)
UPLOAD_PATH = os.path.join(os.path.dirname(__file__), "imgs")


@app.route("/")
def index():
    return f"Hello"


@app.route("/upload/", methods=["POST", "GET"])
def upload():
    if request.method == "GET":
        return render_template("index.html")
    elif request.method == "POST":
        form = UploadForm(CombinedMultiDict([request.form, request.files]))
        if form.validate():
            img_file = form.pic.data
            # 进行安全转换
            file_name = secure_filename(img_file.filename)
            # 保存文件
            img_file.save(os.path.join(UPLOAD_PATH, file_name))

            return f"{file_name} 上传成功"
        else:
            return f"{form.errors}"

    return "不支持的访问"


if __name__ == '__main__':
    app.run(debug=True)

25 Restful 介绍

  1. Restful 接口规范:

    REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful。

    RESTful 是一种软件架构风格、设计风格,而不是标准,只是提供了一组设计原则和约束条件。

    它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次。

    RESTful 接口规范是用于在前端与后台进行通信的一套规范。使用这个规范可以让前后端开发变得更加轻松。

  2. 适用场景:

    一个系统的数据库数据,展现的平台有 PC 端、移动端、app 端、ios 端。

    前端工程师:都遵循 RESTful 编程规范。

    后端工程师:都遵循 RESTful 编程规范。

    最终结果:开发效率高,便于管理。

  3. 协议:

    用 http 或者 https 协议。

  4. 数据传输格式:

    数据传输的格式应该都用 json 格式。

  5. 链接规则:

    url 链接中,不能有动词,只能有名词。

    并且对于一些名词,如果出现复数,那么应该在后面加 s

    比如:获取新闻列表,应该使用 /news/,而不应该使用 /get_news/

  6. HTTP 请求方式:

    GET:从服务器上获取资源。

    POST:在服务器上新增或者修改一个资源。

    PUT:在服务器上更新资源(客户端提供所有改变后的数据)。

    PATCH:在服务器上更新资源(客户端只提供需要改变的属性)。

    DELETE:从服务器上删除资源。

  7. 状态码:

状态 码 原因描述 描述
200 OK 服务器成功响应客户端的请求。
400 INVALID REQUEST 用户发出的请求有错误,服务器没有进行新建或修改数据的操作。
401 Unauthorized 用户没有权限访问这个请求。
403 Forbidden 因为某些原因禁止访问这个请求。
404 NOTFOUND 用户请求的 url 不存在。
406 NOT Acceptable 用户请求不被服务器接收(比如服务器期望客户端发送某个字段, 但是没有发送)。
500 Internal server error 服务器内部错误,比如遇到 bug。

26 Restful 的基本使用

26.1 介绍

优势

Flask-Restful 是一个专门用来写 restful api 的一个插件。

使用它可以快速的集成 restful api 接口功能。

在系统的纯 api 的后台中,这个插件可以帮助我们节省很多时间。

缺点

如果在普通的网站中,这个插件就没有优势了,因为在普通的网站开发中,是需要去渲染 HTML 代码的,

而 Flask-Restful 在每个请求中都是返回 json 格式的数据。

26.2 安装

pip install flask-restful

26.3 基本使用

定义 Restful 的类视图:

  1. flask_restful 中导入 Api ,来创建一个 api 对象。
  2. 写一个类视图,让他继承自 Resource 类,然后在这个里面,使用你想要的请求方式来定义相应的方法,比如你想要将这个类视图只能采用 post 请求,那么就定义一个 post 方法。
  3. 使用 api.add_resource 来添加类视图与 url
from flask import Flask, url_for
from flask_restful import Resource, Api

app = Flask(__name__)
# 建立 Api 对象,并绑定应用 APP
api = Api(app)


class LoginView(Resource):
    def get(self):
        return {"flag": True}

    def post(self):
        return {"flag": False}


# 建立路由映射
api.add_resource(LoginView, "/login/", "/login2/", endpoint="login")

with app.test_request_context():
    # werkzeug.routing.BuildError: Could not build url for endpoint 'LoginView'.
    # Did you mean 'loginview' instead?
    # 默认没有写 endpoint,url_for 函数通过小写的自定义的 resource 类名来反向获取 url
    # 如果有多个 url,会返回第 1 个 URL
    print(url_for("login"))

if __name__ == '__main__':
    app.run(debug=True)
注意
1. 如果你想返回 json 数据,那么就使用 flask_restful,如果你是想渲染模版,那么还是采用之前的方式,就是 `app.route` 的方式。
> 2. url 还是跟之前的一样,可以传递参数。也跟之前的不一样,可以指定多个 url。 > 3. endpoint 是用来给 url_for 反转 url 的时候指定的。如果不写 endpoint,那么将会使用**视图类名**的**小写**来作为 endpoint。 > 4. 如果在 add_resource 中指定了多个 url,那么 url_for 函数返回第一个 url。 > 5. add_resource 的第二个参数是访问这个视图函数的 url,这个 url 可以跟之前的 route 一样,可以传递参数,并且还有一点不同的是,这个方法可以传递多个 url 来指定这个视图函数。

27 Flask_RESTful 参数验证

27.1 参数验证

参数验证也叫参数解析。

Flask-Restful 插件提供了类似 WTForms 来验证提交的数据是否合法的包,叫做 reqparse。

27.2 基本用法

  1. 通过 flask_restful.reqparseRequestParser 建立解析器。
  2. 通过 RequestParser 中的 add_argument 方法定义字段与解析规则。
  3. 通过 RequestParser 中的 parse_args 来解析参数。
    1. 解析正确,返回正确参数。
    2. 解析错误,返回错误信息给前端。

27.3 add_argument 方法参数详解

add_argument 方法可以指定这个字段的名字,这个字段的数据类型等,验证错误提示信息等,具体如下:

  1. default:默认值,如果这个参数没有值,那么将使用这个参数指定的默认值。
  2. required:是否必须。默认为 False,如果设置为 True,那么这个参数就必须提交上来。
  3. type:这个参数的数据类型,如果指定,那么将使用指定的数据类型来强制转换提交上来的值。可以使用 python 自带的一些数据类型 (如 str 或者 int),也可以使用 flask_restful.inputs 下的一些特定的数据类型来强制转换。
    1. url:会判断这个参数的值是否是一个 url,如果不是,那么就会抛出异常。
    2. regex:正则表达式。
    3. date:将这个字符串转换为 datetime.date 数据类型。如果转换不成功,则会抛出一个异常。
  4. choices:固定选项。提交上来的值只有满足这个选项中的值才符合验证通过,否则验证不通过。
  5. help:错误信息。如果验证失败后,将会使用这个参数指定的值作为错误信息。
  6. trim:是否要去掉前后的空格。

27.4 代码案例


28 Flask_RESTful 规范返回值

对于一个类视图,可以指定好一些字段做标准化用于返回。

以后使用 ORM 模型或者自定义模型的时候,他会自动的获取模型中的相应的字段,

生成 json 格式数据,然后再返回给客户端。

28.1 使用方法

  1. 导入 flask_restful.marshal_with 装饰器
  2. 定义一个字典变量来指定需要返回的标准化字段,以及该字段的数据类型

在请求方法中,返回自定义对象的时候,flask_restful 会自动的读取对象模型上的所有属性。

组装成一个符合标准化参数的 json 格式字符串返回给客户端


29 Flask_RESTful 规范返回值 - 参数设置

29.1 设置重命名属性和默认值

问题
规范给出的属性名和模型内部的属性名不相同
解决方案
使用 `attribute` 配置这种映射,比如: `fields.String(attribute='username')`
问题
某些字段,没有值,但想给一个值做为默认值
解决方案
使用 `default` 指定默认值,比如: `fields.String(default='sxt')`

30 Flask_RESTFul 规范返回值 - 类型设置

大型的互联网项目中,返回的数据格式,有时是比较复杂的结构。

如:豆瓣电影

https://movie.douban.com/j/chart/top_list?type=24&interval_id=100%3A90&action=&start=20&limit=20

返回的值里有 json 或者列表数据,这时可以通过以字段来实现

  • fields.List 放置一个列表
  • fields.Nested 放置一个字典

31 Flask_RESTful 结合蓝图使用

31.1 Flask_RESTful 结合蓝图使用

在蓝图中,如果使用 Flask_RESTful,创建 Api 对象的时候,传入蓝图对象即可,不再是传入 app 对象

user/views.py


user/__init__.py


app.py


32 Flask_RESTful 渲染模版

渲染模版就是在 Flask_RESTful 的类视图中要返回 html 片段代码,或者是整个 html 文件代码。

如何需要浏览器渲染模板内容应该使用 api.representation 这个装饰器来定义一个函数,

在这个函数中,应该对 html 代码进行一个封装,再返回。

注意
`api.representation` 装饰器修饰的函数必须返回一个 Response 对象

posted @ 2026-04-11 02:05  挖掘鱼  阅读(4)  评论(0)    收藏  举报