# 習題 51: 從瀏覽器中獲取輸入
雖然能讓瀏覽器顯示“Hello World”是很有趣的一件事情,但是如果能讓用戶通過表單(form)向你的應用程序提交文本就更有趣了。這節習題中,我們將使用 form 改進你的 web 程序,并且將用戶相關的信息保存到他們的“會話(session)”中。
### Web 的工作原理
該學點無趣的東西了。在創建 form 前你需要先多學一點關于 web的工作原理。這里講并不完整,但是相當準確,在你的程序出錯時,它會幫你找到出錯的原因。另外,如果你理解了 form 的應用,那么創建 form 對你來說就會更容易了。
我將以一個簡單的圖示講起,它向你展示了 web 請求的各個不同的部分,以及信息傳遞的大致流程:

為了方便講述 HTTP 請求(request) 的流程,我在每條線上面加了字母標簽以作區別。
1. 你在瀏覽器中輸入網址 http://learnpythonthehardway.org/,然后瀏覽器會通過你的電腦的網絡設備發出 request(線路 A)。
1. 你的 request 被傳送到互聯網(線路 B),然后再抵達遠端服務器(線路 C),然后我的服務器將接受這個 request。
1. 我的服務器接受 request 后,我的 web 應用程序就去處理這個請求(線路 D),然后我的 Python 代碼就會去運行 index.GET 這個“處理程序(handler)”。
1. 在代碼 return 的時候,我的 Python 服務器就會發出響應(response),這個響應會再通過線路 D 傳遞到你的瀏覽器。
1. 這個網站所在的服務器將響應由線路 D 獲取,然后通過線路 C 傳至互聯網。
1. 響應通過互聯網由線路 B 傳至你的計算機,計算機的網卡再通過線路 A 將響應傳給你的瀏覽器。
1. 最后,你的瀏覽器顯示了這個響應的內容。
這段詳解中用到了一些術語。你需要掌握這些術語,以便在談論你的 web 應用時你能明白而且應用它們:
瀏覽器(browser)這是你幾乎每天都會用到的軟件。大部分人不知道它真正的原理,他們只會把它叫作“網”。它的作用其實是接收你輸入到地址欄網址(例如 [http://learnpythonthehardway.org](http://learnpythonthehardway.org)),然后使用該信息向該網址對應的服務器提出請求(request)。地址(address)通常這是一個像 [http://learnpythonthehardway.org/](http://learnpythonthehardway.org/) 一樣的 URL (Uniform Resource Locator,統一資源定位器),它告訴瀏覽器該打開哪個網站。前面的 http 指出了你要使用的協議(protocol),這里我們用的是“超文本傳輸協議(Hyper-Text Transport Protocol)”。你還可以試試 [ftp://ibiblio.org/](#) ,這是一個“FTP 文件傳輸協議(File Transport Protocol)”的例子。learnpythonthehardway.org 這部分是“主機名(hostname)”,也就是一個便于人閱讀和記憶的字串,主機名會被匹配到一串叫作“IP 地址”的數字上面,這個“IP 地址”就相當于網絡中一臺計算機的電話號碼,通過這個號碼可以訪問到這臺計算機。最后,URL 中還可以尾隨一個“路徑”,例如 [http://learnpythonthehardway.org/book/](http://learnpythonthehardway.org/book/) 中的 /book/,它對應的是服務器上的某個文件或者某些資源,通過訪問這樣的網址,你可以向服務器發出請求,然后獲得這些資源。網站地址還有很多別的組成部分,不過這些是最主要的。連接(connection)一旦瀏覽器知道了協議(http)、服務器(learnpythonthehardway.org)、以及要獲得的資源,它就要去創建一個連接。這個過程中,瀏覽器讓操作系統(Operating System, OS)打開計算機的一個“端口(port)”(通常是 80 端口),端口準備好以后,操作系統會回傳給你的程序一個類似文件的東西,它所做的事情就是通過網絡傳輸和接收數據,讓你的計算機和 learnpythonthehardway.org 這個網站所屬的服務器之間實現數據交流。 當你使用 [http://localhost:8080/](http://localhost:8080/) 訪問你自己的站點時,發生的事情其實是一樣的,只不過這次你告訴了瀏覽器要訪問的是你自己的計算機(localhost),要使用的端口 不是默認的 80,而是 8080。你還可以直接訪問 [http://learnpythonthehardway.org:80/](http://learnpythonthehardway.org:80/), 這和不輸入端口效果一樣,因為 HTTP 的默認端口本來就是 80。請求(request)你的瀏覽器通過你提供的地址建立了連接,現在它需要從遠端服務器要到它(或你)想要的資源。如果你在 URL 的結尾加了 /book/,那你想要的就是 /book/ 對應的文件或資源,大部分的服務器會直接為你調用 /book/index.html 這個文件,不過我們就假裝不存在好了。瀏覽器為了獲得服務器上的資源,它需要向服務器發送一個“請求”。這里我就不講細節了,為了得到服務器上的內容,你必須先向服務器發送一個請求才行。有意思的是,“資源”不一定非要是文件。例如當瀏覽器向你的應用程序提出請求的時候,服務器返回的其實是你的 Python 代碼生成的一些東西。服務器(server)服務器指的是瀏覽器另一端連接的計算機,它知道如何回應瀏覽器請求的文件和資源。大部分的 web 服務器只要發送文件就可以了,這也是服務器流量的主要部分。不過你學的是使用 Python 組建一個服務器,這個服務器知道如何接受請求,然后返回用 Python 處理過的字符串。當你使用這種處理方式時,你其實是假裝把文件發給了瀏覽器,其實你用的都只是代碼而已。就像你在《習題 50》中看到的,要構建一個“響應”其實也不需要多少代碼。響應(response)這就是你的服務器回復你的請求,發回至瀏覽器的 HTML,它里邊可能有 css、javascript、或者圖像等內容。以文件響應為例,服務器只要從磁盤讀取文件,發送給瀏覽器就可以了,不過它還要將這些內容包在一個特別定義的“頭部信息(header)”中,這樣瀏覽器就會知道它獲取的是什么類型的內容。以你的 web 應用程序為例,你發送的其實還是一樣的東西,包括 header 也一樣,只不過這些數據是你用 Python 代碼即時生成的。
這個可以算是你能在網上找到的關于瀏覽器如何訪問網站的最快的快速課程了。這節課程應該可以幫你更容易地理解本節的習題,如果你還是不明白,就到處找資料多多了解這方面的信息,知道你明白為止。有一個很好的方法,就是你對照著上面的圖示,將你在《習題 50》中創建的 web 程序中的內容分成幾個部分,讓其中的各部分對應到上面的圖示。如果你可以正確地將程序的各部分對應到這個圖示,你就大致開始明白它的工作原理了。
### 表單(form) 的工作原理
熟悉“表單”最好的方法就是寫一個可以接收表單數據的程序出來,然后看你可以對它做些什么。先將你的 bin/app.py 修改成下面的樣子:
<table class="highlighttable"><tbody><tr><td class="linenos"> <div class="linenodiv"> <pre> 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20</pre> </div> </td> <td class="code"> <div class="highlight"> <pre>import web
urls = (
'/hello', 'Index'
)
app = web.application(urls, globals())
render = web.template.render('templates/')
class Index(object):
def GET(self):
form = web.input(name="Nobody")
greeting = "Hello, %s" % form.name
return render.index(greeting = greeting)
if __name__ == "__main__":
app.run()
</pre> </div> </td> </tr></tbody></table>
重啟你的 web 程序(按 CTRL-C 后重新運行),確認它有運行起來,然后使用瀏覽器訪問 http://localhost:8080/hello,這時瀏覽器應該會顯示“I just wanted to say Hello, Nobody.”,接下來,將瀏覽器的地址改成 http://localhost:8080/hello?name=Frank,然后你可以看到頁面顯示為“Hello, Frank.”,最后將 name=Frank 修改為你自己的名字,你就可以看到它對你說“Hello”了。
讓我們研究一下你的程序里做過的修改。
1. 我們沒有直接為 greeting 賦值,而是使用了 web.input 從瀏覽器獲取數據。這個函數會將一組 key=value 的表述作為默認參數,解析你提供的 URL 中的 ?name=Frank 部分,然后返回一個對象,你可以通過這個對象方便地訪問到表單的值。
1. 然后我通過 form 對象的 form.name 屬性為 greeting 賦值,這句你應該已經熟悉了。
1. 其他的內容和以前是一樣的,我們就不再分析了。
URL 中該還可以包含多個參數。將本例的 URL 改成這樣子: http://localhost:8080/hello?name=Frank&greet=Hola。然后修改代碼,讓它去獲取form.name 和 form.greet,如下所示:
~~~
greeting = "%s, %s" % (form.greet, form.name)
~~~
修改完畢后,試著訪問新的 URL。然后將 &greet=Hola 部分刪除,看看你會得到什么樣的錯誤信息。由于我們在 web.input(name="Nobody") 中沒有為 greet 設定默認值,這樣 greet 就變成了一個必須的參數,如果沒有這個參數程序就會報錯。現在修改一下你的程序,在 web.input 中為 greet 設一個默認值試試看。另外你還可以設 greet=None,這樣你可以通過程序檢查 greet 的值是否存在,然后提供一個比較好的錯誤信息出來,例如:
~~~
form = web.input(name="Nobody", greet=None)
if form.greet:
greeting = "%s, %s" % (form.greet, form.name)
return render.index(greeting = greeting)
else:
return "ERROR: greet is required."
~~~
### 創建 HTML 表單
你可以通過 URL 參數實現表單提交,不過這樣看上去有些丑陋,而且不方便一般人使用,你真正需要的是一個“POST 表單”,這是一種包含了 <form> 標簽的特殊 HTML 文件。這種表單收集用戶輸入并將其傳遞給你的 web 程序,這和你上面實現的目的基本是一樣的。
讓我們來快速創建一個,從中你可以看出它的工作原理。你需要創建一個新的 HTML 文件, 叫做 templates/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>
~~~
然后將 bin/app.py 改成這樣:
<table class="highlighttable"><tbody><tr><td class="linenos"> <div class="linenodiv"> <pre> 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21</pre> </div> </td> <td class="code"> <div class="highlight"> <pre>import web
urls = (
'/hello', 'Index'
)
app = web.application(urls, globals())
render = web.template.render('templates/')
class Index(object):
def GET(self):
return render.hello_form()
def POST(self):
form = web.input(name="Nobody", greet="Hello")
greeting = "%s, %s" % (form.greet, form.name)
return render.index(greeting = greeting)
if __name__ == "__main__":
app.run()
</pre> </div> </td> </tr></tbody></table>
都寫好以后,重啟 web 程序,然后通過你的瀏覽器訪問它。
這回你會看到一個表單,它要求你輸入“一個問候語句(A Greeting)”和“你的名字(Your Name)”,等你輸入完后點擊“提交(Submit)”按鈕,它就會輸出一個正常的問候頁面,不過這一次你的URL 還是 http://localhost:8080/hello,并沒有添加參數進去。
在 hello_form.html 里面關鍵的一行是 <formaction="/hello"method="POST"> ,它告訴你的瀏覽器以下內容:
1. 從表單中的各個欄位收集用戶輸入的數據。
1. 讓瀏覽器使用一種 POST 類型的請求,將這些數據發送給服務器。這是另外一種瀏覽器請求,它會將表單欄位“隱藏”起來。
1. 將這個請求發送至 /hello URL,這是由 action="/hello" 告訴瀏覽器的。
你可以看到兩段 <input> 標簽的名字屬性(name)和代碼中的變量是對應的,另外我們在 classindex 中使用的不再只是 GET 方法,而是另一個 POST 方法。
這個新程序的工作原理如下:
1. 瀏覽器訪問到 web 程序的 /hello 目錄,它發送了一個 GET 請求,于是我們的 index.GET 函數就運行并返回了 hello_form。
1. 你填好了瀏覽器的表單,然后瀏覽器依照 <form> 中的要求,將數據通過 POST 請求的方式發給 web 程序。
1. Web 程序運行了 index.POST 方法(不是 index.GET 方法)來處理這個請求。
1. 這個 index.POST 方法完成了它正常的功能,將 hello 頁面返回,這里并沒有新的東西,只是一個新函數名稱而已。
作為練習,在 templates/index.html 中添加一個鏈接,讓它指向 /hello,這樣你可以反復填寫并提交表單查看結果。確認你可以解釋清楚這個鏈接的工作原理,以及它是如何讓你實現在 templates/index.html 和 templates/hello_form.html 之間循環跳轉的,還有就是要明白你新修改過的 Python 代碼,你需要知道在什么情況下會運行到哪一部分代碼。
### 創建布局模板(layout template)
在你下一節練習創建游戲的過程中,你需要創建很多的小 HTML 頁面。如果你每次都寫一個完整的網頁,你會很快感覺到厭煩的。幸運的 是你可以創建一個“布局模板”,也就是一種提供了通用的頭文件和腳注的外殼模板,你可以用它將你所有的其他網頁包裹起來。好程序員會盡可能減少重復動作,所以要做一個好程序員,使用布局模板是很重要的。
將 templates/index.html 修改成這樣:
~~~
$def with (greeting)
$if greeting:
I just wanted to say <em style="color: green; font-size: 2em;">$greeting</em>.
$else:
<em>Hello</em>, world!
~~~
然后把 templates/hello_form.html 修改成這樣:
~~~
<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>
~~~
上面這些修改的目的,是將每一個頁面頂部和底部的反復用到的“boilerplate”代碼剝掉。這些被剝掉的代碼會被放到一個單獨的 templates/layout.html 文件中,從此以后,這些反復用到的代碼就由 layout.html 來提供了。
上面的都改好以后,創建一個 templates/layout.html 文件,內容如下:
~~~
$def with (content)
<html>
<head>
<title>Gothons From Planet Percal #25</title>
</head>
<body>
$:content
</body>
</html>
~~~
這個文件和普通的模板文件類似,不過其它的模板的內容將被傳遞給它,然后它會將其它 模板的內容“包裹”起來。任何寫在這里的內容多無需寫在別的模板中了。你需要注意$:content 的用法,這和其它的模板變量有些不同。
最后一步,就是將 render 對象改成這樣:
~~~
render = web.template.render('templates/', base="layout")
~~~
這會告訴 lpthw.web 讓它去使用 templates/layout.html 作為其它模板的基礎模板。重啟你的程序觀察一下,然后試著用各種方法修改你的 layout 模板,不要修改你別的模板,看看輸出會有什么樣的變化。
### 為表單撰寫自動測試代碼
使用瀏覽器測試 web 程序是很容易的,只要點刷新按鈕就可以了。不過畢竟我們是程序員嘛,如果我們可以寫一些代碼來測試我們的程序,為什么還要重復手動測試呢?接下來你要做的,就是為你的 web 程序寫一個小測試。這會用到你在《習題 47》學過的一些東西,如果你不記得的話,可以回去復習一下。
為了讓 Python 加載 bin/app.py 并進行測試,你需要先做一點準備工作。首先創建一個 bin/__init__.py 空文件,這樣 Python 就會將 bin/ 當作一個目錄了。(在《習題 52》中你會去修改 __init__.py,不過這是后話。)
我還為 lpthw.web 創建了一個簡單的小函數,讓你判斷(assert) web 程序的響應,這個函數有一個很合適的名字,就叫 assert_response。創建一個 tests/tools.py 文件,內容如下:
<table class="highlighttable"><tbody><tr><td class="linenos"> <div class="linenodiv"> <pre> 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19</pre> </div> </td> <td class="code"> <div class="highlight"> <pre>from nose.tools import *
import re
def assert_response(resp, contains=None, matches=None, headers=None, status="200"):
assert status in resp.status, "Expected response %r not in %r" % (status, resp.status)
if status == "200":
assert resp.data, "Response data is empty."
if contains:
assert contains in resp.data, "Response does not contain %r" % contains
if matches:
reg = re.compile(matches)
assert reg.matches(resp.data), "Response does not match %r" % matches
if headers:
assert_equal(resp.headers, headers)
</pre> </div> </td> </tr></tbody></table>
準備好這個文件以后,你就可以為你的 bin/app.py 寫自動測試代碼了。創建一個新文件,叫做 tests/app_tests.py,內容如下:
<table class="highlighttable"><tbody><tr><td class="linenos"> <div class="linenodiv"> <pre> 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22</pre> </div> </td> <td class="code"> <div class="highlight"> <pre>from nose.tools import *
from bin.app import app
from tests.tools import assert_response
def test_index():
# check that we get a 404 on the / URL
resp = app.request("/")
assert_response(resp, status="404")
# test our first GET request to /hello
resp = app.request("/hello")
assert_response(resp)
# make sure default values work for the form
resp = app.request("/hello", method="POST")
assert_response(resp, contains="Nobody")
# test that we get expected values
data = {'name': 'Zed', 'greet': 'Hola'}
resp = app.request("/hello", method="POST", data=data)
assert_response(resp, contains="Zed")
</pre> </div> </td> </tr></tbody></table>
最后,使用 nosetests 運行測試腳本,然后測試你的 web 程序。
~~~
$ nosetests
.
----------------------------------------------------------------------
Ran 1 test in 0.059s
OK
~~~
這里我所做的,是將 bin/app.py 這個模塊中的整個 web 程序都 import 進來,然后手動運行這個 web 程序。lpthw.web 有一個非常簡單的 API 用來處理請求,看上去大致是這樣子的:
~~~
app.request(localpart='/', method='GET', data=None, host='0.0.0.0:8080',
headers=None, https=False)
~~~
你可以將 URL 作為第一個參數,然后你可以修改修改 request 的方法、form 的數據、以及 header 的內容,這樣你無須啟動 web 服務器,就可以使用自動測試來測試你的 web 程序了。
為了驗證函數的響應,你需要使用 tests.tools 中定義的 assert_response 函數,用法屬下:
~~~
assert_response(resp, contains=None, matches=None, headers=None, status="200")
~~~
把你調用 app.request 得到的響應傳遞給這個函數,然后將你要檢查的內容作為參數傳遞給誒這個函數。你可以使用 contains 參數來檢查響應中是否包含指定的值,使用status 參數可以檢查指定的響應狀態。這個小函數其實包含了很多的信息,所以你還是自己研究一下的比較好。
在 tests/app_tests.py 自動測試腳本中,我首先確認 / 返回了一個“404 Not Found”響應,因為這個 URL 其實是不存在的。然后我檢查了 /hello 在 GET 和 POST 兩種請求的情況下都能正常工作。就算你沒有弄明白測試的原理,這些測試代碼應該是很好讀懂的。
花一些時間研究一下這個最新版的 web 程序,重點研究一下自動測試的工作原理。確認你理解了將 bin/app.py 做為一個模塊導入,然后進行自動化測試的流程。這是一個很重要的技巧,它會引導你學到更多東西。
### 加分習題
1. 閱讀和 HTML 相關的更多資料,然后為你的表單設計一個更好的輸出格式。你可以先在紙上設計出來,然后用 HTML 去實現它。
1. 這是一道難題,試著研究一下如何進行文件上傳,通過網頁上傳一張圖像,然后將其保存到磁盤中。
1. 更難的難題,找到 HTTP RFC 文件(講述 HTTP 工作原理的技術文件),然后努力閱讀一下。這是一篇很無趣的文檔,不過偶爾你會用到里邊的一些知識。
1. 又是一道難題,找人幫你設置一個 web 服務器,例如 Apache、Nginx、或者 thttpd。試著讓服務器伺服一下你創建的 .html 和 .css 文件。如果失敗了也沒關系,web 服務器本來就都有點挫。
1. 完成上面的任務后休息一下,然后試著多創建一些 web 程序出來。你應該仔細閱讀 web.py (它和 lpthw.web 是同一個程序)中關于會話(session)的內容,這樣你可以 明白如何保持用戶的狀態信息。
- 譯者前言
- 前言:笨辦法更簡單
- 習題 0: 準備工作
- 習題 1: 第一個程序
- 習題 2: 注釋和井號
- 習題 3: 數字和數學計算
- 習題 4: 變量(variable)和命名
- 習題 5: 更多的變量和打印
- 習題 6: 字符串(string)和文本
- 習題 7: 更多打印
- 習題 8: 打印,打印
- 習題 9: 打印,打印,打印
- 習題 10: 那是什么?
- 習題 11: 提問
- 習題 12: 提示別人
- 習題 13: 參數、解包、變量
- 習題 14: 提示和傳遞
- 習題 15: 讀取文件
- 習題 16: 讀寫文件
- 習題 17: 更多文件操作
- 習題 18: 命名、變量、代碼、函數
- 習題 19: 函數和變量
- 習題 20: 函數和文件
- 習題 21: 函數可以返回東西
- 習題 22: 到現在你學到了哪些東西?
- 習題 23: 讀代碼
- 習題 24: 更多練習
- 習題 25: 更多更多的練習
- 習題 26: 恭喜你,現在可以考試了!
- 習題 27: 記住邏輯關系
- 習題 28: 布爾表達式練習
- 習題 29: 如果(if)
- 習題 30: Else 和 If
- 習題 31: 作出決定
- 習題 32: 循環和列表
- 習題 33: While 循環
- 習題 34: 訪問列表的元素
- 習題 35: 分支和函數
- 習題 36: 設計和調試
- 習題 37: 復習各種符號
- 習題 38: 閱讀代碼
- 習題 39: 列表的操作
- 習題 40: 字典, 可愛的字典
- 習題 41: 來自 Percal 25 號行星的哥頓人(Gothons)
- 習題 42: 物以類聚
- 習題 43: 你來制作一個游戲
- 習題 44: 給你的游戲打分
- 習題 45: 對象、類、以及從屬關系
- 習題 46: 一個項目骨架
- 習題 47: 自動化測試
- 習題 48: 更復雜的用戶輸入
- 習題 49: 創建句子
- 習題 50: 你的第一個網站
- 習題 51: 從瀏覽器中獲取輸入
- 習題 52: 創建你的 web 游戲
- 下一步
- 老程序員的建議