Flask 框架高级(上)

作者: pdnbplus | 发布时间: 2024/06/17 | 阅读量: 200

Flask 框架高级(上) -- 潘登同学的 flask 学习笔记

Cookie

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

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

Cookie工作原理

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

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

注意

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

对 Cookie 的增删改查

  • 新增 Cookie(通过 response 对象)
from flask import Flask,make_response

# 创建对象
app = Flask(__name__)
# 路由地址
@app.route("/")

def index():
    return "pandeng"


@app.route('/set_cookie/')

def set_cookie():
    resp = make_response('设置了一个Cookie信息')
    resp.set_cookie('uname','pd')
    return resp

if __name__ == "__main__":
    app.config.from_pyfile("./setting.py")
    app.run()

新增Cookie

  • 在后端查看 cookie(通过 request 对象,既然能查当然也能改)
@app.route('/get_cookie/')

def get_cookie():
    uname = request.cookies.get('uname')
    return f'Cookie里面,uname的内容是{uname}'
  • 删除 cookie(通过 response 对象)(这样只是删除值,要是想把键也删掉可以在浏览器中手动删除)
@app.route('/del_cookie/')

def del_cookie():
    resp = make_response('删除了一个Cookie信息')
    resp.delete_cookie('uname')
    return resp

Cookie 的有效期

  • 默认的过期时间:如果没有显示的指定过期时间,那么这个 cookie 将会在浏览器关闭后过期。
  • max_age:以秒为单位,距离现在多少秒后 cookie 会过期。
  • expires:为 datetime 类型。这个时间需要设置为格林尼治时间,相对北京时间来说 会自动+8 小时

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

注意

  • max_age 在 IE8 以下的浏览器是不支持的。
  • expires 虽然在新版的 HTTP 协议中是被废弃了,但是到目前为止,所有的浏览器都还是能够支持,所以如果想要兼容 IE8 以下的浏览器,那么应该使用 expires,否则可以使用 max_age。
@app.route('/set_cookie/')

def set_cookie():
    resp = make_response('设置了一个Cookie信息')
    # temp_time = datetime(2022,1,12,hour=18,minute=0,second=0)
    age = 60*60*2  # 设置两个小时后到期
    # resp.set_cookie('uname','pd',expires=temp_time)
    resp.set_cookie('uname','pd',max_age=age)
    return resp

由于datetime也可以这样操作

@app.route('/set_cookie1/')

def set_cookie1():
    resp = make_response('设置了一个Cookie1信息')
    # 设置标准时间的 两个小时后到期(就是10个小时后)
    temp_time = datetime.now() + timedelta(hours=2)
    resp.set_cookie('uname','pd',expires=temp_time)
    return resp

Session

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

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

客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了。

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

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

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

Session 的跟踪机制

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

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

Session运行机制

  • 问题:若客户端禁用了浏览器的 Cookie 功能,session 功能想继续保留,该咋整?给出你的实现思路

答:URL 地址携带 SessionID

设置 Session 的盐

from os import urandom

# 直接设置  内容随便设置
app.secret_key = "houwehvowv"

'''
# 类方式
class DefaultConfig:
    SERECT_KEY = urandom(24) # 设置长度为24的字符串

app.config.from_object(DefaultConfig)
'''

Session 的增删改查

# 记得先设置Session的盐

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

@app.route("/get_session/")
def get_session():
    # 能查当然能改
    uname = session.get('uname')
    # 根据uame值,从数据库中查询
    return f'session里面,uname的内容是{uname}'

@app.route("/del_session/")
def del_session():
    # pop删除一个key
    session.pop('uname')
    # 可以用clear删除全部信息
    # session.clear()
    return '删除了一个session对象'

Session 的有效期

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

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

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

  • session.permanent=True
  • 可以设置 app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hour=2) 在两个小时后过期。
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=2)  # 设置session两天后过期

