Flask框架高階(上)

2022-01-13 11:00:13

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物件,可以正常執行函數