練習(xí) 51. 從瀏覽器獲取輸入
雖然能讓瀏覽器顯示“Hello World”是件很激動人心的事情丧荐,但是如果能讓用戶通過表單(form)向你的應(yīng)用程序提交文本豫柬,那就更令人興奮了。在這個練習(xí)中琼蚯,我們會使用 form 改進(jìn)你的 web 程序僵蛛,并且將用戶相關(guān)的信息保存到他們的“會話(session)”中。
Web 是如何工作的瑟幕?
該學(xué)點無趣的東西了磕蒲。在創(chuàng)建 form 前你需要先多學(xué)一點關(guān)于 web 的工作原理。這里的描述并不完整只盹,但是相當(dāng)準(zhǔn)確辣往,在你的程序出錯時,它會幫你找到出錯的原因殖卑。另外站削,如果你理解了 form 的應(yīng)用,那么創(chuàng)建 form 對你來說就會更容易孵稽。
我會從一個簡單的圖示講起许起,它向你展示了 web 請求的不同部分十偶,以及信息傳遞的大致流程:為了方便講述一個常規(guī)請求(request)的流程,我在每條線上面加了字母標(biāo)簽以作區(qū)別:
你在瀏覽器輸入網(wǎng)址 http://test.com//园细,它會通過你電腦的網(wǎng)絡(luò)設(shè)備發(fā)送請求(線路 A)惦积。
你的請求被傳送到互聯(lián)網(wǎng)(線路 B),然后再抵達(dá)遠(yuǎn)程服務(wù)器(線路 C)猛频,然后我的服務(wù)器會接受這個請求狮崩。
ai醬注: 這里之所以用“my server”是因為舊版書中,作者舉例用的鏈接是 http://learnpythonthehardway.org/鹿寻,這是作者自己的網(wǎng)站睦柴,所以對應(yīng)也會指向他的服務(wù)器。在新版書中毡熏,雖然更換了鏈接坦敌,但是作者并沒有對這里的表述加以更正。我的服務(wù)器接受請求后招刹,我的 web 應(yīng)用程序就會去處理這個請求(線路 D),然后我的 Python 代碼會去運行
index.GET
這個“處理程序(handler)”窝趣。在代碼 return 的時候疯暑,我的 Python 服務(wù)器就會發(fā)出響應(yīng)(response),這個響應(yīng)會再通過線路 D 傳遞到你的瀏覽器哑舒。
運行這個網(wǎng)站的服務(wù)器會從線路 D 獲得響應(yīng)妇拯,然后服務(wù)器將這個網(wǎng)站通過線路 C 傳回至互聯(lián)網(wǎng)。
響應(yīng)通過互聯(lián)網(wǎng)由線路 B 傳至你的計算機洗鸵,計算機的網(wǎng)卡再通過線路 A 將響應(yīng)傳給你的瀏覽器越锈。
最后,你的瀏覽器顯示了這個響應(yīng)的內(nèi)容膘滨。
這段描述中有幾個術(shù)語需要你了解一下甘凭,以便你在談?wù)?web 應(yīng)用時能夠明白并應(yīng)用它們:
瀏覽器(browser) 這是你幾乎每天都會用到的軟件。大部分人并不知道它真正的原理火邓,他們只會把它叫作“網(wǎng)”(the Internet)丹弱。它的作用其實是接收你輸入到地址欄網(wǎng)址(例如http://learnpythonthehardway.org),然后使用該信息向該網(wǎng)址對應(yīng)的服務(wù)器提出請求铲咨。
地址(address) 通常這是一個像 http://test.com// 一樣的 URL (Uniform Resource Locator躲胳,統(tǒng)一資源定位器),它告訴瀏覽器該打開哪個網(wǎng)站纤勒。前面的 http 指出了你要使用的協(xié)議 (protocol)坯苹,這里我們用的是“超文本傳輸協(xié)議(Hyper-Text Transport Protocol)”。你還可以試試 ftp://ibiblio.org/ 摇天,這是一個“FTP 文件傳輸協(xié)議(File Transport Protocol)”的例子粹湃。test.com
這部分是“主機名(hostname)”恐仑,也就是一個便于人閱讀和記憶的地址,主機名會被匹配到一串叫作“IP 地址”的數(shù)字上面再芋,這個“IP 地址”就相當(dāng)于網(wǎng)絡(luò)中一臺計算機的電話號碼菊霜,通過這個號碼可以訪問到這臺計算機。最后济赎,URL 后面還可以跟一個路徑鉴逞,就像 http://test.com//book/ 中的 /book/
部分,它對應(yīng)的是服務(wù)器上的某個文件或者某些資源司训,通過訪問這樣的網(wǎng)址构捡,你可以向服務(wù)器發(fā)出請求,然后獲得這些資源壳猜。網(wǎng)站地址還有很多別的組成部分勾徽,不過這些是最主要的。
連接(connection) 一旦瀏覽器知道了你想用的協(xié)議(http)统扳、你想訪問的服務(wù)器(http://test.com/)喘帚、以及該服務(wù)器需要獲取的資源,它就要創(chuàng)建一個連接咒钟。瀏覽器會讓操作系統(tǒng)(Operating System, OS)打開計算機的一個“端口(port)”(通常是 80 端口)吹由,端口準(zhǔn)備好以后,操作系統(tǒng)會回傳給你的程序一個類似文件的東西朱嘴,它所做的事情就是通過網(wǎng)絡(luò)傳輸和接收數(shù)據(jù)倾鲫,讓你的計算機和 http://test.com/ 這個網(wǎng)站所屬的服務(wù)器之間實現(xiàn)數(shù)據(jù)交換。當(dāng)你使用 http://localhost:8080/ 訪問你自己的站點時萍嬉,發(fā)生的事情其實是一樣的乌昔,只不過這次你告訴了瀏覽器要訪問的是你自己的計算機(localhost),要使用的端口不是默認(rèn)的 80壤追,而是 8080磕道。你還可以直接訪問 http://test.com:80/,這和不輸入端口效果一樣行冰,因為 HTTP 的默認(rèn)端口本來就是 80捅厂。
請求(request) 你的瀏覽器通過你提供的地址建立了連接,現(xiàn)在它需要從遠(yuǎn)端服務(wù)器要到它(或你)想要的資源资柔。如果你在 URL 的結(jié)尾加了 /book/
焙贷,那你想要的就是 /book/
對應(yīng)的文件或資源,大部分的服務(wù)器會直接為你調(diào)用 /book/index.html
這個文件贿堰,不過我們就假裝它不存在好了辙芍。瀏覽器為了獲得服務(wù)器上的資源,它需要向服務(wù)器發(fā)送一個“請求”。這里我就不講細(xì)節(jié)了故硅,你只需要明白庶灿,為了得到服務(wù)器上的內(nèi)容,它必須先向服務(wù)器發(fā)送一個請求才行吃衅。有意思的是往踢,“資源”不一定非要是文件。例如當(dāng)瀏覽器向你的應(yīng)用程序提出請求的時候徘层,服務(wù)器返回的其實是你的 Python 代碼生成的一些東西峻呕。
服務(wù)器(server) 服務(wù)器指的是瀏覽器另一端連接的計算機,它知道如何回應(yīng)瀏覽器請求的文件和資源趣效。大部分的 web 服務(wù)器只要發(fā)送文件就可以了瘦癌,這也是服務(wù)器流量的主要部分。不過你學(xué)的是使用 Python 組建一個服務(wù)器跷敬,這個服務(wù)器知道如何接受請求讯私,然后返回用 Python 處理過的字符串。當(dāng)你使用這種處理方式時西傀,你其實是假裝把文件發(fā)給了瀏覽器斤寇,其實你用的都只是代碼而已。就像你在《練習(xí) 50》中看到的拥褂,要構(gòu)建一個“響應(yīng)”其實也不需要多少代碼娘锁。
響應(yīng)(response) 這就是你的服務(wù)器回復(fù)你的請求,發(fā)回至瀏覽器的 HTML(包括 css肿仑、javascript 或 images)致盟。以文件響應(yīng)為例碎税,服務(wù)器只要從磁盤讀取文件尤慰,發(fā)送給瀏覽器就可以了,不過它還要將這些內(nèi)容包在一個特別定義的“頭部信息(header)”中雷蹂,這樣瀏覽器就會知道它獲取的是什么類型的內(nèi)容伟端。以你的 web 應(yīng)用程序為例,你發(fā)送的其實還是一樣的東西匪煌,包括 header 也一樣责蝠,只不過這些數(shù)據(jù)是你用 Python 代碼即時生成的。
這可以算是你能在網(wǎng)上找到的關(guān)于瀏覽器如何訪問網(wǎng)站的最快的快速課程了萎庭。這個課程應(yīng)該可以幫你更容易地理解本節(jié)的練習(xí)霜医,如果你還是不明白,就找找資料多多了解這方面的信息驳规,直到你明白為止肴敛。有一個很好的方法,就是你對照著上面的圖示,把你在《練習(xí) 50》中創(chuàng)建的 web 程序中的內(nèi)容分成幾個部分医男,讓其中的各部分對應(yīng)到上面的圖示中砸狞。如果你可以正確地將程序的各部分對應(yīng)到這個圖示,那你就大致明白它的工作原理了镀梭。
表單(forms)是如何工作的
熟悉“表單”最好的方法就是寫一個可以接收表單數(shù)據(jù)的程序出來刀森,然后看你可以對它做些什么。先將你的 app.py
文件修改成下面的樣子:
form_test.py
1 from flask import Flask
2 from flask import render_template
3 from flask import request
4
5 app = Flask(__name__)
6
7 @app.route("/hello")
8 def index():
9 name = request.args.get('name', 'Nobody')
10
11 if name:
12 greeting = f"Hello, {name}"
13 else:
14 greeting = "Hello World"
15
16 return render_template("index.html", greeting=greeting)
17
18 if __name__ == "__main__":
19 app.run()
重啟 flask(按 CTRL + C报账,然后再次運行)確保它再次加載研底,然后用瀏覽器訪問 http://localhost:5000/hello,應(yīng)該會顯示 “I just wanted to say Hello, Nobody.” 接著笙什,把瀏覽器中的 URL 改為 http://localhost:5000/hello?name=Frank飘哨,你會看到 “Hello, Frank.” 最后,把 name=Frank
這里改成你的名字琐凭,它就會對你說 Hello算芯。
讓我們拆解一下腳本中的這些變更:
- 我們沒有直接為
greeting
賦值,而是使用了request.args
從瀏覽器獲取數(shù)據(jù)栽烂。這是一個用鍵值對(key=value pairs) 來包含表單值的簡單字典撑毛。 - 然后我用新的
name
構(gòu)建greeting
,這句你應(yīng)該已經(jīng)很熟悉了愁憔。 - 其他的內(nèi)容和以前是一樣的腕扶,我們就不再分析了。
URL 中還可以包含多個參數(shù)吨掌。將本例的兩個變量改成這樣:http://localhost:5000/hello?name=Frank&greet=Hola半抱。然后修改代碼,讓它像這樣獲取 name
和 greet
:
greet = request.args.get( ' greet ' , ' Hello ' )
greeting = f"{greet}, {name}"
你還應(yīng)該試著不在 URL 上給出 greet 和 name 參數(shù)膜宋,只讓瀏覽器訪問 http://localhost:5000/hello窿侈,然后你會看到,name
會默認(rèn)為 “Nobody”秋茫,greet
會默認(rèn)為 “Hello”史简。
創(chuàng)建 HTML 表單
在 URL 上傳遞參數(shù)也可以,但就是有點丑肛著,而且對普通用戶來說有點難用圆兵。你真正想要的是一個“發(fā)送表單”(POST form),這是一個特殊的 HTML 文件枢贿,里面有一個 <form>
標(biāo)簽殉农。這個表單會從用戶那里收集信息,然后發(fā)送給你的網(wǎng)站局荚,就像你之前做的那樣超凳。
讓我們來快速創(chuàng)建一個,從中你可以看出它的工作原理。你需要創(chuàng)建一個新的 HTML 文件 templates/hello_form.html:
hello_form.html
<html>
<head>
<title>Sample Web Form</title>
</head>
<body>
<h1>Fill Out This Form</h1>
<form action="/hello" method="POST">
A Greeting: <input type="text" name="greet">
<br/>
Your Name: <input type="text" name="name">
<br/>
<input type="submit">
</form>
</body>
</html>
然后你需要把 app.py 改成這樣:
app.py
1 from flask import Flask
2 from flask import render_template
3 from flask import request
4
5 app = Flask(__name__)
6
7 @app.route("/hello", methods=['POST', 'GET'])
8 def index():
9 greeting = "Hello World"
10
11 if request.method == "POST":
12 name = request.form['name']
13 greet = request.form['greet']
14 greeting = f"{greet}, {name}"
15 return render_template("index.html", greeting=greeting)
16 else:
17 return render_template("hello_form.html")
18
19
20 if __name__ == "__main__":
21 app.run()
改完之后聪建,再次重啟 web 應(yīng)用钙畔,像之前一樣刷新瀏覽器。
這次你會看到一個表單金麸,向你獲取“A Greeting”和“Your Name.”擎析。當(dāng)你點擊表單上的提交( Submit )按鈕時,它會給你跟之前一樣的問候挥下。不過這次揍魂,瀏覽器上面的 URL 還是 http://localhost:5000/hello,哪怕你已經(jīng)傳遞了參數(shù)棚瘟。
讓這個發(fā)揮作用的是 hello_form.html
文件中的這一行:<form action="/hello" method="POST">
现斋。這告訴瀏覽器:
- 從表單中的各個欄位收集用戶輸入的數(shù)據(jù)。
- 使用一種 POST 類型的請求偎蘸,將這些數(shù)據(jù)發(fā)送給服務(wù)器庄蹋。這是另外一種瀏覽器請求,它會將表單欄位“隱藏”起來迷雪。
- 將這個請求發(fā)送至
/hello
URL限书,這是由action="/hello"
這部分內(nèi)容告訴瀏覽器的。
你可以看到這兩個 <input>
標(biāo)簽是如何和你新代碼中的變量名相匹配的章咧。還要注意一下倦西,在 class index
里面,我沒有用 GET 方法赁严,而是使用了 POST 方法扰柠。這個新程序的工作原理如下:
你的新請求像之前一樣去到了
index()
,不過現(xiàn)在有一個 if 語句來檢查request.method
是 "POST" 還是 "GET" 方法疼约。這樣瀏覽器就能告訴app.py
一個請求是表單提交還是 URL 參數(shù)卤档。如果
request.method
是 "POST",程序就會對表單填寫和提交的內(nèi)容進(jìn)行處理忆谓,并返回合適的問候語裆装。如果
request.method
是其他東西踱承,那你只要返回hello_form.html
讓用戶來填寫倡缠。
作為練習(xí),在 templates/index.html
中添加一個鏈接茎活,讓它指向 /hello
昙沦,這樣你可以反復(fù)填寫、提交表單并查看結(jié)果载荔。
確認(rèn)你可以解釋清楚這個鏈接的工作原理盾饮,以及它是如何讓你實現(xiàn)在
templates/index.html
和 templates/hello_form.html
之間循環(huán)跳轉(zhuǎn)的,還有就是要明白你新修改過的 Python 代碼中,運行的是哪一部分代碼丘损。
創(chuàng)建布局模板(layout template)
在你下一節(jié)練習(xí)創(chuàng)建游戲的過程中普办,你需要創(chuàng)建很多的小 HTML 頁面。如果你每次都寫一個完整的網(wǎng)頁徘钥,你會很快感覺到厭煩的衔蹲。幸運的 是你可以創(chuàng)建一個“布局模板”,也就是一種提供了通用的頭文件(headers)和腳注(footers)的外殼模板呈础,你可以用它將你所有的其他網(wǎng)頁包裹起來舆驶。好程序員會盡可能減少重復(fù)動作,所以要做一個好程序員而钞,使用布局模板是很重要的沙廉。
將 templates/index.html
修改為這樣:
index_laid_out.html
{% extends "layout.html" %}
{% block content %}
{% if greeting %}
I just wanted to say
<em style="color: green; font-size: 2em;">{{ greeting }}</em>.
{% else %}
<em>Hello</em>, world!
{% endif %}
{% endblock %}
然后將 templates/hello_form.html
修改為這樣:
hello_form_laid_out.html
{% extends "layout.html" %}
{% block content %}
<h1>Fill Out This Form</h1>
<form action="/hello" method="POST">
A Greeting: <input type="text" name="greet">
<br/>
Your Name: <input type="text" name="name">
<br/>
<input type="submit">
</form>
{% endblock %}
我們所做的就是把每一個頁面頂部和底部反復(fù)用到的“boilerplate”(樣板)代碼去掉。這些被去掉的代碼會被放到一個單獨的 templates/layout.html
文件中臼节,之后撬陵,這些反復(fù)用到的代碼就由 layout.html 來提供了。
修改好之后网缝,創(chuàng)建一個 templates/layout.html
文件袱结,內(nèi)容如下:
layout.html
<html>
<head>
<title>Gothons From Planet Percal #25</title>
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>
這個文件和普通的模板文件類似,不過它會收到其它模板傳遞的內(nèi)容途凫,并將它們“包裹”起來垢夹。任何寫在這里的內(nèi)容都無需寫在別的模板中了。你的其他 HTML 模板會被插入到 {% block content %}
中维费。Flask 知道要把 layout.html
文件用作布局果元,因為你在模板的頂部放了 {% extends "layout.html" %}
。
為表單撰寫自動測試代碼
使用瀏覽器測試 web 程序是很容易的犀盟,只要點刷新按鈕就可以了而晒。不過畢竟我們是程序員嘛,如果我們可以寫一些代碼來測試我們的程序阅畴,為什么還要重復(fù)手動測試呢倡怎?接下來你要做的,就是為你的 web 程序?qū)懸粋€小測試贱枣。這會用到你在《練習(xí) 47》學(xué)過的一些東西监署,如果你不記得的話,可以回去復(fù)習(xí)一下纽哥。
創(chuàng)建一個新文件钠乏,并命名為 tests/app_tests.py,其內(nèi)容如下:
app_tests.py
1 from nose.tools import *
2 from app import app
3
4 app.config['TESTING'] = True
5 web = app.test_client()
6
7 def test_index():
8 rv = web.get('/', follow_redirects=True)
9 assert_equal(rv.status_code, 404)
10
11 rv = web.get('/hello', follow_redirects=True)
12 assert_equal(rv.status_code, 200)
13 assert_in(b"Fill Out This Form", rv.data)
14
15 data = {'name': 'Zed', 'greet': 'Hola'}
16 rv = web.post('/hello', follow_redirects=True, data=data)
17 assert_in(b"Zed", rv.data)
18 assert_in(b"Hola", rv.data)
最后春塌,用 nosetests
運行這個測試程序晓避,來測試你的 web 應(yīng)用:
$ nosetests
.
---------------
Ran 1 test in 0.059s OK
我在這兒其實是把整個應(yīng)用都從 app.py
模塊中引入進(jìn)來了簇捍,然后手動運行它。flask 框架有一個非常簡單用來處理請求的 API俏拱,它看起來像這樣:
data = {'name': 'Zed', 'greet': 'Hola'}
rv = web.post('/hello', follow_redirects=True, data=data)
這意味著你可以用 post()
方法發(fā)送一個 POST 請求暑塑,然后把表單數(shù)據(jù)作為字典傳給它。其他都和測試 web.get()
請求一模一樣锅必。
在 tests/app_tests.py
自動測試腳本中梯投,我首先確認(rèn) /
返回了一個“404 Not Found”響應(yīng),因為這個 URL 其實是不存在的况毅。然后我檢查了 /hello
在 GET 和 POST 兩種請求的情況下都能正常工作分蓖。就算你沒有弄明白測試的原理,這些測試代碼應(yīng)該是很好讀懂的尔许。
花些時間研究一下這個最新版的 web 程序么鹤,重點研究一下自動測試的工作原理。確保你理解了將 app.py
做為一個模塊導(dǎo)入味廊,然后進(jìn)行自動化測試的流程蒸甜。這是一個很重要的技巧,它會引導(dǎo)你學(xué)到更多東西余佛。
附加練習(xí)
- 閱讀和 HTML 相關(guān)的更多資料柠新,然后為你的表單設(shè)計一個更好的輸出格式。你可以先在紙上設(shè)計出來辉巡,然后用 HTML 去實現(xiàn)它恨憎。
- 這是一道難題,試著研究一下如何進(jìn)行文件上傳郊楣,通過網(wǎng)頁上傳一張圖像憔恳,然后將其保存到磁盤中。
- 更難的難題净蚤,找到 HTTP RFC 文件(講述 HTTP 工作原理的技術(shù)文件)钥组,然后努力閱讀一下。這是一篇很無趣的文檔今瀑,不過偶爾你也會用到里邊的一些知識程梦。
- 又是一道難題,找人幫你設(shè)置一個 web 服務(wù)器橘荠,例如 Apache屿附、Nginx 或者 thttpd。試著讓服務(wù)器 serve 一下你創(chuàng)建的
.html
和.css
文件砾医。如果失敗了也沒關(guān)系拿撩,web 服務(wù)器本來就都有點爛衣厘。 - 完成上面的任務(wù)后休息一下如蚜,然后試著多創(chuàng)建一些 web 程序出來压恒。
拆解
這里很適合講一下如何拆解 web 應(yīng)用。你應(yīng)該這樣做:
打開
FLASK_DEBUG
會造成多大的損害错邦?注意做這個的時候別把自己電腦搞垮了探赫。假設(shè)你沒有為表單設(shè)置默認(rèn)參數(shù),哪里會出錯撬呢?
你先檢查
POST
然后是“其他東西”伦吠。你可以用curl
命令行工具生成不同的請求類型』昀梗看看會發(fā)生什么毛仪?