構建一個web應用
前面的學習回顧:
- IDLE是Python內置的IDE,用來試驗和執行Python代碼,可以是單語句代碼段,也可以是文本編輯器中的多語句程序。
- 四個內置數據結構:列表、字典、集合和元組。
- 已經使用過的Python語句:if , elif , else , return , for , from , import 。
- 已經知道Python提供的豐富的標准庫,已經使用過的模塊:datetime , random , sys , os , time , html , pprint , setuptools , pip 。
- Python的內置函數,稱為BIF。已經用過的BIF:print , dir , help , range , list , len , input , sorted , dict , set , tuple , type 。
- 已經見過的操作符:in , not in , + , - , = , == , += , * 。
- 處理一個序列時,[ ] 除了可以擴展元素還可以支持切片,即指定開始,步長,結束。
- 用def 創建一個自己的函數。
- 利用pip 模塊把函數分組為模塊,模塊構成了代碼重用的基礎。
- Python中一切都是對象。
接下來我們要做的是利用之前的search_for_letters函數,讓人們可以在web瀏覽器上訪問這個函數提供的服務。關於這個函數的內容在前面的隨筆有介紹。
def search_for_letters(phrase: str, letters: str) -> set: '''FUNC:return a set of 'letters' found in 'phrase' . '''
return set(letters).intersection(set(phrase))
先來探索一下web是怎么工作的
無論在web上做什么,都與請求和響應有關。Web請求作為用戶交互的結果從一個web瀏覽器發送到一個web服務器。在web服務器上,會形成一個web響應(或應答),並返回給web瀏覽器。整個過程可以總結為五步:
- 你的用戶輸入一個web地址,或者單擊一個超鏈接,或者在他的web瀏覽器中單擊一個按鈕。
- Web瀏覽器將用戶的動作轉換為一個web請求,通過互聯網將它發送到一個服務器。
- Web服務器接收到這個請求,必須決定接下來做什么……
這時候有;兩種可能性。
如果web請求只是請求一個靜態的內容,如一個HTML文件、圖像或存儲在web服務器硬盤上的其他資源,web服務器會找到這個資源,准備把它作為一個web響應返回給web瀏覽器。
如果請求的是動態內容,也就是說必須生成的內容,如果搜索結果是一個在線購物車的當前內容,web服務器會運行一些代碼來生成web響應。
- web服務器通過互聯網將響應發回給正在等待的web瀏覽器。
- Web瀏覽器接收到web響應,把它顯示在用戶的屏幕上。
我們想讓web實現的功能
讓web瀏覽器與我們的web應用交互。在瀏覽器的地址欄輸入這個微博應用的URL地址來訪問應用的服務。瀏覽器上會出現一個頁面,要求用戶提供search_for_letters函數所需要的參數。一旦輸入參數,用戶點擊一個按鈕就會看到結果。
回憶這個函數的定義,它指出這個函數需要至少一個(至多兩個)參數:phrase和letters(要在phrase中搜索letters,letters是可選的,因為它的默認值aeiou)
def search_for_letters(phrase: str, letters: str)->set:
關於這web頁面什么樣,下面是我畫出的草圖:
當用戶單擊DO IT! 時,瀏覽器將這個數據發送給正在等待的web服務器,它會抽取phrase和letters的值,然后等待用戶調用search_for_letters函數。
我們需要做的事情
要構建一個能實際運行的服務器端web應用,還需要了解web應用框架,它提供了一組通用的基礎技術,可以基於這些技術構建web應用。在這里直接選擇一個名為Flask的流行框架,繼續我們的工作。
安裝Flask
Flask是一個第三方模塊,與標准庫模塊不同,需要先安裝才能導入和使用。
Python社區維護了一個集中管理第三方模塊的網站,名為PyPI(Python Package Index)
依舊是使用管理員運行cmd
python -m pip install flask
順便安裝了四個模塊如圖中所示,這些是flask的依賴包。
Flask提供了一組模塊可以幫助構建服務器端web應用。從理論上來講,這是一個微web框架,比較全面的web框架是Django,不過我們的需求不用這個復雜的框架,flask完全能夠勝任現在的需求。
檢查flask
創建一個新文件保存為hello_flask.py 放在一個名為webapp的文件夾中。
from flask import Flask app = Flask(__name__) #注意此處是兩個下划線
@app.route('/') def hello() -> str: return 'hello world from flask' app.run()
運行flask web應用
這是在那個文件夾里面同時按住shift和點擊右鍵,會出現在此處打開命令窗口,這樣一來可以免去切換目錄的問題。
輸入Python hello_flask.py會出現:
看到最后一行的內容表示一切正常,表示flask的服務器已經准備就緒,正在等待。現在打開一個你喜歡的瀏覽器,輸入剛才消息里面的URL地址:
http://127.0.0.1:5000/
瀏覽器窗口出現了剛才return后的內容,現在再來看剛才的web應用終端窗口,出現了新的狀態消息:
分析web應用的代碼
from flask import Flask
‘’’ 模塊名 類名
這里也可以直接寫import flask ,然后用flask.Flask指示類,不過這個方法在這里可讀性不好。’’’
app = Flask(__name__)
‘’’創建一個Flask類型的對象,把它賦給app變量。
__name__是什么?
這個值由Python解釋器維護,在程序代碼中任何地方使用這個值時,它會設置為當前活動模塊的名字。
Python中把__name__叫做dunder name ‘’’
@app.route('/')#函數修飾符,有一個@前綴。
#函數修飾符可以調整一個已有的函數的行為,而不修改函數的代碼!
‘’’這個web應用代碼中,app變量使用Flask的route修飾符。Route允許你將一個URL web路徑與一個已有的Python函數相關聯。在這里URL ‘/’將與下一行代碼中定義的函數關聯,這個函數名為hello。當一個指向‘/’URL的請求到達服務器時,route修飾符會安排Flask web服務器調用這個函數,然后route修飾符會等待所修飾的函數生成的輸出,再將輸出返回給服務器,然后服務器再將輸出返回給正在等待的web瀏覽器。’’’
def hello() -> str:
return 'hello world from Flask!'
#這個函數返回一個字符串,調用時返回消息:hello world from Flask!
app.run()
#讓web應用開始運行
此時,Flask會啟動它的內置web服務器,並在這個服務器中運行你的web應用代碼。Web服務器接收到指向‘/’URL的請求時,會響應“hello world from Flask!”消息,而指向其他URL請求會得到一個404 NOT FOUND錯誤消息。
而終端窗口中運行的web應用也會有一個消息更新狀態:
接下來修改hello_flask.py來包含第二個URL:/search
編寫代碼使這個URL與一個名為do_search函數相關聯,它會調用search_for_letters函數(從vsearch模塊中)然后讓do_search函數返回搜素時確定的結果,在這里要用短語“life, the universe, and everything!”中搜索字符串“eriu,!”。
新的代碼:
1 from flask import Flask 2 from vsearch import search_for_letters 3
4 app = Flask(__name__) 5
6 @app.route('/') 7 def hello() -> str: 8 return 'hello world from Flask'
9
10 @app.route('/search') 11 def do_search() -> str: 12 return str(search_for_letters('life,the universe,and everything!','eriu,!')) 13
14 app.run()
在命令提示符上終止剛才的終端(Ctrl+C),然后按向上的那個鍵,重新開啟一個web應用。現在處於等待狀態,然后在;瀏覽器輸入剛才定義的新的URL路徑。http://127.0.0.1:5000/search
瀏覽器出現了調用函數search_for_letters的結果:
在這里,127.0.0.1是localhost,也成為本地環回地址,而5000是協議端口號,是Flask默認的端口。
我們現在只是實現了一部分的任務,回想最開始的目的,還記得之前的草圖嗎?我們希望有一個web頁面接收輸入,另外還需要一個web頁面顯示結果。
像草圖中這樣的頁面,而不是{'e', 'i', '!', 'u', 'r', ','}。
構建HTML表單
利用模板引擎,程序員可以應用面向對象的繼承和重用概念來生成文本數據,如web頁面。
網站的外觀可以在一個頂層HTML模板中定義,這稱為基模板,然后其他HTML頁面繼承這個模板。如果對基模板進行修改,那么這個修改就會體現在所有繼承這個基模板的HTML頁面中體現。
Flask提供的模板引擎名為Jinja2,這里只作簡單的介紹,為我們所用的三個模板:base.html entry.html results.html
三個模板可以從這里下載http://python.itcarlow.ie/ed2/ch05/templates/
這個網址是我學習的書《Head First Python》提供的。
關於base.html
關於entry.html
用戶可以與這個HTML表單交互來提供web應用所需要的phrase和letters值。這個模板繼承了基模板,為名為body的塊提供了一個替代塊。
關於results.html
這個文件用來呈現搜索結果
在這里,對HTML的內容不做多的介紹,現在大概了解了它的一些基礎內容,會用即可。
從flask呈現模板
Flask提供了一個名為render_template的函數,如果指定一個模板名和所需的參數,調用這個函數時會返回一個HTML串。為了使用render_template,要把這個函數名增加到從flask模板導入的函數列表中,然后根據需要調用這個函數。把之前的web應用代碼文件hello_flask.py重命名一個更適合的名字:vsearch_for_web.py。並對該代碼進行以下修改:
- 導入render_template函數。
from flask import Flask , render_template
- 創建一個新的URL,這里是/entry。
@app.route(‘/entry’)
- 創建一個函數返回正確呈現的HTML。
def entry_page() -> ‘html’:
return render_template(‘entry.html’, the_title = ‘Welcome to search_for_letters on the web !’)
改完的代碼如下:
1 from flask import Flask, render_template 2 from vsearch import search_for_letters 3
4 app = Flask(__name__) 5
6 @app.route('/') 7 def hello() -> str: 8 return 'hello world from Flask
9
10 @app.route('/search') 11 def do_search() -> str: 12 return str(search_for_letters('life, the universe, and everything!', 'eriu,!')) 13
14 @app.route('/entry') 15 def entry_page() -> 'html': 16 return render_template('entry.html', the_title = 'Welcome to search_for_letters on the web!') 17
18 app.run()
需要在webapp文件夾里面創建如下的內容,這些內容都可以在剛才提供的網站里下載
Static文件夾里有一個名為hf.css的文件
現在已經准備好了,回到之前的命令提示符窗口(就是那個在webapp文件里面按住shift和右鍵然后點擊從此處運行的那個窗口),執行新的代碼:
python vsearch_for_web.py
窗口像剛才那樣出現了running
現在使用新的URL:http://127.0.0.1:5000/entry
瀏覽器出現了類似於草圖的窗口:
第一個對應/entry請求,第二個是對應瀏覽器對hf.css樣式表的請求有關。
雖然這個頁面看起來有些不美觀,但是我們現在先研究它能不能完成需要的功能。
現在輸入一個短語單擊do it!發生了一個錯誤:
看到服務器端窗口出現了一個內容:
關於http狀態碼
HTTP是允許web瀏覽器和服務器通信的協議。每個web請求都會生成一個http狀態碼響應。
狀態碼主要有五類:100類、200類、300類、400類和500類。
100~199范圍內的狀態碼是信息消息:服務器在提供關於客戶端請求的詳細信息。
200~299范圍內的狀態碼是成功消息:服務器已經接收、理解和處理客戶端的請求,一切正常。
300~399范圍內的狀態碼是重定向消息:服務器通知客戶端請求可以在別處處理。
400~499范圍內的狀態碼是客戶端錯誤消息:服務器從客戶端接收到一個它不理解也無法處理的請求,通常是客戶端的問題。
500~599范圍內的狀態碼是服務器錯誤消息:服務器從客戶端接收到一個請求,但是服務器嘗試處理這個請求時失敗了,通常是服務器的問題。
HTTP方法中的GET和POST
瀏覽器通常使用GET方法從web服務器請求一個資源,這是HTTP默認的方法。
POST方法允許web瀏覽器向服務器通過HTTP發送數據,這與HTML<form>標記相關聯。
可以讓flask web應用從瀏覽器接收提交的數據,為此要在@app.route行上提供一個額外的參數——methods。
下面對vsearch_for_web.py中的代碼進行修改:第十行增加了POST方法
這與entry.html文件中的第7行代碼對應,注意參數名method的單數復數形式。
下面打開調試模式,將最后一行的代碼改為:app.run(debug=True)
現在我們的代碼應該如下所示:
1 from flask import Flask, render_template 2 from vsearch import search_for_letters 3
4 app = Flask(__name__) 5
6 @app.route('/') 7 def hello() -> str: 8 return 'hello world from Flask'
9
10 @app.route('/search', methods = ['POST']) 11 def do_search() -> str: 12 return str(search_for_letters('life,the universe,and everything!','eriu,!')) 13
14 @app.route('/entry') 15 def entry_page() -> 'html': 16 return render_template('entry.html', the_title = 'Welcome to search_for_letters on the web!') 17
18 app.run(debug=True)
記住每次調試時都需要把之前打開的客戶端窗口重新啟動一次,現在已經是調試模式了:
下來試試我們的web應用交互:
點擊DO IT時會看到無論之前的phrase輸入什么,都會出現一個相同的結果:
這是因為之前代碼中有一些硬編碼值:life,the universe,and everything!','eriu,!
所以我們需要修改web應用的代碼來接收數據,然后才能進行新的操作。
Flask提供了一個內置對象:request,利用這個對象可以訪問所提交的數據。request對象包含一個名為form的字典屬性,form支持中括號記法,即要訪問表單中的一個數據可以把表單元素的名字放在中括號中:request.form[‘phrase’]和request.form[‘letters’]
這個對象仍需要導入才能使用。以下是新增加的內容:
第一行:
from flask import Flask, render_template, request
第十行:
@app.route('/search', methods = ['POST'])
def do_search() -> str:
phrase = request.form['phrase']
letters = request.form['letters']
return str(search_for_letters(phrase, letters))
看一看命令提示窗口,flask調試器發現代碼有更改,會重啟web應用……
測試:
set()表示沒有搜索到letters
我們希望結果也生成一個類似的HTML表單。
回顧一下results.html模板的內容:
有四個變量:the_title,the_phrase,the_letters,the_results
對代碼進行修改:
def do_search() -> 'html':
phrase = request.form['phrase']
letters = request.form['letters']
title = 'Here are your results:'
results = str(search_for_letters(phrase, letters))
return render_template('results.html', the_phrase = phrase, the_letters = letters, the_title = title, the_results = results, )
保存后在測試,發現結果頁面已經生成了HTML表單
就目前來看我們已經成功的構建了一個web應用,還使用了交互的HTML頁面,現在這個版本的web應用支持三個URL:/, /search, /entry
/地址會返回一個hello world from Flask消息,但是如果刪除這一個URL,再請求/時則會出現一個404 not found 錯誤(找不到對象確實聽起來不是很舒服),下面讓Flask把對/URL的所有請求重定向到/entry URL。調整hello函數是必要的。
重定向:
在第一行導入列表增加redirect;
修改hello函數:
這樣一來訪問http://127.0.0.1:5000/和http://127.0.0.1:5000/entry都是返回一個頁面。但是這個時候,一個請求指向/ URL,我們的web應用首先響應一個302重定向,然后web瀏覽器會發送另一個請求指向/entry URL,這會成功得到web應用的服務,但是每次指向/的一個請求就會變成兩個請求,顯然這樣是浪費資源的。
函數可以有多個URL
刪除hello函數,把@app.route('/')剪切到下面的@app.route('/entry')上面。
這樣就解決了剛才的浪費問題。
最終版本的代碼:
1 from flask import Flask, render_template, request, redirect 2 from vsearch import search_for_letters 3
4 app = Flask(__name__) 5
6 @app.route('/search', methods = ['POST']) 7 def do_search() -> 'html': 8 phrase = request.form['phrase'] 9 letters = request.form['letters'] 10 title = 'Here are your results:'
11 results = str(search_for_letters(phrase, letters)) 12 return render_template('results.html', the_phrase = phrase, the_letters = letters, the_title = title, the_results = results, ) 13
14 @app.route('/') 15 @app.route('/entry') 16 def entry_page() -> 'html': 17 return render_template('entry.html', the_title = 'Welcome to search_for_letters on the web!') 18
19 app.run(debug=True)