从浏览器中获取输入
让浏览器显示是一件很有趣的事,但如果能让用户通过表单(form)向应用程序提交文本就更有趣了。在此,我们将使用表单改进 你的web应用程序,并且将用户相关的信息保存到他们的会话(session)中
web的工作原理
关于web的工作原理,这里讲的并不完整,但相当准确,在你的程序出错时,他会帮你找到出错的原因。另外如果你理解了表单的应用,那创建表单对你来说就会更容易。
我将以一张简单的图说起,它向你展示了web请求的各个部分,以及信息传递的大致流程。
为方便讲述HTTP请求(request)的流程,我在每个线条上加了字母标签以示区别。
1. 你在浏览器中输入网址http://learnpythonthehardway.org/,然后浏览器会通过你的计算机的网络设备发送请求(线路A)
2. 你的请求被传送到互联网(线路B),然后在抵达远程服务器(线路C),然后我的服务器将接受这个请求。
3. 服务器接受请求后,我的web应用程序就去处理这个请求(线路D),然后我的python代码就会去运行index.GET这个“处理程序”(handler)
4. 在代码返回的时候,我的python服务器就会发出响应,这个响应会再通过线路D传递到你的浏览器。
5. 这个网站所在的服务器将获取由线路D发出的响应,然后通过线路C传至因特网。
6. 响应通过互联网由线路B传至你的计算机,计算机的网卡在通过线路A将响应传给你的浏览器。
7. 最后,你的浏览器显示了这个响应的内容。
以上涉及的你应该掌握的术语,以便在谈论web应用程序时你能明白且应用它们。
- 浏览器(browser)。你每天再用的软件。它的作用是接收你输入到地址栏的网址(如http://learnpythonthehardway.org),然后使用该信息向该网址对应的服务器提出请求。
- 地址(address)。通常这是一个像http://learnpythonthehardway.org/一样的URL(Uniform Resource Locator,统一资源定位器),它告诉浏览器该打开哪个网站。前面的http指出了你要使用的协议,这里用的是“超文本传输协议”(Hyper-Text Transport Protocol,http)。你还可以试试ftp://ibiblio.org/,这是一个”文本传输协议”(File Transport Protocol, FTP)的例子。learnpythonthehardway.org这部分是“主机名”(hostname),也就是一个便于人阅读和记忆的地址,主机名会被分配到一串叫做“IP地址”的数字上面,这个“IP地址”就相当于网络中一台计算机的电话号码,通过这个号码可以访问这台计算机。最后,URL中还可以跟随一个“路径”,例如http://learnpythonthehardway.org/book/中的/book/,它对应的是服务器上的某个文件或者某些资源,通过访问这样的网址,你可向服务器发出请求,然后获取这些资源。网站地址还有很多别的组成部分,不过上述讲的最主要。
- 连接(connection)。一旦浏览器知道了你用的协议(http)、你想联络的服务器及要从服务器上获得的资源,它就要去创建一个连接。这个过程中,浏览器让操作系统(operating System, OS)打开计算机的一个“端口”(port)(通常是80端口),端口准备好以后,操作系统会回传给你的程序一个类似文件的东西,它所做的事情就是通过网络传输和接收数据,让你的计算机和learnpythonthehardway.org这个网站所属的服务器之间实现数据交流。但你使用http://localhost:8080/访问自己的站点时,发生的事情其实是一样的,只不过这次你告诉了浏览器要访问的是你自己的计算机(localhost),要使用的端口不是默认的80,而是8080.你还可以直接访问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应用程序为例,你发送的其实还是一样的东西,包括首部信息也一样,只不过这些数据是你用python代码即时生成的。
以上就是浏览器如何访问网站的最快的快速课程了。你要弄明白上述流程。有一个好的办法是,对照上面的图,将你在习题50中创建的web应用程序的内容分为几部分,让其中的各部分对应到上面的图。如果你能正确将程序的各部分对应到这张图,你就大致开始明白它的工作原理了。
表单的工作原理
熟悉“表单”最好的办法就是写一个可以接收表单数据的程序出来,然后看你可以对它做些什么。先将你的bin/app.py修改成下面的样子。
# form_test.py
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()
重启你的web应用程序(按Ctrl+C后重新运行),确认它已运行起来,然后使用浏览器访问http://localhost:8080/hello,这是浏览器应该会显示“I just wanted to say Hello, Nobady.”,接下来,将浏览器的地址改为http://localhost:8080/hello?name=Frank,然后你可看到页面显示为“Hello, Frank.”最后将name=Frank修改为你自己的名字,你就可以看到它对你再说“Hello”了。
研究一下你的程序里做过的修改:
1. 这里没有直接为greeting赋值,而是使用了web.input从浏览器获取数据。这个函数会将一组“键=值”的表述作为默认参数,解析你提供的URL中的?name=Frank部分,然后返回一个对象,你可以通过这个对象方便地访问到表单的值。
2. 然后通过form对象的form.name属性为greeting赋值
3. 其他内容和以前一样
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:
# 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改成下面样子
# post_form.py
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()
都写好以后重启web应用程序,然后通过浏览器访问它。
这次访问你会看到一个表单,它要求你输入“一个问候语句”(A Greeting)和“你的名字”(Your Name),等你输入完后点击“提交”(Submit)按钮,它就会输出一个正常的问候页面,不过这一次你的URL还是http://localhost:8080/hello,并没有包含你提交的参数。
在hello_form.html里面关键的一行是<form action="/hello" method="POST">,它告诉你浏览器以下内容。
1. 从表单中的各个栏位收集用户输入的数据。
2. 让浏览器使用一种POST类型的请求,将这些数据发送给服务器。这是另外一种浏览器请求,它会将表单栏位“隐藏”起来。
3. 将这个请求发送至/hello URL,这是由action="/hello"告诉浏览器的。
你可以看到两端<input>标签的名字(name)属性和代码中的变量是对应的,另外我们在class index中使用的不再只是GET方法,而是另一个POST方法。这个新程序的工作原理如下。
1. 浏览器访问web应用程序的/hello目录,它发送了一个GET请求,于是我们的index.GET函数就运行并返回了hello_form。
2. 你填好了浏览器的表单,然后浏览器依照<form>中的要求,将数据通过POST请求的方式发给web应用程序。
3. web应用程序运行了index.POST方法(而不是index.GET方法)来处理这个请求。
4. 这个index.POST方法完成了它正常的功能,将hello页面返回,这里并没有新的东西,只是一个新的函数名称而已。
作为练习,在templates/index.html中添加一个链接,让它指向/hello,这样你可以反复填写并提交表单查看结果。确认你可以解释清楚这个链接的工作原理,以及它是如何让你实现在templates/index.html和templates/hello_form.html之间循环跳转的,还有就是要明白你新修改过的python代码,清楚在什么情况下会运行到那一部分代码。
创建布局模板
在下一个习题中创建游戏的过程中,你需要创建很多的小HTML页面。如果你每次都写一个完整的网页,你会很快感觉到厌烦。幸运的是,你可以创建一个“布局模板”(layout template),也就是一种提供了通用的头文件和脚注的外壳模板,你可以用它将你所有的其他网页包裹起来。好程序员会尽可能减少重复动作,所以布局模板很重要。
将templates/index.html修改成下面这个样子
# index_laid_out.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修改如下。
# hello_form_laid_out.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>
上面这些修改的目的是将每一个页面顶部和底部的反复用到的“样板代码”代码剥掉。这些被剥掉的代码会被放到一个单独的templates/layout.html文件中,此后,这些反复用到的代码就由templates/layout.html来提供。
改好以后,创建一个templates/layout.html文件。
# layout.html
$def with (content)
<html><head>
<title>Gothons Form Planet Percal #25</title>
</head>
<body>
$:content
</body></html>
这个文件和普通模板文件类似,只是其他模板的内容将被传递给它,然后他会将其他模板的内容“包裹”起来。任何写在这里的内容都无需写在别的模板中了。要注意$:content的用法,这和其他模板变量有些不同。
最后一步,将render对象改成这样:
render = web.templates.render('templates/', base="layout")
这会告诉lpthw.web去使用templates/layout.html作为其他模板的基础模板。重启你的应用从程序观察一下,然后试着用各种方法修改你的布局模板,不要修改别的模板,看看输出会有什么变化。
为表单撰写自动测试代码
使用浏览器测试web应用程序很容易,只要点击刷新按钮就可以了。但如果可以写一些代码来测试我们的程序,为什么还要重复手动测试呢?你需要为你的web应用程序写一个小测试。会用到ex47中学过的一些东西,如果你不记得,回去复习。
为了让python加载bin/app.py并进行测试,需要点准备工作。首先创建一个bin/__init__.py空文件,这样python就会将bin/当做一个目录了。
我还为lpthw.web创建了一个简单的小程序,让你断言(assert)web应用程序的响应,这个函数有一个很合适的名字:assert_response。创建一个tests/tools.py文件:
#tools.py
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)
准备好这个文件以后,你就可以为bin/app.py写自动测试代码了。创建一个叫tests/app_tests.py的新文件:
# app_tests.py
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 /helloresp = app.request("/hello")
assert_response(resp)
# make sure default values work for the formresp = app. request("/hello", method="POST")
assert_response(resp, contains="Nobody")
# test that we get expected valuesdata = {'name': 'Zed', 'greet': 'Hola'}
resp = app.request("/hello", method="POST", data=data)
assert_response(resp, contains="Zed")
最后,使用nosetests运行这个测试脚本,测试你的web应用程序。
$ nosetests
.
---------------------------------------------------------------------------
Ran 1 test in 0.059s
OK
这里我做的就是将bin/app.py这个模块中的整个web应用程序都导入进来,然后手动运行这个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去实现。
2. 难题:试着研究一下如何进行文件上传,通过网页上传一张图像,然后将其保存到磁盘中。
3. 更难:找到HTTP RFC 文件(讲述HTTP工作原理的技术文件),然后尽力阅读。很无趣的文档,但有时你会用到里边的一些知识。
4. 难题:找人帮你设置一个web服务器,如Apache、Nginx或thttpd。试着让服务器伺服一下你创建的.html和.css文件。如果失败了也没关系,web服务器本就有点让人失望。
5. 歇息一下,然后尝试多创建一些web应用程序。你应仔细阅读web.py(它和lpthw.web一样)中关于回话(session)的内容,这样你就明白如何保持用户的状态信息。
常见问题回答
我看到了ImportError "No module named bin.app"。
再次说明,要么是你引用路径不对,要么是没创建bin/__init__.py文件,要么是在shell中没有设置PYTHONPATH=.。记住这些解决方案!!!其经常发生。
运行模板时发生__templates__() takes no arguments (1 given)错误。
很可能忘记在模板开头放置$def with (greeting) 或者类似的变量声明。