@app.route("/set_session/")
def set_session():
    # 设置session的持久化
    session.permanent=True
    session['uname'] = 'pandeng'
    return '设置了一个session对象'

深入 Session

  • 问题:假如 Session 的有效期是:2days,在第一天晚上的时候,服务器突然崩掉了,那么 Session 还能保存吗?

app.secret_key随机的情况下,先进入http://127.0.0.1:5000/set_session/, 再进入http://127.0.0.1:5000/get_session/,能正常显示;关掉进程,重新打开,直接进入http://127.0.0.1:5000/get_session/,返回的 session 值为None

app.secret_key写死的情况下,先进入http://127.0.0.1:5000/set_session/, 再进入http://127.0.0.1:5000/get_session/,能正常显示;关掉进程,重新打开,直接进入http://127.0.0.1:5000/get_session/,也能正常显示;

核心理解就是: session 其实就是保留的,但是密钥的服务器独有的,session 是服务器颁发的唯一的,只要用服务器的密钥解不开,就是 None;所以也可以理解为密钥写死下,重启服务器,Session 不会过期;随机下,重启服务器就会过期(实际没过期,只是服务器看不懂了)

Session 实战 用户免登录

拿之前的代码改吧改吧就成了!!

  • 1.前面那个登录注册的文件, './10_类视图的使用/login.html'
<div class="zhuce_box">
  <div class="beijing_box"></div>
  <div class="zhuce_con_box">
    <div class="zhuce_nav clearfix">
      <a href="/" class="zhuce_logo_box fl"
        ><img src="../../static/img/logo.png" alt=""
      /></a>
      <p class="phone_zhuce_box fr">
        <img
          src="../../static/img/zhucephone.png"
          alt=""
        />联系电话:18514000360
      </p>
    </div>
    <div class="zhuce_info_box denglu_info_box clearfix">
      <div class="zhuce_txt_box fl">
        <h2>百战 老师好!!</h2>
        <p>让人人享有高品质教育</p>
      </div>
      <div class="user_in_con_b fr">
        <div class="user_in_info">
          <ul class="login_btn clearfix">
            <li class="user_li_on">
              <a href="{{ url_for('login') }}">登录</a>
            </li>
            <li class="user_li_on1">
              <a href="{{ url_for('register') }}">注册</a>
            </li>
          </ul>
          <div class="user_dengzhu_box">
            <div class="user_dengzhu_info">
              <form method="post" action="/login/">
                <div class="user_input_info">
                  <input
                    type="text"
                    placeholder="用户名"
                    name="uname"
                    class="login_phone"
                  />
                  <p class="info_error error_login_phone"></p>
                </div>
                <div class="captcha">
                  <div id="your-dom-id" class="nc-container"></div>
                  <input type="hidden" name="captcha" value="0" />
                </div>
                <div class="phone_code">
                  <div class="phone_code_info">
                    <input
                      type="text"
                      placeholder="密码"
                      name="pwd"
                      class="dxcode login_code"
                    />
                    <input
                      type="button"
                      value="获取验证码"
                      class="get_dxcode get_login_code"
                    />
                  </div>
                  <p class="info_error error_login_code">
                    {{ msg | default('',boolean=True) }}
                  </p>
                </div>
                <div class="zhuce_btn">
                  <input type="submit" class="submit" value="登录" />
                </div>
              </form>
              <div class="user_zhu_txt">
                手机号不能用?<a href="">通过申诉更改手机号</a>
              </div>
              <a href="" class="weixin_button"
                ><span class="icon iconfont icon-weixin6"></span
                >微信扫码一键登录</a
              >
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

{% block script %}
<script></script>
{% endblock %}
  • 2.'./10_类视图的使用/register.html'基本不用改
{% extends "./10_类视图的使用/login.html" %} {% block title %} 注册页面 {%
endblock %} {% block script %}
<script>
  var login = document.querySelector(".user_li_on");
  var register = document.querySelector(".user_li_on1");
  login.setAttribute("style", "border-bottom: none;color:black;");
  register.setAttribute(
    "style",
    "border-bottom: 5px solid #00b683;color:#00b683;"
  );
