习题 51: 从浏览器中获取输入
表单的工作原理
首先写一个可以接受表单数据的程序出来,先将你的bin/app.py修改成如下样子:
import web
urls = (
'/hello','Index'
)
app = web.application(urls, globals())
render = web.template.render('templates/')
class Index(object):
def GET(self):
#使用了web.input从浏览器获取数据,
#这个函数会将一组 key=value 的表述作为默认参数,
#解析你提供的URL中的?name=Frank部分,然后返回一个对象,你可以通过这个对象方便地访问到表 单的值。
form = web.input(name = "Nobody")
#通过form对象的form.name属性为greeting赋值
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, Nobody.”,接下来,将浏览器的地址改成http://localhost:8080/hello?name=Frank,然后你可以看到页面显示为“I just wanted to say Hello, Frank. ”,最后将 nam= Frank修改为你自己的名字,你就可以看到它对你说“Hello”了。
URL 中该还可以包含多个参数。将本例的 URL 改成这样子:http://localhost:8080/hello?name=Frank&greet=Hola 。然后修改代码,让它去获取 form.name和form.greet ,如下所示:
greeting = "%s, %s" % (form.greet,form.name)
在web.input中为greet设一个默认值,另外你还可以设 greet=None,这样你可以通过程序检查 的 值是否存在,然后提供一个比较好的错误信息出来,例如:
form = web.input(name = "Nobody", greet = None)
if form.greet:
#通过form对象的form.name属性为greeting赋值
greeting = "%s, %s" % (form.greet,form.name)
return render.index(greeting = greeting)
else:
return "ERROR: greet is required."
修改完毕后,试着访问新的URL。
创建HTML表单
你可以通过 URL 参数实现表单提交,不过这样看上去有些丑陋,而且不方便一 般人使用,你真正需要的是一个“POST 表单”,这是一种包含了<form>标签的 特殊 HTML 文件。这种表单收集用户输入并将其传递给你的 web 程序,这和 你上面实现的目的基本是一样的。
让我们来快速创建一个,从中你可以看出它的工作原理。你需要创建一个新的 HTML 文件, 叫做 :
templates/hello_form.html:
<html>
<head>
<title>Sample Web Form #25</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改成这样:
##-- coding: utf-8 --
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">,它告诉你的浏览器以下内容:
- 从表单中的各个栏位手机用户输入的数据。
- 让浏览器使用一种POST类型的请求,将这些数据发送给服务器。这是另外一种浏览器请求,它会将表单栏位“隐藏”起来。
- 将这个请求发送到/hello URL,这是由action="/hello"告诉浏览器的。
你可以看到两段 标签的名字属性(name)和代码中的变量是对应的,另 外我们在 中使用的不再只是 GET 方法,而是另一个 POST 方法。
这个新程序的工作原理如下:
- 浏览器访问到web应用程序的/hello目录,它发送了一个GET请求,于是我们的index.GET函数就运行并返回了hello_form。
- 浏览器对hello_form进行渲染,你填好了浏览器的表单,然后浏览器依照<form>中的要求,将数据通过POST请求的方式发给web程序。
- web程序运行了index.POST方法来处理这个请求。
- 这个index.POST方法完成它的正常的功能,将hello页面返回,这里并没有新的东西,只是一个新函数名称而已。
创建布局模板(layout template)
“布局模板”,也就是一种提供了通用的头文件和脚注的外壳模板,你可以用它将 你所有的其他网页包裹起来。
将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)
<!DOCTYPE html>
<html>
<head>
<title>Gothons From Planet Percal #25</title>
</head>
<body>
$:content
</body>
</html>
这个文件和普通的模板文件类似,不过其它的模板的内容将被传递给它,然后它 会将其它 模板的内容“包裹”起来。
最后一步,就是将render对象改成这样:
render = web.template.render('templates/', base="layout")
这会告诉lpthw.web让它去使用templates/layout.html作为其它模板的基础模板。
为表单撰写自动测试代码
为了让 Python 加载 并进行测试,你需要先做一点准备工作。首先创建一个bin/__init__空文件,这样 Python 就会将bin/当作一个目录了。
我还为lpthw.web创建了一个简单的小函数,让你判断(assert)web 程序的响 应,这个函数有一个很合适的名字,就叫assert_resonpse。创建一个 tests/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_test.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 /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")
最后,使用 nosetests 运行测试脚本,然后测试你的 web 程序。
第一次执行自动化测试总是报以下错误,提示没有bin.app模块。
$ nosetests
E
======================================================================
ERROR: Failure: ImportError (No module named bin.app)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/local/Cellar/numpy/1.16.2/libexec/nose/lib/python2.7/site-packages/nose/loader.py", line 418, in loadTestsFromName
addr.filename, addr.module)
File "/usr/local/Cellar/numpy/1.16.2/libexec/nose/lib/python2.7/site-packages/nose/importer.py", line 47, in importFromPath
return self.importFromDir(dir_path, fqname)
File "/usr/local/Cellar/numpy/1.16.2/libexec/nose/lib/python2.7/site-packages/nose/importer.py", line 94, in importFromDir
mod = load_module(part_fqname, fh, filename, desc)
File "/Users/douyueyue/study/python_practise/projects/gothonweb/tests/app_tests.py", line 2, in <module>
from bin.app import app
ImportError: No module named bin.app
----------------------------------------------------------------------
Ran 1 test in 0.007s
FAILED (errors=1)
原因是:未按照书中的步骤在bin目录下创建一个bin/__init__空文件。修改后测试通过。
/gothonweb$ nosetests
.
----------------------------------------------------------------------
Ran 1 test in 0.115s
OK
加分习题
- 阅读和 HTML 相关的更多资料,然后为你的表单设计一个更好的输出格式。你可 以先在纸上设计出来,然后用 HTML 去实现它。
- 这是一道难题,试着研究一下如何进行文件上传,通过网页上传一张图像,然后将 其保存到磁盘中。
- 更难的难题,找到 HTTP RFC 文件(讲述 HTTP 工作原理的技术文件),然后努 力阅读一下。这是一篇很无趣的文档,不过偶尔你会用到里边的一些知识。
- 又是一道难题,找人帮你设置一个 web 服务器,例如 Apache、Nginx、或者 thttpd。 试着让服务器伺服一下你创建的 .html 和 .css 文件。如果失败了也没关系,web 服务器本来就都有点挫。
- 完成上面的任务后休息一下,然后试着多创建一些 web 程序出来。你应该仔细阅 读web.py(它和 lpthw.web 是同一个程序)中关于会话(session)的内容,这样你可 以 明白如何保持用户的状态信息。