Flask
Flask是一個基於python的,微型web框架。之所以被稱為微型是因為其核心非常簡單,同時具有很強的擴展能力。它幾乎不給使用者做任何技術決定。
安裝flask時應該注意其必須的幾個支持包比如Jinja2,Werkzeug等。如果使用easy_install或者pip這樣的安裝工具的話那么就不必擔心這么多了。另外flask還有一些可選的輔助模塊,使用它們可以讓程序更加簡潔易懂,比如SQLAlchemy用於數據庫的連接和操作,flask-WTForm用於網頁上表單的設計。
最簡單的一個flask的web應用如下:
from flask import Flask app = Flask(__name__) @app.route('/') def index(): return "<h1>Hello,World</h1>" if __name__ == '__main__': app.run(host='0.0.0.0',port=5000,debug=True,threaded=True)
因為這段很熟悉了,很多要素就不多做解釋。只是最近看書看到的,以前也不是很清楚的一點,就是Flask是基於flask的web應用的基礎,而flask程序在運行過程中會使用這個參數作為定位模板和其他靜態文件的基礎。當我們傳遞__name__時,就保證了不管這個腳本是以__main__形式被調用還是被其他腳本調用,都能讓app指代flask的web應用。換句話說,如果我們直接跑這個腳本的話,那么大可以把__name__直接換成"__main__",只不過這樣做會讓其他腳本調用這里的app時出錯。
在被route修飾的index函數在我這被稱為響應函數,響應函數一定要有返回值,這個例子中最后返回的是一個HTML的代碼,當然也可以返回XML,JSON消息體等等。另外需要注意的是在這個腳本里面,app.run開始之后的所有代碼都不會被執行,因為程序進入了監聽HTTP請求的模式。關於run方法中的幾個參數,host指定了監聽的ip地址,默認是127.0.0.1,即只有本地可以訪問而外部是無法訪問的;port是監聽端口,默認5000,debug是指出是否處於調試模式,如果是那么當程序執行出錯時的調用棧和錯誤信息全部都會返回給客戶端瀏覽器。當上生產環境的時候千萬記得要把這個改成False否則會有暴露代碼的風險。
■ 渲染模板
現在的網站很少有全靜態頁面的了,而動態頁面靠的就是模板的渲染。flask的默認模板引擎是Jinja2,它有一套自己的模板語言,一個簡單的模板長成下面這樣:
<html> <body> {% if name %} <h1>Hello,{{name}}!</h1> {% else %} <h1>Hello,Stranger!</h1> {% endif %} </body> </html>
這個模板體現了Jinja2中的選擇結構,類似的還有:
循環結構: {% for name in names %} <h1>Hello,{{ name }}</h1> {% endfor %}
宏(類似函數): {% macro rend_mess(item) %} <li>{{ item }}</li> {% endmacro %} 然后就可以在下文中 {% for item in items %} {{ rend_mess(item) }} {% endfor %}
引入外部文件: {% import 'macro.html' as macros %} <ul> {% for item in items %} {{ macros.rend_mess(item) }} {% endfor %} </ul>
這些模板文件,應該放在主腳本的同目錄下的templates目錄。
那么這些模板到底要如何和主程序關聯起來呢?這就是需要主程序中調用render_template這個函數。比如對於上面那個選擇結構的模板可以在某個路由下:
@app.route("/") @app.route('/<name>') def show_name(name=None): return flask.render_template('template.html',name=name)
從這段代碼中我們可以看到幾點。
首先,路由可以不是固定的,由<變量名>的形式可以制作動態路由(后面應該會在講路由的時候再細說)。
第二,函數的第一個參數只寫了模板的文件名而不是模板的路徑,這就是把模板放在固定的templates目錄下的意義。
第三,一個響應函數可以關聯多個路由,使這幾個路由都定向到這個函數。
第四,render_template函數的參數可以有很多,除了第一個指定了模板名之外,還有就是模板中變量的名字,比如這里的name,或者寫一個items=[1,2,3]就可以把這個items列表傳遞到{% for item in items %}中去。此例中如果不寫name參數也不會報錯,走else分支顯示Stranger就是了。如果響應函數沒有為name設置默認值None,這樣在訪問'/'的時候會報錯找不到變量name而此時訪問/Frank是OK的。這說明動態路由中變量的值被作為相同名字的參數的值傳遞到相應函數中去了。如果把name這個參數拿掉,那么不論訪問/還是/Frank都是報錯的,前者報錯因為找不到name這個變量的值,后者則是因為明明要傳遞一個值進響應函數做參數的但是響應函數卻規定沒有參數。
在模板渲染中還存在一個小問題,就是對HTML的特殊字符如 < > / 空格 這些字符的處理。在flask中有flask.Markup這個類,將特殊字符轉義成HTML可以識別的形式。比如下面這個例子:
print '<strong>Hi,%s</strong>' % '<blink>Dav</blink>' #獲得結果是<strong>Hi <blink>Dav</blink>!</strong> print flask.Markup('<strong>Hi %s</strong>') % '<blink>Dav</blink>' #得到結果是<strong>Hi <blink>Dav</blink>!</strong>
模板還存在繼承機制,用法是在子模板第一行寫上{% extends "xxx.html" %}。一般而言,在父類模板中會有{% block xxx %}...{% endblock %}這樣的標記,這表示這一片區域中的內容被歸屬於block xxx。在子模板中可以以block為單位對block中的內容進行重寫。此外在子模板中還可以調用super()宏,表示在子模板中的特定位置填充父模板這個block中的內容。如下面這個例子:
<!-- parent.html --> <html> <head> {% block head %} <meta charset="UTF-8"> <title>{% block title %}{% endblock %}</title> {% endblock %} </head> <body> {% block body %} <p>some original content</p> {% endblock %} </body> </html> <!-- child.html --> {% extends "parent,html" %} {% block title %}Title{% endblock %} {% block head %}{{ super() }}{% endblock %} {% block body %}{{ super() }}<h1>Hello,World!</h1>{% endblock %}
子模板就不用再寫一些重復的內容,只要對想要作出修改的block做出修改即可。需要注意的是重寫是覆蓋式的,所以如果要保留父模板原有內容的話就需要調用super。另外在這個例子中,看起來似乎是先對block title做出修改,然后再對block head重載,因為block head包含了block title,之前對title的修改可能會無效。但是不會。原因可能是因為針對block的修改是在block內非子block區域內,block之間看似是有包含關系的,實際上並不。
■ 重定向和錯誤處理
flask模塊中還自帶了處理重定向和錯誤處理的函數。
重定向是用了flask.redirect函數。比如在某個路由的響應函數下寫return redirect('/index'),這樣就可以把這個針對這個路由的訪問重定向到主頁去。注意是要return redirect函數返回的內容而不是直接執行redirect函數就好了。比如:
@app.route('/') def rootpath(): return redirect('/index')
HTTP訪問中難免出現各種各樣的錯誤,比如我們熟悉的404之類的。那么在編程中自然需要我們可以手動地拋出錯誤以及規定處理錯誤的方式。拋出錯誤用abort函數,用法如下:
@app.route('/error') def error(): abort(401) #之后的代碼都不會被執行
這樣就拋出了一個401錯誤,訪問/error這個路由就會報401錯了。有了拋出錯誤的操作自然就需要catch錯誤的操作。在flask中有一個裝飾器@app.errorhandler可以用來指定發生相關錯誤的時候應該如何處理。比如:
@app.errorhandler(400) def error_handler_400(error): #自帶一個error參數 return render_template("bad_request.html"),400
可以看到,錯誤處理的響應函數返回的不單單是一些html內容,而是一個元組,元組的第二項是這個錯誤的錯誤碼。需要注意的是,客戶端在頁面上看到的錯誤信息,是由響應函數返回值決定的。比如上例中bad_request.html中提示了401錯誤,那么客戶端看到的就是401錯誤的提示,盡管發生的是400錯誤。另一方面,返回元組的第二項,這個返回碼也是人為規定的,比如明明發生了401錯誤而我故意返回400,那么在客戶端瀏覽器控制台等地方看到的就是400返回碼,甚至可以返回一個666這種不存在的碼。當然返回一個標准規定的碼的好處在於,瀏覽器可以根據這個碼給出一些提示。
■ 路由和route函數
之前就說過了,路由中可以添加變量,以<variable_name>這種形式。再次強調一下,想要在路由中設置一個變量,這個變量必須聲明兩次。第一次是在route函數的第一個參數中,以<variable_name>的形式,第二次是以和相關路由關聯的響應函數的參數的形式,兩次聲明的變量必須同名。
進一步深挖route函數中的路由設置,還可以發現以下幾點。首先,路由中的變量可以設置類型。比如'/addone/<int:number>',這樣訪問這個路由時number這個變量的部分就自動被識別成一個int類型的變量傳入響應函數中處理。可用的類型除了int,還有float和path。其中path是默認值,即接受路徑的一個字符串。
另外還有一個小細節,路徑末尾還可以添加上一個額外的斜杠,比如@app.route('/school/'),這么做的意義在於,當訪問地址hostname/school和hostname/school/都可以訪問到。如果不寫這個斜杠,后者是無法訪問到的。
● 規定HTTP方法
route函數可以添加methods參數來規定相關路由認可的訪問HTTP方法,默認情況下是認可GET方法。比如:
@app.route('/SendMessage',methods=['GET','POST']) def sendMessage(): if request.method == 'GET': show_send_form() if request.method == 'POST': do_send() #request是flask中一個全局對象,用來抽象每一次訪問時的請求對象。
同時也可以通過對methods的控制來實現一個路由的靈活應用:
@app.route('/index',methods=['GET']) def index(): return render_template('index.html') @app.route('/index',methods=['POST']) def index_post(): return render_template('index_post.html')
● 反向生成路由地址
有時候,知道了某個響應函數,想要知道其對應的url路由,可以使用url_for這個函數。這個函數接受一個字符串函數名作為參數,然后返回本web應用中這個函數名作為響應函數的相關路由字符串。當路由中存在變量的時候,那么url_for函數應該添加上一些額外的,以變量名為名稱的參數才能確保正確返回路由字符串。當路由沒有變量而url_for有額外參數時默認將返回GET方法帶參數的URL。當多個路由關聯到同一個響應函數時,url_for只返回寫在最下面的那個修飾器的路由。舉例如下:
@app.route('/one') def one(): return 'one' @app.route('/two/<var>') def two(var): return 'two' @app.route('/three') @app.route('/four') def threeandfour(): return 'four' print url_for('one') #返回內容'/one' print url_for('one',var='MyVar') #返回內容'/one?var=MyVar' print url_for('two',var='MyVar') #返回內容'/two/MyVar',這里var這個參數是必須的否則報錯 print url_for('threeandfour') #返回內容'/four',而不是'/three'
*上面這個例子還不是很准確,url_for這個函數正確運行需要上下文的支持,也就是說要保證url_for這個函數所處的地方必須是一個app里面。一般而言把它寫在某個響應函數里面的話肯定沒有問題,但是如果像這樣想在一個app外面的url_for的話,可以在前面用with關鍵字指定上下文,比如:
with app.test_request_context(): print url_for("index",var="MyVar")
test_request_context方法用於告訴解釋器為在其作用域中的代碼模擬一個HTTP請求的上下文環境,這個環境由app提供。這樣就可以正確運行了。
另外,url_for可以加上參數_external=True來把原來生成的相對URI路徑轉換成帶域名的,完整的URL路徑。
在程序中應該盡量使用url_for這種靈活的函數,因為反向解析比硬編碼有更好的可讀性和維護性,另外url_for還會自動轉化一些需要轉義的特殊字符。比如上例中如果var=My Var,那么最終會自動把空格轉化成URL的形式,返回'/index/My%20Var'。
■ 靜態文件
一般所有flask項目都會內置一個默認的路由,是hostname/static/<filename>。這個路由是專門開放給客戶端用來訪問服務端的靜態文件的。它將映射到主腳本同目錄下的static目錄。需要注意的是這個目錄的名字只能是static,訪問路由時也必須完整地指出文件的名字,否則都是訪問失敗的。
static這個文件夾可以算是flask為開發者提供了一個窗口,讓外部訪問者可以接觸到一些服務端的文件,而哪些文件可以對外部開放就有開發者自己決定。
■ 上下文
上下文在web編程中是一個很重要的概念,它是服務器端獲得應用及請求相關信息的對象。從功能目的上說,上下文一般可以讓多次操作間共享一些信息,提高效率,而這里的“操作”有好多層面,可以是同一個客戶端的多次不同請求,可以是同一個請求的不同處理函數或者單次請求。
● 會話上下文
在web環境中,所謂的會話是一種客戶端和服務端保持狀態的解決方案。同一個用戶的多個請求共享一個會話對象。會話機制通常是通過cookie來實現。基本原理就是開啟會話時會在cookie中添加一條當前會話的SessionID作為這個會話標識。之后檢查會話的情況和存活就只要關注這個ID即可。
通俗來說,我打開一個瀏覽器后,會話成立,就可以往這個會話中存儲一些信息。操作期間我可能打開或關閉很多標簽頁或其他瀏覽器窗口,這期間會話一直保持不會斷,信息也可以一直共享。但是一旦把所有瀏覽器都關掉了,會話就斷了,再開一個瀏覽器后原會話中的信息也就不見了。
在flask中,我們不必再cookie層面上操作,flask自帶了一個flask.session對象,這個對象支持__setitem__和__getitem__等方法,所以可以像操作字典一樣操作這個對象。借用書上的一個例子:
app.secret_key = 'SECRET_KEY'
@app.route('/write') def write_session(): flask.session['time'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') return flask.session['time'] @app.route('/read') def read_session(): return flask.session.get('time') #訪問write可以將當前時間寫入會話對象中,然后緊接着訪問read,此時會話沒有改變所以可以獲取到上一次寫入時的時間。
關於session這個對象,還有幾點要提。它和之前說到過的url_for一樣必須在請求環境中調用,否則會報錯。呆想想也是這樣,如果它都不在請求環境中那程序哪會知道會話含有一些什么信息呢。然后session還有幾個自帶的屬性可以用來查看其狀態。比如session.new和session.modified,可以用來判斷當前的會話是不是剛剛新建的或者是剛剛被修改過。
在使用session前,一定要為app指定一個秘鑰,這個秘鑰用來加密sessionID。
● 全局對象
在flask中,可能會有一個請求觸發多個響應函數的情況,此時如果想要在不同的響應函數之間共享一些信息就需要全局對象,flask.g(沒錯,這個對象名就一個字母g)。g中存儲信息的方式不是__setitem__的中括號,而是__setattr__的點。
相比於session,g並不能跨瀏覽器甚至不能跨標簽頁地共享數據,但是在一個標簽頁內的一個請求過程中,g中存儲的數據可以共享。也就是說,g的生命周期和一個request是差不多長的。
一般函數式編程中,不同函數間的信息交流可以通過全局變量來實現。但是在flask這種裝飾器模式和線程環境下用全局變量的global不太合理或者說不太保險,所以用flask.g是一個更好的選擇。比如:
@app.route('/connect') def connect(): cursor = connect_to_db(addr,user,passwd) flask.g.cursor = cursor return 'connected' @app.route('/getdata') def get_data(): if hasattr(flask.g,cursor): return flask.g.cursor.get_data_from_db()
● 請求上下文
如果說flask.g是一個在服務端提供的,函數間通信的容器的話,request(請求上下文)就是承載了客戶端請求的信息的容器。而且因為要觸發響應函數必然需要一個請求,所以request這個對象天生就是帶有數據的(數據就是那些請求的信息了)。flask中包含了很多信息,如下:
request.method 本次請求的方法
host 服務端的主機名
args GET方法的請求的數據,是一個字典
form POST或者PUT方法請求中的數據,是一個字典
json 如果客戶端是通過JSON格式提交的數據,那么就可以通過本方法進行解析。
cookies 從客戶端傳送過來的cookie,以字典的形式讀寫
headers 請求頭信息,以字典的形式讀寫
files 訪問通過POST或者PUT方法向服務端上傳的文件。(詳細做法可參見文檔http://docs.jinkan.org/docs/flask/patterns/fileuploads.html)
URL族屬性,比如對於URL:"http://localhost/app/page.html?x=y"而言有:
base_url 代表http://localhost/app/page.html
path 代表/page.html
script_root 代表/app
url 代表http://localhost/app/page.html?x=y
url_root 代表http://localhost/app/page.html
● 回調接入點
其實在使用flask框架的時候,很多操作框架都代替開發人員做了,在這些操作的一些節點上如果我們想附加一些操作的話就要用到回調接入點。使用方法是把接入點函數作為一個裝飾器來裝飾某些函數。比如:
@app.before_request def before_request(): '''Something done before every request '''
接入點主要有下面這幾個:
before_request 每個請求在被處理之前首先被接入這個接入點,不必一定要返回一個什么值,且可以綁定多個處理函數,這種場合下解釋器將依次一一執行這些函數。
after_request 在每個請求的URL響應函數被調用后調用本接入點,其綁定的處理函數可以有一個固有參數response,代表request得到的response(此時還沒發送回給客戶端)。可以對這個response的內容進行修改和檢查。
teardown_request 和after_request基本類似,不過after_request不會在出錯的響應函數之后被調用。換言之,某個response產出前響應函數出錯了的話,after_request就沒用了,而teardown_request依然會被執行,所以teardown_request也可以被用於異常處理。
before_fisrt_request 這個是在app開始run之后接收到第一個請求之前做的事。之前在還沒有會用flask-script之前,用這個裝飾器來進行db.create_all()的工作
before_app_request 這個用起來似乎和before_request一樣,但是實際上是有微妙不同的。在應用中,我們點擊一個超鏈接發起的請求屬於用戶手動發起請求,而程序內設定的重定向,雖然也會向某個特定路徑發起請求,但是屬於程序自動發起的請求。before_request只在手動發起請求時被調用,before_app_request則不論手動自動,只要有請求發起就被調用。
下面是一個利用了回調接入點的,實現了緩存靜態頁面的常用邏輯:
from werkzeug.contrib.cache import SimpleCache from flask import request,render_template CACHE_TIMEOUT = 300 cache = SimpleCache() cache.default_timeout = CACHE_TIMEOUT @app.before_request #每個請求被處理之前先檢查想要的頁面是否在緩存里,如果在就可以直接返回它 def return_cached(): if not request.values: #當請求沒有附加數據時,認為是個單純的頁面請求,開始檢查緩存 response = cache.get(request.path) if response: print "Got the page from cache" return response print "Loading page..." @app.after_request #做出處理之后,再檢查回復的頁面是否在緩存里,如果不在就保存進去供下次快速反應使用。 def cache_response(response): if not request.values: cache.set(request.path,response,CACHE_TIMEOUT) return response @app.route('/get_index') def get_index(): return render_template("index.html")
寫了一下flask中比較基礎的一些內容,還有一些附加的模塊和其他更細節的東西會另開筆記寫。