</script>
{% endblock %}
  • 3.再拿一个之前用过的主页, ./06Jinja2模板/index1.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>用户主页</title>
    <style>
        a{
            float:right;
        }
    </style>
</head>
<body>
    <a href="{{ url_for('index') }}">回到首页</a>

    <h1>{{ uname }}: {{ age }}</h1>
    <p>最喜欢的金融课目是{{ hobby.finance }}</p>
    <p>最喜欢的体育项目是{{ hobby.sports }}</p>
    <p>最喜欢的编程语言是{{ hobby.it }}</p>
</body>
</html>
  • 4.主逻辑部分
from flask import Flask, session,views,render_template,request,redirect,url_for

app = Flask(__name__)
app.secret_key = 'vbdibv'

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

class LoginView(views.MethodView):
    def _jump(self,msg=None):
        return render_template('./10_类视图的使用/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 ==  'pandeng' and pwd == '123':
            # 验证用户信息
            session['uname'] = uname
            return redirect(url_for('user'))
        return self._jump(msg="用户名密码错误")


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

@app.route('/register/')
def register():
    return render_template('./10_类视图的使用/register.html')

@app.route("/user/")
def user():
    uname = session.get('uname')
    if uname:
        # 就从数据库中查hobby, 这里写死了
        hobby = {
            "finance":"quantify",
            "sports":"riding",
            "it":"Python",
        }
        return render_template('./06Jinja2模板/index1.html',uname=uname,hobby=hobby)
    else:
        return '请先' + '<a href="/login/">登录</a>'

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

最终效果

  • 第一次点进http://127.0.0.1:5000/user/

Session实战1

  • 要是登录错误就会反复登录

Session实战2

  • 登录正确就会跳转到用户主页

Session实战3

  • 要是再次输入http://127.0.0.1:5000/user/,就无需登录,不会再跳转去登录页面了

Local 对象

需求

  • 要实现并发效果, 每一个请求进来的时候我们都开启一个进程, 这显然是不合理的, 于是就可以使用线程
  • 那么线程中数据互相不隔离,存在修改数据的时候数据不安全的问题

复习进程线程协程

  • 进程:一条生产线就是一个进程
  • 线程:一条生产线上 10 个工人就是 10 个线程
  • 携程:一条生成线上 10 个工人有的工作量少,有的多,闲的人去帮忙干点事就是携程

错误想法

Local错误做法

正确想法

Local正确做法

Local 对象

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

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

flask = werkzeug + sqlalchemy + jinja2

ThreadLocal 变量

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

from threading import Thread,local

local = local()

local.request = '这个是请求的数据1'

class MyThread(Thread):
    def run(self):
        local.request = '我是pd'
        print('子线程:', local.request)


my_thread = MyThread()
my_thread.start()
my_thread.join()

print('主线程:', local.request)

可以看到主线程与子线程是相互分离,互不影响的

Thread内容

  • werkzeug 中的 Local
from threading import Thread
from werkzeug.local import Local

local = Local()

local.request = '这个是请求的数据1'

class MyThread(Thread):
    def run(self):
        local.request = '我是pd'
        print('子线程:', local.request)

my_thread = MyThread()
my_thread.start()
my_thread.join()

print('主线程:', local.request)

这个的结果与上面的是一致的。

总结

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

Flask_app 上下文

上下文(感性的理解)

每一段程序都有很多外部变量,只有像 add 这种简单的函数才是没有外部变量的。 一旦一段程序有了外部变量,这段程序就不完整,不能独立运行。为了能让这段程序可以运行,就要给所有的外部变量一个一个设置一些值。就些值所在的集合就是叫上下文。

并且上下文这一概念在中断任务的场景下具有重大意义,其中任务在被中断后,处理器保存上下文并提供中断处理,因些在这之后,任务可以在同一个地方继续执行。(上下文越小,延迟越小)

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

LocalStack栈

那到底是不是这样呢? 去看源码!!!

LocalStack源码1

LocalStack源码2

LocalStack源码3

from flask import Flask,current_app

app = Flask(__name__)

# 创建应用上下文
app_ctx = app.app_context()
app_ctx.push()
print(current_app.name)

'''
# 方法2
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)
  • 那么应用上下文到底有什么用呢?

上下文的一个典型应用场景就是用来缓存一些我们需要在发生请求之前或者要使用的资源。举个例子,比如数据库连接。当我们在应用上下文中来存储东西的时候你得选择一个唯一的名字,这是因为应用上下文为 Flask 应用和扩展所共享。

注意

在视图函数中,不用担心应用上下文的问题。因为视图函数要执行,那么肯定是通过访问url的方式执行的,
那么这种情况下,Flask底层就已经自动的帮我们把应用上下文都推入到了相应的栈中。

如果想要在视图函数外 则执行相关的操作,

Flask 请求上下文

请求上下文

请求上下文也是存放到一个 LocalStack 的栈中。和请求相关的操作就必须用到请求上下文,比如使用 url_for 反转视图函数;

错误示范--报错解决

from flask import Flask,current_app,url_for
app = Flask(__name__)

@app.route('/')
def index():
    return f'Hello,这是一个{current_app.name}应用'

@app.route('/test/')
def test():
    url = url_for('index')
    return f'这个是一个小测试-{url}'

url = url_for('index')
print(url)

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

RuntimeError: Attempted to generate a URL without the application context being pushed. This has to be executed when application context is available.

Flask请求上下文报错解决

原因就是因为没有手动推入请求上下文;

就算手动设置了:

with app.app_context():
    url = url_for('index')
    print(url)

RuntimeError: Application was not able to create a URL adapter for request independent URL generation. You might be able to fix this by setting the SERVER_NAME config variable.

Flask请求上下文报错解决1

怎么办? 回去康康源码!! 点进去url_for,发现其实不仅需要LocalStack的栈,还需要request的栈

LocalStack源码4

解决方案:

with app.test_request_context():
    url = url_for('index')
    print(url)

总结

为什么上下文需要放在栈中?

1.应用上下文:Flask 底层是基于 werkzeug,werkzeug 是可以包含多个 app 的,所以这时候用一个栈来保存。

如果你在使用 app1,那么 app1 应该是要在栈的顶部,如果用完了 app1,那么 app1 应该从栈中删除。方便其他代码使用下面的 app。

2.如果在写测试代码,或者离线脚本的时候,我们有时候可能需要创建多个请求上下文,这时候就需要存放到一个栈中了。

使用哪个请求上下文的时候,就把对应的请求上下文放到栈的顶部,用完了就要把这个请求上下文从栈中移除掉。(所以这也是使用 with 的原因)

G 对象(Flask 全局对象)

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

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

在主逻辑下,新建一个文件utils.py里面放处理业务的函数

from flask import g

def func_a():
    return f"最喜欢的金融课目是:{g.hobby['finance']}"

def func_b():
    return f"最喜欢的体育项目是:{g.hobby['sports']}"

def func_c():
    return f"最喜欢的编程语言是:{g.hobby['it']}"

回到主逻辑中

from flask import g
from utils import *

@app.route('/user/')
def user():
    hobby = {
            "finance":"quantify",
            "sports":"riding",
            "it":"Python",
        }
    g.hobby = hobby
    a = func_a()
    b = func_b()
    c = func_c()
    return f'PD <br> {a} <br> {b} <br> {c}'

还是那个熟悉的页面:

G对象(Flask全局对象)

最后来梳理一遍上面的过程,先是进入user()函数,然后数据赋给 G 对象,然后调用函数时,引用的 g 对象也是赋值后的 g 对象,可以正常执行函数